def evaluate_output(task_name, input_path, output_path):
     """
     Given an input of a task, evaluate the correctness of the output
     :param task_name: Name of the task
     :param input_path: Path to the user's input file
     :param output_path: Path to the user's output file
     :return: The stdout of the checker
     """
     try:
         # call the checker and store the output
         start_time = time.monotonic()
         output = gevent.subprocess.check_output([
             ContestManager.tasks[task_name]["checker"],
             StorageManager.get_absolute_path(input_path),
             StorageManager.get_absolute_path(output_path)
         ])
         if time.monotonic() > start_time + 1:
             Logger.warning(
                 "TASK", "Evaluation of output %s "
                 "for task %s, with input %s, took %f "
                 "seconds" % (output_path, task_name, input_path,
                              time.monotonic() - start_time))
     except:
         # TODO log the stdout and stderr of the checker
         Logger.error(
             "TASK", "Error while evaluating output %s "
             "for task %s, with input %s: %s" %
             (output_path, task_name, input_path, traceback.format_exc()))
         raise
     Logger.info(
         "TASK", "Evaluated output %s for task %s, with input %s" %
         (output_path, task_name, input_path))
     return output
예제 #2
0
    def submit(self, output, source):
        """
        POST /submit
        """
        input = Database.get_input(output["input"])
        if input is None:
            Logger.warning("DB_CONSISTENCY_ERROR",
                           "Input %s not found in the db" % output["input"])
            self.raise_exc(BadRequest, "WRONG_INPUT",
                           "The provided input in invalid")
        if output["input"] != source["input"]:
            Logger.warning("POSSIBLE_CHEAT",
                           "Trying to submit wrong pair source-output")
            self.raise_exc(Forbidden, "WRONG_OUTPUT_SOURCE",
                           "The provided pair of source-output is invalid")

        score = ContestHandler.compute_score(input["task"], output["result"])
        Database.begin()
        try:
            submission_id = Database.gen_id()
            if not Database.add_submission(submission_id,
                                           input["id"],
                                           output["id"],
                                           source["id"],
                                           score,
                                           autocommit=False):
                self.raise_exc(BadRequest, "INTERNAL_ERROR",
                               "Error inserting the submission")
            ContestHandler.update_user_score(input["token"], input["task"],
                                             score)
            Database.set_user_attempt(input["token"],
                                      input["task"],
                                      None,
                                      autocommit=False)
            Database.commit()
        except sqlite3.IntegrityError as ex:
            Database.rollback()
            # provide a better error message if the input has already been
            # submitted
            if "UNIQUE constraint failed: submissions.input" in str(ex):
                self.raise_exc(Forbidden, "ALREADY_SUBMITTED",
                               "This input has already been submitted")
            raise
        except:
            Database.rollback()
            raise
        Logger.info(
            "CONTEST", "User %s has submitted %s on %s" %
            (input["token"], submission_id, input["task"]))
        return InfoHandler.patch_submission(
            Database.get_submission(submission_id))
예제 #3
0
    def wsgi_app(self, environ, start_response):
        route = self.router.bind_to_environ(environ)
        request = Request(environ)

        try:
            endpoint, args = route.match()
        except HTTPException:
            Logger.warning("HTTP_ERROR",
                           "%s %s %s 404" % (BaseHandler.get_ip(request),
                                             request.method, request.url))
            return NotFound()

        controller, action = endpoint.split("#")

        return self.handlers[controller].handle(action, args, request)
예제 #4
0
    def get_input(task_name, attempt):
        """
        Fetch an input from the queue and properly rename it
        :param task_name: Name of the task
        :param attempt: Number of the attempt for the user
        :return: A pair, the first element is the id of the input file,
        the second the path
        """
        if ContestManager.input_queue[task_name].empty():
            Logger.warning("TASK", "Empty queue for task %s!" % task_name)

        input = ContestManager.input_queue[task_name].get()
        path = StorageManager.new_input_file(input["id"], task_name, attempt)
        StorageManager.rename_file(input["path"], path)

        return input["id"], path
예제 #5
0
    def drop_contest(self, admin_token):
        """
        POST /admin/drop_contest
        """
        if not os.path.exists(Config.encrypted_file):
            self.raise_exc(NotFound, "NOT_FOUND", "No packs found")
        Logger.warning("DROP_CONTEST", "Started dropping contest")
        with open(Config.encrypted_file, "rb") as f:
            pack = f.read()

        db_token = Database.get_meta("admin_token")
        # contest has been extracted but the token is wrong
        if db_token is not None and db_token != admin_token:
            self.raise_exc(Forbidden, "FORBIDDEN", "Wrong token")
        # contest has not been extracted
        if db_token is None:
            try:
                password = crypto.recover_file_password_from_token(admin_token)
                crypto.decode(password, pack)
            except nacl.exceptions.CryptoError:
                # pack password is wrong
                self.raise_exc(Forbidden, "FORBIDDEN", "Wrong pack token")

        metadata = ruamel.yaml.safe_load(crypto.metadata(pack).strip(b"\x00"))
        if not metadata.get("deletable"):
            self.raise_exc(Forbidden, "FORBIDDEN", "Contest not deletable")

        shutil.rmtree(Config.storedir, ignore_errors=True)
        shutil.rmtree(Config.statementdir, ignore_errors=True)
        shutil.rmtree(Config.contest_path, ignore_errors=True)
        for f in (Config.encrypted_file, Config.decrypted_file):
            try:
                os.remove(f)
            except FileNotFoundError:
                pass

        Database.disconnect_database()
        for f in glob.glob(Config.db + "*"):
            os.remove(f)
        Database.connect_to_database()
        Logger.warning("DROP_CONTEST", "Contest dropped")
        return {}
def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("-c",
                        "--config",
                        help="Path to the config file",
                        default="config/config.yaml")
    args = parser.parse_args()

    if args.config:
        Config.set_config_file(args.config)

    Logger.set_log_level(Config.log_level)
    Logger.connect_to_database()
    Database.connect_to_database()
    try:
        ContestManager.read_from_disk(remove_enc=False)
    except:
        Logger.warning("CONTEST", "Failed to read the contest from disk...")
    server = Server()
    server.run()
예제 #7
0
    def _validate_admin_token(token, ip):
        """
        Ensure the admin token is valid
        :param token: Token to check
        :param ip: IP of the client
        """

        correct_token = Database.get_meta("admin_token")

        if correct_token is None:
            ContestManager.extract_contest(token)
            ContestManager.read_from_disk()
            correct_token = token

        if token != correct_token:
            Logger.warning("LOGIN_ADMIN", "Admin login failed from %s" % ip)
            BaseHandler.raise_exc(Forbidden, "FORBIDDEN",
                                  "Invalid admin token!")
        else:
            if Database.register_admin_ip(ip):
                Logger.info("LOGIN_ADMIN",
                            "An admin has connected from a new ip: %s" % ip)
예제 #8
0
 def internet_detected(self, token):
     """
     POST /internet_detected
     """
     Logger.warning("INTERNET_DETECTED",
                    "User %s has been detected with internet!" % token)
예제 #9
0
    def worker(task_name):
        """ Method that stays in the background and generates inputs """
        task = ContestManager.tasks[task_name]
        queue = ContestManager.input_queue[task_name]

        while True:
            try:
                id = Database.gen_id()
                path = StorageManager.new_input_file(id, task_name, "invalid")
                seed = int(sha256(id.encode()).hexdigest(), 16) % (2**31)

                stdout = os.open(
                    StorageManager.get_absolute_path(path),
                    os.O_WRONLY | os.O_CREAT,
                    0o644,
                )

                try:
                    start_time = time.monotonic()
                    # generate the input and store the stdout into a file
                    retcode = gevent.subprocess.call(
                        [task["generator"], str(seed), "0"], stdout=stdout)
                    if time.monotonic() > start_time + 1:
                        Logger.warning(
                            "TASK",
                            "Generation of input %s for task %s took %f seconds"
                            % (seed, task_name, time.monotonic() - start_time),
                        )
                finally:
                    os.close(stdout)

                if retcode != 0:
                    Logger.error(
                        "TASK",
                        "Error %d generating input %s (%d) for task %s" %
                        (retcode, id, seed, task_name),
                    )
                    # skip the input
                    continue

                # if there is a validator in the task use it to check if the
                # generated input is valid
                if "validator" in task:
                    stdin = os.open(StorageManager.get_absolute_path(path),
                                    os.O_RDONLY)
                    try:
                        start_time = time.monotonic()
                        # execute the validator piping the input file to stdin
                        retcode = gevent.subprocess.call(
                            [task["validator"], "0"], stdin=stdin)
                        if time.monotonic() > start_time + 1:
                            Logger.warning(
                                "TASK",
                                "Validation of input %s for task %s took %f "
                                "seconds" % (seed, task_name,
                                             time.monotonic() - start_time),
                            )
                    finally:
                        os.close(stdin)

                    if retcode != 0:
                        Logger.error(
                            "TASK",
                            "Error %d validating input %s (%d) for task %s" %
                            (retcode, id, seed, task_name),
                        )
                        # skip the input
                        continue

                Logger.debug(
                    "TASK",
                    "Generated input %s (%d) for task %s" %
                    (id, seed, task_name),
                )
                # this method is blocking if the queue is full
                queue.put({"id": id, "path": path})
            except:
                Logger.error(
                    "TASK",
                    "Exception while creating an input file: " +
                    traceback.format_exc(),
                )
예제 #10
0
    def read_from_disk(remove_enc=True):
        """
        Load a task from the disk and load the data into the database
        """
        try:
            contest = ContestManager.import_contest(Config.contest_path)
        except FileNotFoundError as ex:
            error = (
                "Contest not found, you probably need to unzip it. Missing file %s"
                % ex.filename)
            Logger.warning("CONTEST", error)
            shutil.rmtree(Config.statementdir, ignore_errors=True)
            shutil.rmtree(Config.web_statementdir, ignore_errors=True)
            shutil.rmtree(Config.contest_path, ignore_errors=True)
            if remove_enc:
                with suppress(Exception):
                    os.remove(Config.encrypted_file)
            with suppress(Exception):
                os.remove(Config.decrypted_file)
            Database.del_meta("admin_token")
            BaseHandler.raise_exc(UnprocessableEntity, "CONTEST", error)

        if not Database.get_meta("contest_imported", default=False, type=bool):
            Database.begin()
            try:
                Database.set_meta("contest_duration",
                                  contest["duration"],
                                  autocommit=False)
                Database.set_meta("contest_name",
                                  contest.get("name", "Contest"),
                                  autocommit=False)
                Database.set_meta(
                    "contest_description",
                    contest.get("description", ""),
                    autocommit=False,
                )
                Database.set_meta(
                    "window_duration",
                    # if None the contest is not USACO-style
                    contest.get("window_duration"),
                    autocommit=False,
                )
                count = 0

                for task in contest["tasks"]:
                    Database.add_task(
                        task["name"],
                        task["description"],
                        task["statement_path"],
                        task["max_score"],
                        count,
                        autocommit=False,
                    )
                    count += 1

                for user in contest["users"]:
                    Database.add_user(user["token"],
                                      user["name"],
                                      user["surname"],
                                      autocommit=False)

                for user in Database.get_users():
                    for task in Database.get_tasks():
                        Database.add_user_task(user["token"],
                                               task["name"],
                                               autocommit=False)

                Database.set_meta("contest_imported", True, autocommit=False)
                Database.commit()
            except:
                Database.rollback()
                raise
        else:
            # TODO: check that the contest is still the same
            pass

        # store the task in the ContestManager singleton
        ContestManager.tasks = dict(
            (task["name"], task) for task in contest["tasks"])
        ContestManager.has_contest = True

        # create the queues for the task inputs
        for task in ContestManager.tasks:
            ContestManager.input_queue[task] = gevent.queue.Queue(
                Config.queue_size)
            gevent.spawn(ContestManager.worker, task)