Example #1
0
class ContestHandler(BaseHandler):
    """A handler that has a contest attached.

    Most of the RequestHandler classes in this application will be a
    child of this class.

    """
    def prepare(self):
        super(ContestHandler, self).prepare()
        self.choose_contest()

        self._ = self.locale.translate

        if self.is_multi_contest():
            self.contest_url = \
                create_url_builder(self.url(self.contest.name))
        else:
            self.contest_url = self.url

        # Run render_params() now, not at the beginning of the request,
        # because we need contest_name
        self.r_params = self.render_params()

    def choose_contest(self):
        """Fill self.contest using contest passed as argument or path.

        If a contest was specified as argument to CWS, fill
        self.contest with that; otherwise extract it from the URL path.

        """
        if self.is_multi_contest():
            # Choose the contest found in the path argument
            # see: https://github.com/tornadoweb/tornado/issues/1673
            contest_name = self.path_args[0]

            # Select the correct contest or return an error
            try:
                self.contest = self.contest_list[contest_name]
            except KeyError:
                self.contest = Contest(
                    name=contest_name, description=contest_name)
                self.r_params = self.render_params()
                raise tornado.web.HTTPError(404)
        else:
            # Select the contest specified on the command line
            self.contest = Contest.get_from_id(
                self.application.service.contest_id, self.sql_session)

    def get_current_user(self):
        """Return the currently logged in participation.

        The name is get_current_user because tornado requires that
        name.

        The participation is obtained from one of the possible sources:
        - if IP autologin is enabled, the remote IP address is matched
          with the participation IP address; if a match is found, that
          participation is returned; in case of errors, None is returned;
        - if username/password authentication is enabled, and the cookie
          is valid, the corresponding participation is returned, and the
          cookie is refreshed.

        After finding the participation, IP login and hidden users
        restrictions are checked.

        In case of any error, or of a login by other sources, the
        cookie is deleted.

        return (Participation|None): the participation object for the
            user logged in for the running contest.

        """
        cookie_name = self.contest.name + "_login"

        participation = None

        if self.contest.ip_autologin:
            try:
                participation = self._get_current_user_from_ip()
                # If the login is IP-based, we delete previous cookies.
                if participation is not None:
                    self.clear_cookie(cookie_name)
            except RuntimeError:
                return None

        if participation is None \
                and self.contest.allow_password_authentication:
            participation = self._get_current_user_from_cookie()

        if participation is None:
            self.clear_cookie(cookie_name)
            return None

        # Check if user is using the right IP (or is on the right subnet),
        # and that is not hidden if hidden users are blocked.
        ip_login_restricted = \
            self.contest.ip_restriction and participation.ip is not None \
            and not check_ip(self.request.remote_ip, participation.ip)
        hidden_user_restricted = \
            participation.hidden and self.contest.block_hidden_participations
        if ip_login_restricted or hidden_user_restricted:
            self.clear_cookie(cookie_name)
            participation = None

        return participation

    def _get_current_user_from_ip(self):
        """Return the current participation based on the IP address.

        return (Participation|None): the only participation matching
            the remote IP address, or None if no participations could
            be matched.

        raise (RuntimeError): if there is more than one participation
            matching the remote IP address.

        """
        try:
            # We encode it as a network (i.e., we assign it a /32 or
            # /128 mask) since we're comparing it for equality with
            # other networks.
            remote_ip = ipaddress.ip_network(unicode(self.request.remote_ip))
        except ValueError:
            return None
        participations = self.sql_session.query(Participation)\
            .filter(Participation.contest == self.contest)\
            .filter(Participation.ip.any(remote_ip))

        # If hidden users are blocked we ignore them completely.
        if self.contest.block_hidden_participations:
            participations = participations\
                .filter(Participation.hidden.is_(False))

        participations = participations.all()

        if len(participations) == 1:
            return participations[0]

        # Having more than participation with the same IP,
        # is a mistake and should not happen. In such case,
        # we disallow login for that IP completely, in order to
        # make sure the problem is noticed.
        if len(participations) > 1:
            logger.error("%d participants have IP %s while"
                         "auto-login feature is enabled." % (
                             len(participations), remote_ip))
            raise RuntimeError("More than one participants with the same IP.")

    def _get_current_user_from_cookie(self):
        """Return the current participation based on the cookie.

        If a participation can be extracted, the cookie is refreshed.

        return (Participation|None): the participation extracted from
            the cookie, or None if not possible.

        """
        cookie_name = self.contest.name + "_login"

        if self.get_secure_cookie(cookie_name) is None:
            return None

        # Parse cookie.
        try:
            cookie = pickle.loads(self.get_secure_cookie(cookie_name))
            username = cookie[0]
            password = cookie[1]
            last_update = make_datetime(cookie[2])
        except:
            return None

        # Check if the cookie is expired.
        if self.timestamp - last_update > \
                timedelta(seconds=config.cookie_duration):
            return None

        # Load participation from DB and make sure it exists.
        participation = self.sql_session.query(Participation)\
            .join(Participation.user)\
            .options(contains_eager(Participation.user))\
            .filter(Participation.contest == self.contest)\
            .filter(User.username == username)\
            .first()
        if participation is None:
            return None

        # Check that the password is correct (if a contest-specific
        # password is defined, use that instead of the user password).
        if participation.password is None:
            correct_password = participation.user.password
        else:
            correct_password = participation.password
        if password != correct_password:
            return None

        if self.refresh_cookie:
            self.set_secure_cookie(cookie_name,
                                   pickle.dumps((username,
                                                 password,
                                                 make_timestamp())),
                                   expires_days=None)

        return participation

    def get_user_locale(self):
        self.langs = self.application.service.langs
        lang_codes = self.langs.keys()

        if self.contest.allowed_localizations:
            lang_codes = filter_language_codes(
                lang_codes, self.contest.allowed_localizations)

        # Select the one the user likes most.
        basic_lang = 'en'

        if self.contest.allowed_localizations:
            basic_lang = lang_codes[0].replace("_", "-")

        http_langs = [lang_code.replace("_", "-") for lang_code in lang_codes]
        self.browser_lang = parse_accept_header(
            self.request.headers.get("Accept-Language", ""),
            LanguageAccept).best_match(http_langs, basic_lang)

        self.cookie_lang = self.get_cookie("language", None)

        if self.cookie_lang in http_langs:
            lang_code = self.cookie_lang
        else:
            lang_code = self.browser_lang

        self.set_header("Content-Language", lang_code)
        return self.langs[lang_code.replace("-", "_")]

    @staticmethod
    def _get_token_status(obj):
        """Return the status of the tokens for the given object.

        obj (Contest or Task): an object that has the token_* attributes.
        return (int): one of 0 (disabled), 1 (enabled/finite) and 2
                      (enabled/infinite).

        """
        if obj.token_mode == "disabled":
            return 0
        elif obj.token_mode == "finite":
            return 1
        elif obj.token_mode == "infinite":
            return 2
        else:
            raise RuntimeError("Unknown token_mode value.")

    def render_params(self):
        ret = super(ContestHandler, self).render_params()

        ret["contest"] = self.contest

        if hasattr(self, "contest_url"):
            ret["contest_url"] = self.contest_url

        ret["phase"] = self.contest.phase(self.timestamp)

        ret["printing_enabled"] = (config.printer is not None)
        ret["questions_enabled"] = self.contest.allow_questions
        ret["testing_enabled"] = self.contest.allow_user_tests

        if self.current_user is not None:
            participation = self.current_user

            res = compute_actual_phase(
                self.timestamp, self.contest.start, self.contest.stop,
                self.contest.analysis_start if self.contest.analysis_enabled
                else None,
                self.contest.analysis_stop if self.contest.analysis_enabled
                else None,
                self.contest.per_user_time, participation.starting_time,
                participation.delay_time, participation.extra_time)

            ret["actual_phase"], ret["current_phase_begin"], \
                ret["current_phase_end"], ret["valid_phase_begin"], \
                ret["valid_phase_end"] = res

            if ret["actual_phase"] == 0:
                ret["phase"] = 0

            # set the timezone used to format timestamps
            ret["timezone"] = get_timezone(participation.user, self.contest)

        # some information about token configuration
        ret["tokens_contest"] = self._get_token_status(self.contest)

        t_tokens = sum(self._get_token_status(t) for t in self.contest.tasks)
        if t_tokens == 0:
            ret["tokens_tasks"] = 0  # all disabled
        elif t_tokens == 2 * len(self.contest.tasks):
            ret["tokens_tasks"] = 2  # all infinite
        else:
            ret["tokens_tasks"] = 1  # all finite or mixed

        # TODO Now all language names are shown in the active language.
        # It would be better to show them in the corresponding one.
        ret["lang_names"] = {}

        # Get language codes for allowed localizations
        lang_codes = self.langs.keys()
        if len(self.contest.allowed_localizations) > 0:
            lang_codes = filter_language_codes(
                lang_codes, self.contest.allowed_localizations)
        for lang_code, trans in self.langs.iteritems():
            language_name = None
            # Filter lang_codes with allowed localizations
            if lang_code not in lang_codes:
                continue
            try:
                language_name = translate_language_country_code(
                    lang_code, trans)
            except ValueError:
                language_name = translate_language_code(
                    lang_code, trans)
            ret["lang_names"][lang_code.replace("_", "-")] = language_name

        ret["cookie_lang"] = self.cookie_lang
        ret["browser_lang"] = self.browser_lang

        return ret

    def get_login_url(self):
        """The login url depends on the contest name, so we can't just
        use the "login_url" application parameter.

        """
        return self.contest_url()
Example #2
0
class ContestHandler(BaseHandler):
    """A handler that has a contest attached.

    Most of the RequestHandler classes in this application will be a
    child of this class.

    """
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.contest_url = None

    def prepare(self):
        self.choose_contest()

        if self.contest.allowed_localizations:
            lang_codes = filter_language_codes(
                list(self.available_translations.keys()),
                self.contest.allowed_localizations)
            self.available_translations = dict(
                (k, v) for k, v in self.available_translations.items()
                if k in lang_codes)

        super().prepare()

        if self.is_multi_contest():
            self.contest_url = self.url[self.contest.name]
        else:
            self.contest_url = self.url

        # Run render_params() now, not at the beginning of the request,
        # because we need contest_name
        self.r_params = self.render_params()

    def choose_contest(self):
        """Fill self.contest using contest passed as argument or path.

        If a contest was specified as argument to CWS, fill
        self.contest with that; otherwise extract it from the URL path.

        """
        if self.is_multi_contest():
            # Choose the contest found in the path argument
            # see: https://github.com/tornadoweb/tornado/issues/1673
            contest_name = self.path_args[0]

            # Select the correct contest or return an error
            self.contest = self.sql_session.query(Contest)\
                .filter(Contest.name == contest_name).first()
            if self.contest is None:
                self.contest = Contest(name=contest_name,
                                       description=contest_name)
                # render_params in this class assumes the contest is loaded,
                # so we cannot call it without a fully defined contest. Luckily
                # the one from the base class is enough to display a 404 page.
                super().prepare()
                self.r_params = super().render_params()
                raise tornado.web.HTTPError(404)
        else:
            # Select the contest specified on the command line
            self.contest = Contest.get_from_id(self.service.contest_id,
                                               self.sql_session)

    def get_current_user(self):
        """Return the currently logged in participation.

        The name is get_current_user because tornado requires that
        name.

        The participation is obtained from one of the possible sources:
        - if IP autologin is enabled, the remote IP address is matched
          with the participation IP address; if a match is found, that
          participation is returned; in case of errors, None is returned;
        - if username/password authentication is enabled, and the cookie
          is valid, the corresponding participation is returned, and the
          cookie is refreshed.

        After finding the participation, IP login and hidden users
        restrictions are checked.

        In case of any error, or of a login by other sources, the
        cookie is deleted.

        return (Participation|None): the participation object for the
            user logged in for the running contest.

        """
        cookie_name = self.contest.name + "_login"
        cookie = self.get_secure_cookie(cookie_name)

        try:
            # In py2 Tornado gives us the IP address as a native binary
            # string, whereas ipaddress wants text (unicode) strings.
            ip_address = ipaddress.ip_address(str(self.request.remote_ip))
        except ValueError:
            logger.warning("Invalid IP address provided by Tornado: %s",
                           self.request.remote_ip)
            return None

        participation, cookie = authenticate_request(self.sql_session,
                                                     self.contest,
                                                     self.timestamp, cookie,
                                                     ip_address)

        if cookie is None:
            self.clear_cookie(cookie_name)
        elif self.refresh_cookie:
            self.set_secure_cookie(cookie_name, cookie, expires_days=None)

        return participation

    def render_params(self):
        ret = super().render_params()

        ret["contest"] = self.contest

        if self.contest_url is not None:
            ret["contest_url"] = self.contest_url

        ret["phase"] = self.contest.phase(self.timestamp)

        ret["printing_enabled"] = (config.printer is not None)
        ret["questions_enabled"] = self.contest.allow_questions
        ret["testing_enabled"] = self.contest.allow_user_tests

        if self.current_user is not None:
            participation = self.current_user
            ret["participation"] = participation
            ret["user"] = participation.user

            res = compute_actual_phase(
                self.timestamp, self.contest.start, self.contest.stop,
                self.contest.analysis_start if self.contest.analysis_enabled
                else None, self.contest.analysis_stop
                if self.contest.analysis_enabled else None,
                self.contest.per_user_time, participation.starting_time,
                participation.delay_time, participation.extra_time)

            ret["actual_phase"], ret["current_phase_begin"], \
                ret["current_phase_end"], ret["valid_phase_begin"], \
                ret["valid_phase_end"] = res

            if ret["actual_phase"] == 0:
                ret["phase"] = 0

            # set the timezone used to format timestamps
            ret["timezone"] = get_timezone(participation.user, self.contest)

        # some information about token configuration
        ret["tokens_contest"] = self.contest.token_mode

        t_tokens = set(t.token_mode for t in self.contest.tasks)
        if len(t_tokens) == 1:
            ret["tokens_tasks"] = next(iter(t_tokens))
        else:
            ret["tokens_tasks"] = TOKEN_MODE_MIXED

        return ret

    def get_login_url(self):
        """The login url depends on the contest name, so we can't just
        use the "login_url" application parameter.

        """
        return self.contest_url()

    def get_task(self, task_name):
        """Return the task in the contest with the given name.

        task_name (str): the name of the task we are interested in.

        return (Task|None): the corresponding task object, if found.

        """
        return self.sql_session.query(Task) \
            .filter(Task.contest == self.contest) \
            .filter(Task.name == task_name) \
            .one_or_none()

    def get_submission(self, task, submission_num):
        """Return the num-th contestant's submission on the given task.

        task (Task): a task for the contest that is being served.
        submission_num (str): a positive number, in decimal encoding.

        return (Submission|None): the submission_num-th submission, in
            chronological order, that was sent by the currently logged
            in contestant on the given task (None if not found).

        """
        return self.sql_session.query(Submission) \
            .filter(Submission.participation == self.current_user) \
            .filter(Submission.task == task) \
            .order_by(Submission.timestamp) \
            .offset(int(submission_num) - 1) \
            .first()

    def get_user_test(self, task, user_test_num):
        """Return the num-th contestant's test on the given task.

        task (Task): a task for the contest that is being served.
        user_test_num (str): a positive number, in decimal encoding.

        return (UserTest|None): the user_test_num-th user test, in
            chronological order, that was sent by the currently logged
            in contestant on the given task (None if not found).

        """
        return self.sql_session.query(UserTest) \
            .filter(UserTest.participation == self.current_user) \
            .filter(UserTest.task == task) \
            .order_by(UserTest.timestamp) \
            .offset(int(user_test_num) - 1) \
            .first()

    def add_notification(self, subject, text, level, text_params=None):
        subject = self._(subject)
        text = self._(text)
        if text_params is not None:
            text %= text_params
        self.service.add_notification(self.current_user.user.username,
                                      self.timestamp, subject, text, level)

    def notify_success(self, subject, text, text_params=None):
        self.add_notification(subject, text, NOTIFICATION_SUCCESS, text_params)

    def notify_warning(self, subject, text, text_params=None):
        self.add_notification(subject, text, NOTIFICATION_WARNING, text_params)

    def notify_error(self, subject, text, text_params=None):
        self.add_notification(subject, text, NOTIFICATION_ERROR, text_params)
Example #3
0
class ContestHandler(BaseHandler):
    """A handler that has a contest attached.

    Most of the RequestHandler classes in this application will be a
    child of this class.

    """
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.contest_url = None

    def prepare(self):
        self.choose_contest()

        if self.contest.allowed_localizations:
            lang_codes = filter_language_codes(
                list(self.available_translations.keys()),
                self.contest.allowed_localizations)
            self.available_translations = dict(
                (k, v) for k, v in self.available_translations.items()
                if k in lang_codes)

        super().prepare()

        if self.is_multi_contest():
            self.contest_url = self.url[self.contest.name]
        else:
            self.contest_url = self.url

        # Run render_params() now, not at the beginning of the request,
        # because we need contest_name
        self.r_params = self.render_params()

    def choose_contest(self):
        """Fill self.contest using contest passed as argument or path.

        If a contest was specified as argument to CWS, fill
        self.contest with that; otherwise extract it from the URL path.

        """
        if self.is_multi_contest():
            # Choose the contest found in the path argument
            # see: https://github.com/tornadoweb/tornado/issues/1673
            contest_name = self.path_args[0]

            # Select the correct contest or return an error
            self.contest = self.sql_session.query(Contest)\
                .filter(Contest.name == contest_name).first()
            if self.contest is None:
                self.contest = Contest(
                    name=contest_name, description=contest_name)
                # render_params in this class assumes the contest is loaded,
                # so we cannot call it without a fully defined contest. Luckily
                # the one from the base class is enough to display a 404 page.
                self.r_params = super().render_params()
                raise tornado.web.HTTPError(404)
        else:
            # Select the contest specified on the command line
            self.contest = Contest.get_from_id(
                self.service.contest_id, self.sql_session)

    def get_current_user(self):
        """Return the currently logged in participation.

        The name is get_current_user because tornado requires that
        name.

        The participation is obtained from one of the possible sources:
        - if IP autologin is enabled, the remote IP address is matched
          with the participation IP address; if a match is found, that
          participation is returned; in case of errors, None is returned;
        - if username/password authentication is enabled, and the cookie
          is valid, the corresponding participation is returned, and the
          cookie is refreshed.

        After finding the participation, IP login and hidden users
        restrictions are checked.

        In case of any error, or of a login by other sources, the
        cookie is deleted.

        return (Participation|None): the participation object for the
            user logged in for the running contest.

        """
        cookie_name = self.contest.name + "_login"
        cookie = self.get_secure_cookie(cookie_name)

        try:
            # In py2 Tornado gives us the IP address as a native binary
            # string, whereas ipaddress wants text (unicode) strings.
            ip_address = ipaddress.ip_address(str(self.request.remote_ip))
        except ValueError:
            logger.warning("Invalid IP address provided by Tornado: %s",
                           self.request.remote_ip)
            return None

        participation, cookie = authenticate_request(
            self.sql_session, self.contest, self.timestamp, cookie, ip_address)

        if cookie is None:
            self.clear_cookie(cookie_name)
        elif self.refresh_cookie:
            self.set_secure_cookie(cookie_name, cookie, expires_days=None)

        return participation

    def render_params(self):
        ret = super().render_params()

        ret["contest"] = self.contest

        if self.contest_url is not None:
            ret["contest_url"] = self.contest_url

        ret["phase"] = self.contest.phase(self.timestamp)

        ret["printing_enabled"] = (config.printer is not None)
        ret["questions_enabled"] = self.contest.allow_questions
        ret["testing_enabled"] = self.contest.allow_user_tests

        if self.current_user is not None:
            participation = self.current_user
            ret["participation"] = participation
            ret["user"] = participation.user

            res = compute_actual_phase(
                self.timestamp, self.contest.start, self.contest.stop,
                self.contest.analysis_start if self.contest.analysis_enabled
                else None,
                self.contest.analysis_stop if self.contest.analysis_enabled
                else None,
                self.contest.per_user_time, participation.starting_time,
                participation.delay_time, participation.extra_time)

            ret["actual_phase"], ret["current_phase_begin"], \
                ret["current_phase_end"], ret["valid_phase_begin"], \
                ret["valid_phase_end"] = res

            if ret["actual_phase"] == 0:
                ret["phase"] = 0

            # set the timezone used to format timestamps
            ret["timezone"] = get_timezone(participation.user, self.contest)

        # some information about token configuration
        ret["tokens_contest"] = self.contest.token_mode

        t_tokens = set(t.token_mode for t in self.contest.tasks)
        if len(t_tokens) == 1:
            ret["tokens_tasks"] = next(iter(t_tokens))
        else:
            ret["tokens_tasks"] = TOKEN_MODE_MIXED

        return ret

    def get_login_url(self):
        """The login url depends on the contest name, so we can't just
        use the "login_url" application parameter.

        """
        return self.contest_url()

    def get_task(self, task_name):
        """Return the task in the contest with the given name.

        task_name (str): the name of the task we are interested in.

        return (Task|None): the corresponding task object, if found.

        """
        return self.sql_session.query(Task) \
            .filter(Task.contest == self.contest) \
            .filter(Task.name == task_name) \
            .one_or_none()

    def get_submission(self, task, submission_num):
        """Return the num-th contestant's submission on the given task.

        task (Task): a task for the contest that is being served.
        submission_num (str): a positive number, in decimal encoding.

        return (Submission|None): the submission_num-th submission, in
            chronological order, that was sent by the currently logged
            in contestant on the given task (None if not found).

        """
        return self.sql_session.query(Submission) \
            .filter(Submission.participation == self.current_user) \
            .filter(Submission.task == task) \
            .order_by(Submission.timestamp) \
            .offset(int(submission_num) - 1) \
            .first()

    def get_user_test(self, task, user_test_num):
        """Return the num-th contestant's test on the given task.

        task (Task): a task for the contest that is being served.
        user_test_num (str): a positive number, in decimal encoding.

        return (UserTest|None): the user_test_num-th user test, in
            chronological order, that was sent by the currently logged
            in contestant on the given task (None if not found).

        """
        return self.sql_session.query(UserTest) \
            .filter(UserTest.participation == self.current_user) \
            .filter(UserTest.task == task) \
            .order_by(UserTest.timestamp) \
            .offset(int(user_test_num) - 1) \
            .first()

    def add_notification(self, subject, text, level, text_params=None):
        subject = self._(subject)
        text = self._(text)
        if text_params is not None:
            text %= text_params
        self.service.add_notification(self.current_user.user.username,
                                      self.timestamp, subject, text, level)

    def notify_success(self, subject, text, text_params=None):
        self.add_notification(subject, text, NOTIFICATION_SUCCESS, text_params)

    def notify_warning(self, subject, text, text_params=None):
        self.add_notification(subject, text, NOTIFICATION_WARNING, text_params)

    def notify_error(self, subject, text, text_params=None):
        self.add_notification(subject, text, NOTIFICATION_ERROR, text_params)
Example #4
0
class ContestHandler(BaseHandler):
    """A handler that has a contest attached.

    Most of the RequestHandler classes in this application will be a
    child of this class.

    """
    def __init__(self, *args, **kwargs):
        super(ContestHandler, self).__init__(*args, **kwargs)
        self.contest_url = None

    def prepare(self):
        self.choose_contest()

        if self.contest.allowed_localizations:
            lang_codes = filter_language_codes(
                list(iterkeys(self.available_translations)),
                self.contest.allowed_localizations)
            self.available_translations = dict(
                (k, v) for k, v in iteritems(self.available_translations)
                if k in lang_codes)

        super(ContestHandler, self).prepare()

        if self.is_multi_contest():
            self.contest_url = \
                create_url_builder(self.url(self.contest.name))
        else:
            self.contest_url = self.url

        # Run render_params() now, not at the beginning of the request,
        # because we need contest_name
        self.r_params = self.render_params()

    def choose_contest(self):
        """Fill self.contest using contest passed as argument or path.

        If a contest was specified as argument to CWS, fill
        self.contest with that; otherwise extract it from the URL path.

        """
        if self.is_multi_contest():
            # Choose the contest found in the path argument
            # see: https://github.com/tornadoweb/tornado/issues/1673
            contest_name = self.path_args[0]

            # Select the correct contest or return an error
            self.contest = self.sql_session.query(Contest)\
                .filter(Contest.name == contest_name).first()
            if self.contest is None:
                self.contest = Contest(name=contest_name,
                                       description=contest_name)
                # render_params in this class assumes the contest is loaded,
                # so we cannot call it without a fully defined contest. Luckily
                # the one from the base class is enough to display a 404 page.
                self.r_params = super(ContestHandler, self).render_params()
                raise tornado.web.HTTPError(404)
        else:
            # Select the contest specified on the command line
            self.contest = Contest.get_from_id(self.service.contest_id,
                                               self.sql_session)

    def get_current_user(self):
        """Return the currently logged in participation.

        The name is get_current_user because tornado requires that
        name.

        The participation is obtained from one of the possible sources:
        - if IP autologin is enabled, the remote IP address is matched
          with the participation IP address; if a match is found, that
          participation is returned; in case of errors, None is returned;
        - if username/password authentication is enabled, and the cookie
          is valid, the corresponding participation is returned, and the
          cookie is refreshed.

        After finding the participation, IP login and hidden users
        restrictions are checked.

        In case of any error, or of a login by other sources, the
        cookie is deleted.

        return (Participation|None): the participation object for the
            user logged in for the running contest.

        """
        cookie_name = self.contest.name + "_login"

        participation = None

        if self.contest.ip_autologin:
            try:
                participation = self._get_current_user_from_ip()
                # If the login is IP-based, we delete previous cookies.
                if participation is not None:
                    self.clear_cookie(cookie_name)
            except RuntimeError:
                return None

        if participation is None \
                and self.contest.allow_password_authentication:
            participation = self._get_current_user_from_cookie()

        if participation is None:
            self.clear_cookie(cookie_name)
            return None

        # Check if user is using the right IP (or is on the right subnet),
        # and that is not hidden if hidden users are blocked.
        ip_login_restricted = \
            self.contest.ip_restriction and participation.ip is not None \
            and not check_ip(self.request.remote_ip, participation.ip)
        hidden_user_restricted = \
            participation.hidden and self.contest.block_hidden_participations
        if ip_login_restricted or hidden_user_restricted:
            self.clear_cookie(cookie_name)
            participation = None

        return participation

    def _get_current_user_from_ip(self):
        """Return the current participation based on the IP address.

        return (Participation|None): the only participation matching
            the remote IP address, or None if no participations could
            be matched.

        raise (RuntimeError): if there is more than one participation
            matching the remote IP address.

        """
        try:
            # We encode it as a network (i.e., we assign it a /32 or
            # /128 mask) since we're comparing it for equality with
            # other networks.
            remote_ip = ipaddress.ip_network(str(self.request.remote_ip))
        except ValueError:
            return None
        participations = self.sql_session.query(Participation)\
            .filter(Participation.contest == self.contest)\
            .filter(Participation.ip.any(remote_ip))

        # If hidden users are blocked we ignore them completely.
        if self.contest.block_hidden_participations:
            participations = participations\
                .filter(Participation.hidden.is_(False))

        participations = participations.all()

        if len(participations) == 1:
            return participations[0]

        # Having more than participation with the same IP,
        # is a mistake and should not happen. In such case,
        # we disallow login for that IP completely, in order to
        # make sure the problem is noticed.
        if len(participations) > 1:
            logger.error("%d participants have IP %s while"
                         "auto-login feature is enabled." %
                         (len(participations), remote_ip))
            raise RuntimeError("More than one participants with the same IP.")

    def _get_current_user_from_cookie(self):
        """Return the current participation based on the cookie.

        If a participation can be extracted, the cookie is refreshed.

        return (Participation|None): the participation extracted from
            the cookie, or None if not possible.

        """
        cookie_name = self.contest.name + "_login"

        if self.get_secure_cookie(cookie_name) is None:
            return None

        # Parse cookie.
        try:
            cookie = pickle.loads(self.get_secure_cookie(cookie_name))
            username = cookie[0]
            password = cookie[1]
            last_update = make_datetime(cookie[2])
        except:
            return None

        # Check if the cookie is expired.
        if self.timestamp - last_update > \
                timedelta(seconds=config.cookie_duration):
            return None

        # Load participation from DB and make sure it exists.
        participation = self.sql_session.query(Participation)\
            .join(Participation.user)\
            .options(contains_eager(Participation.user))\
            .filter(Participation.contest == self.contest)\
            .filter(User.username == username)\
            .first()
        if participation is None:
            return None

        # Check that the password is correct (if a contest-specific
        # password is defined, use that instead of the user password).
        if participation.password is None:
            correct_password = participation.user.password
        else:
            correct_password = participation.password
        if password != correct_password:
            return None

        if self.refresh_cookie:
            self.set_secure_cookie(cookie_name,
                                   pickle.dumps(
                                       (username, password, make_timestamp())),
                                   expires_days=None)

        return participation

    def render_params(self):
        ret = super(ContestHandler, self).render_params()

        ret["contest"] = self.contest

        if self.contest_url is not None:
            ret["contest_url"] = self.contest_url

        ret["phase"] = self.contest.phase(self.timestamp)

        ret["printing_enabled"] = (config.printer is not None)
        ret["questions_enabled"] = self.contest.allow_questions
        ret["testing_enabled"] = self.contest.allow_user_tests

        if self.current_user is not None:
            participation = self.current_user
            ret["participation"] = participation
            ret["user"] = participation.user

            res = compute_actual_phase(
                self.timestamp, self.contest.start, self.contest.stop,
                self.contest.analysis_start if self.contest.analysis_enabled
                else None, self.contest.analysis_stop
                if self.contest.analysis_enabled else None,
                self.contest.per_user_time, participation.starting_time,
                participation.delay_time, participation.extra_time)

            ret["actual_phase"], ret["current_phase_begin"], \
                ret["current_phase_end"], ret["valid_phase_begin"], \
                ret["valid_phase_end"] = res

            if ret["actual_phase"] == 0:
                ret["phase"] = 0

            # set the timezone used to format timestamps
            ret["timezone"] = get_timezone(participation.user, self.contest)

        # some information about token configuration
        ret["tokens_contest"] = self.contest.token_mode

        t_tokens = set(t.token_mode for t in self.contest.tasks)
        if len(t_tokens) == 1:
            ret["tokens_tasks"] = next(iter(t_tokens))
        else:
            ret["tokens_tasks"] = TOKEN_MODE_MIXED

        return ret

    def get_login_url(self):
        """The login url depends on the contest name, so we can't just
        use the "login_url" application parameter.

        """
        return self.contest_url()
Example #5
0
class ContestHandler(BaseHandler):
    """A handler that has a contest attached.

    Most of the RequestHandler classes in this application will be a
    child of this class.

    """
    def __init__(self, *args, **kwargs):
        super(ContestHandler, self).__init__(*args, **kwargs)
        self.contest_url = None

    def prepare(self):
        self.choose_contest()

        if self.contest.allowed_localizations:
            lang_codes = filter_language_codes(
                list(iterkeys(self.available_translations)),
                self.contest.allowed_localizations)
            self.available_translations = dict(
                (k, v) for k, v in iteritems(self.available_translations)
                if k in lang_codes)

        super(ContestHandler, self).prepare()

        if self.is_multi_contest():
            self.contest_url = \
                create_url_builder(self.url(self.contest.name))
        else:
            self.contest_url = self.url

        # Run render_params() now, not at the beginning of the request,
        # because we need contest_name
        self.r_params = self.render_params()

    def choose_contest(self):
        """Fill self.contest using contest passed as argument or path.

        If a contest was specified as argument to CWS, fill
        self.contest with that; otherwise extract it from the URL path.

        """
        if self.is_multi_contest():
            # Choose the contest found in the path argument
            # see: https://github.com/tornadoweb/tornado/issues/1673
            contest_name = self.path_args[0]

            # Select the correct contest or return an error
            self.contest = self.sql_session.query(Contest)\
                .filter(Contest.name == contest_name).first()
            if self.contest is None:
                self.contest = Contest(
                    name=contest_name, description=contest_name)
                # render_params in this class assumes the contest is loaded,
                # so we cannot call it without a fully defined contest. Luckily
                # the one from the base class is enough to display a 404 page.
                self.r_params = super(ContestHandler, self).render_params()
                raise tornado.web.HTTPError(404)
        else:
            # Select the contest specified on the command line
            self.contest = Contest.get_from_id(
                self.service.contest_id, self.sql_session)

    def get_current_user(self):
        """Return the currently logged in participation.

        The name is get_current_user because tornado requires that
        name.

        The participation is obtained from one of the possible sources:
        - if IP autologin is enabled, the remote IP address is matched
          with the participation IP address; if a match is found, that
          participation is returned; in case of errors, None is returned;
        - if username/password authentication is enabled, and the cookie
          is valid, the corresponding participation is returned, and the
          cookie is refreshed.

        After finding the participation, IP login and hidden users
        restrictions are checked.

        In case of any error, or of a login by other sources, the
        cookie is deleted.

        return (Participation|None): the participation object for the
            user logged in for the running contest.

        """
        cookie_name = self.contest.name + "_login"

        participation = None

        if self.contest.ip_autologin:
            try:
                participation = self._get_current_user_from_ip()
                # If the login is IP-based, we delete previous cookies.
                if participation is not None:
                    self.clear_cookie(cookie_name)
            except RuntimeError:
                return None

        if participation is None \
                and self.contest.allow_password_authentication:
            participation = self._get_current_user_from_cookie()

        if participation is None:
            self.clear_cookie(cookie_name)
            return None

        # Check if user is using the right IP (or is on the right subnet),
        # and that is not hidden if hidden users are blocked.
        ip_login_restricted = \
            self.contest.ip_restriction and participation.ip is not None \
            and not check_ip(self.request.remote_ip, participation.ip)
        hidden_user_restricted = \
            participation.hidden and self.contest.block_hidden_participations
        if ip_login_restricted or hidden_user_restricted:
            self.clear_cookie(cookie_name)
            participation = None

        return participation

    def _get_current_user_from_ip(self):
        """Return the current participation based on the IP address.

        return (Participation|None): the only participation matching
            the remote IP address, or None if no participations could
            be matched.

        raise (RuntimeError): if there is more than one participation
            matching the remote IP address.

        """
        try:
            # We encode it as a network (i.e., we assign it a /32 or
            # /128 mask) since we're comparing it for equality with
            # other networks.
            remote_ip = ipaddress.ip_network(str(self.request.remote_ip))
        except ValueError:
            return None
        participations = self.sql_session.query(Participation)\
            .filter(Participation.contest == self.contest)\
            .filter(Participation.ip.any(remote_ip))

        # If hidden users are blocked we ignore them completely.
        if self.contest.block_hidden_participations:
            participations = participations\
                .filter(Participation.hidden.is_(False))

        participations = participations.all()

        if len(participations) == 1:
            return participations[0]

        # Having more than participation with the same IP,
        # is a mistake and should not happen. In such case,
        # we disallow login for that IP completely, in order to
        # make sure the problem is noticed.
        if len(participations) > 1:
            logger.error("%d participants have IP %s while"
                         "auto-login feature is enabled." % (
                             len(participations), remote_ip))
            raise RuntimeError("More than one participants with the same IP.")

    def _get_current_user_from_cookie(self):
        """Return the current participation based on the cookie.

        If a participation can be extracted, the cookie is refreshed.

        return (Participation|None): the participation extracted from
            the cookie, or None if not possible.

        """
        cookie_name = self.contest.name + "_login"

        if self.get_secure_cookie(cookie_name) is None:
            return None

        # Parse cookie.
        try:
            cookie = pickle.loads(self.get_secure_cookie(cookie_name))
            username = cookie[0]
            password = cookie[1]
            last_update = make_datetime(cookie[2])
        except:
            return None

        # Check if the cookie is expired.
        if self.timestamp - last_update > \
                timedelta(seconds=config.cookie_duration):
            return None

        # Load participation from DB and make sure it exists.
        participation = self.sql_session.query(Participation)\
            .join(Participation.user)\
            .options(contains_eager(Participation.user))\
            .filter(Participation.contest == self.contest)\
            .filter(User.username == username)\
            .first()
        if participation is None:
            return None

        # Check that the password is correct (if a contest-specific
        # password is defined, use that instead of the user password).
        if participation.password is None:
            correct_password = participation.user.password
        else:
            correct_password = participation.password
        if password != correct_password:
            return None

        if self.refresh_cookie:
            self.set_secure_cookie(cookie_name,
                                   pickle.dumps((username,
                                                 password,
                                                 make_timestamp())),
                                   expires_days=None)

        return participation

    def render_params(self):
        ret = super(ContestHandler, self).render_params()

        ret["contest"] = self.contest

        if self.contest_url is not None:
            ret["contest_url"] = self.contest_url

        ret["phase"] = self.contest.phase(self.timestamp)

        ret["printing_enabled"] = (config.printer is not None)
        ret["questions_enabled"] = self.contest.allow_questions
        ret["testing_enabled"] = self.contest.allow_user_tests

        if self.current_user is not None:
            participation = self.current_user
            ret["participation"] = participation
            ret["user"] = participation.user

            res = compute_actual_phase(
                self.timestamp, self.contest.start, self.contest.stop,
                self.contest.analysis_start if self.contest.analysis_enabled
                else None,
                self.contest.analysis_stop if self.contest.analysis_enabled
                else None,
                self.contest.per_user_time, participation.starting_time,
                participation.delay_time, participation.extra_time)

            ret["actual_phase"], ret["current_phase_begin"], \
                ret["current_phase_end"], ret["valid_phase_begin"], \
                ret["valid_phase_end"] = res

            if ret["actual_phase"] == 0:
                ret["phase"] = 0

            # set the timezone used to format timestamps
            ret["timezone"] = get_timezone(participation.user, self.contest)

        # some information about token configuration
        ret["tokens_contest"] = self.contest.token_mode

        t_tokens = set(t.token_mode for t in self.contest.tasks)
        if len(t_tokens) == 1:
            ret["tokens_tasks"] = next(iter(t_tokens))
        else:
            ret["tokens_tasks"] = TOKEN_MODE_MIXED

        return ret

    def get_login_url(self):
        """The login url depends on the contest name, so we can't just
        use the "login_url" application parameter.

        """
        return self.contest_url()