# CodeQ: an online programming tutor. # Copyright (C) 2015 UL FRI # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU Affero General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more # details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from concurrent.futures import ThreadPoolExecutor import traceback from errors.session import * import server import logging class CodeqService(object): """Base class for all CodeQ services. """ session_is_optional = False def process(self, request): pass class CreateSession(CodeqService): """Creates a new anonymous session. To promote the session to the authenticated status, a subsequent login or signup must follow. """ session_is_optional = True def process(self, request): request.reply({'code': 0, 'message': 'OK', 'sid': server.user_session.UserSession().get_sid()}) class DestroySession(CodeqService): def process(self, request): request.session.destroy() request.reply({'code': 0, 'message': 'OK'}) class Login(CodeqService): """Logs in a client, authenticating the session. """ def process(self, request): js = request.data username = js.get('username') password = js.get('password') if username is None: request.reply({'code': 1, 'message': 'Username was not provided'}) elif password is None: request.reply({'code': 2, 'message': 'Password was not provided'}) else: session = request.session try: name, email, is_admin, date_joined, last_login = session.login(username, password) except AuthenticationFailed: request.reply({'code': 3, 'message': 'Username or password do not match'}) else: settings = session.get_settings() request.reply({ 'code': 0, 'message': 'OK', 'username': username, 'name': name, 'email': email, 'admin': is_admin, 'joined': date_joined.isoformat(), 'last-login': last_login.isoformat(), 'settings': settings }) class Logout(CodeqService): def process(self, request): request.session.logout() request.reply({'code': 0, 'message': 'OK'}) class Signup(CodeqService): def process(self, request): js = request.data username = js.get('username') name = js.get('name') email = js.get('email') password = js.get('password') lang = js.get('lang') or 'en' if username is None: request.reply({'code': 1, 'message': 'Username was not provided'}) if email is None: request.reply({'code': 2, 'message': 'Email was not provided'}) elif password is None: request.reply({'code': 3, 'message': 'Password was not provided'}) else: try: request.session.signup(username, name, email, password, lang) except UserExists: request.reply({'code': 10, 'message': 'Username already exists'}) except SignupFailed: request.reply({'code': 11, 'message': 'Sign-up failed'}) else: request.reply({'code': 0, 'message': 'OK'}) class ChangePassword(CodeqService): def process(self, request): js = request.data password = js.get('password') if password is None: request.reply({'code': 1, 'message': 'Password was not provided'}) else: try: request.session.change_password(password) except PasswordChangeFailed: request.reply({'code': 2, 'message': 'Password change failed'}) else: request.reply({'code': 0, 'message': 'OK'}) class UpdateSettings(CodeqService): def process(self, request): js = request.data settings = js.get('settings') if settings is None: request.reply({'code': 1, 'message': 'New settings not provided'}) else: try: request.session.update_settings(settings) except NoSuchSession: request.reply({'code': 2, 'message': 'No such session'}) else: request.reply({'code': 0, 'message': 'OK'}) class Activity(CodeqService): def process(self, request): js = request.data trace = js.get('trace') solution = js.get('solution') problem_id = js.get('problem_id') if (trace is not None) or (solution is not None): # we have something to do if problem_id is None: request.reply({'code': 1, 'message': 'Problem ID is missing'}) else: request.session.update_solution(problem_id, trace=trace, solution=solution) request.end() # no feedback, just acknowledge the reception class Query(CodeqService): def process(self, request): js = request.data step = js.get('step') if step is None: request.reply({'code': 1, 'message': '"step" is not set'}) else: problem_id = js.get('problem_id') if problem_id is None: request.reply({'code': 4, 'message': 'Problem ID not given'}) else: session = request.session trace = js.get('trace') program = None prolog = session.current_language_session() if prolog is None: result = {'code': 6, 'message': 'No language session is active'} elif not isinstance(prolog, server.prolog_session.PrologSession): result = {'code': 7, 'message': 'The currently active session is not Prolog'} elif step == 'run': program = js.get('program') query = js.get('query') if program is None: result = {'code': 2, 'message': 'No program specified'} elif query is None: result = {'code': 3, 'message': 'No query specified'} else: messages, status, have_more = prolog.run_for_user(session.get_uid(), problem_id, program, query) result = {'code': 0, 'message': 'ok', 'terminal': {'messages': messages, 'status': status, 'have_more': have_more}} elif step == 'next': messages, status, have_more = prolog.step() result = {'code': 0, 'message': 'ok', 'terminal': {'messages': messages, 'status': status, 'have_more': have_more}} elif step == 'end': messages, status, have_more = prolog.destroy() result = {'code': 0, 'message': 'ok', 'terminal': {'messages': messages, 'status': status, 'have_more': have_more}} else: result = {'code': 5, 'message': 'Unknown prolog step: {0}'.format(step)} if program or trace: session.update_solution(problem_id, trace=trace, solution=program) request.reply(result) # Push user program to the Python interpreter to be exec'd. class PythonExec(CodeqService): def process(self, request): program = request.data.get('program') python = request.session.current_language_session() if program is None: request.reply({'code': 1, 'message': 'No program specified'}) elif not isinstance(python, server.python_session.PythonSession): request.reply({'code': 2, 'message': 'The currently active session is not Python'}) else: python.exec(program) request.reply({'code': 0, 'message': 'ok'}) # Send an interrupt to the Python interpreter. class PythonStop(CodeqService): def process(self, request): python = request.session.current_language_session() if not isinstance(python, server.python_session.PythonSession): request.reply({'code': 2, 'message': 'The currently active session is not Python'}) else: python.stop() request.reply({'code': 0, 'message': 'ok'}) # Push stdin to the Python interpreter. class PythonPush(CodeqService): def process(self, request): text = request.data.get('text') python = request.session.current_language_session() if text is None: request.reply({'code': 1, 'message': 'No input specified'}) elif not isinstance(python, server.python_session.PythonSession): request.reply({'code': 2, 'message': 'The currently active session is not Python'}) else: python.push(text) request.reply({'code': 0, 'message': 'ok'}) class Hint(CodeqService): def process(self, request): js = request.data problem_id = js.get('problem_id') program = js.get('program') if problem_id is None: request.reply({'code': 1, 'message': 'No problem ID specified'}) elif program is None: request.reply({'code': 2, 'message': 'No program specified'}) else: session = request.session lang_session = session.current_language_session() if lang_session is None: request.reply({'code': 3, 'message': 'No active session exists'}) else: hints = lang_session.hint(session.get_sid(), problem_id, program) request.reply({'code': 0, 'message': 'ok', 'hints': hints}) class Test(CodeqService): def process(self, request): js = request.data problem_id = js.get('problem_id') program = js.get('program') if problem_id is None: request.reply({'code': 1, 'message': 'No problem ID specified'}) elif program is None: request.reply({'code': 2, 'message': 'No program specified'}) else: session = request.session lang_session = session.current_language_session() if lang_session is None: request.reply({'code': 3, 'message': 'No active session exists'}) else: try: hints = lang_session.test(session.get_sid(), problem_id, program) request.reply({'code': 0, 'message': 'ok', 'hints': hints}) except Exception as ex: request.reply({'code': 4, 'message': str(ex)}) class GetAttempts(CodeqService): def process(self, request): language = request.data.get('language') if language is None: request.reply({'code': 1, 'message': 'No language specified'}) else: request.reply({'code': 0, 'data': request.session.get_attempts(language)}) class GetSolutions(CodeqService): def process(self, request): problem_ids = request.data.get('problem_ids') if problem_ids is None: request.reply({'code': 1, 'message': 'No problem IDs specified'}) else: request.reply({'code': 0, 'data': request.session.get_solutions(problem_ids)}) class GetCurrentSolution(CodeqService): def process(self, request): js = request.data problem_id = js.get('problem_id') if problem_id is None: request.reply({'code': 1, 'message': 'Problem ID not specified'}) else: request.reply({'code': 0, 'message': 'ok', 'data': request.session.current_solution(problem_id)}) class LoadProblem(CodeqService): def process(self, request): problem_id = request.data.get('problem_id') if problem_id is None: request.reply({'code': 1, 'message': 'There is no active session'}) else: session = request.session.load_language_session(problem_id) if session is None: request.reply({'code': 2, 'message': 'The session failed to load'}) else: request.reply({'code': 0, 'message': 'OK'}) class EndProblem(CodeqService): def process(self, request): request.session.end_language_session() request.end() class GetUserStat(CodeqService): def process(self, request): stat = request.session.get_stat() request.reply({'code': 0, 'stat': stat}) class SamlLogin(CodeqService): def process(self, request): js = request.data saml_data = js.get('saml_data') gui_lang = js.get('gui_lang', 'en') upgrade_account = js.get('upgrade_account') upgrade_password = js.get('upgrade_password') if saml_data is None: request.reply({'code': 3, 'message': 'SAML user data not specified'}) else: session = request.session try: name, email, date_joined, last_login = session.saml_login_or_signup(saml_data, gui_lang, upgrade_account, upgrade_password) except AccountMergeRequired as amr: request.reply({'code': 1, 'message': 'Retry with "upgrade_account" and "upgrade_password"', 'username': amr.username}) except AuthenticationFailed as af: request.reply({'code': 4, 'message': 'Password is incorrect'}) # except Exception as e: # request.reply({'code': 2, 'message': 'SAML login failed: ' + str(e)}) else: request.reply({'code': 0, 'message': 'OK', 'name': name, 'email': email, 'joined': date_joined.isoformat(), 'last-login': last_login.isoformat(), 'settings': session.get_settings()}) # maps actions to their handlers incoming_handlers = { 'create_session': CreateSession(), 'destroy_session': DestroySession(), 'login': Login(), 'signup': Signup(), 'change_password': ChangePassword(), 'get_attempts': GetAttempts(), 'get_solutions': GetSolutions(), 'get_current_solution': GetCurrentSolution(), 'logout': Logout(), 'activity': Activity(), 'query': Query(), 'python_exec': PythonExec(), 'python_push': PythonPush(), 'python_stop': PythonStop(), 'hint': Hint(), 'update_settings': UpdateSettings(), 'test': Test(), 'load_problem': LoadProblem(), 'end_problem': EndProblem(), 'user_stat': GetUserStat(), 'saml_login': SamlLogin(), 'saml_logout': Logout() } class Request(object): def __init__(self, tid, original_sid, session, data): """Creates a new request :param tid: communicator-level transaction ID (global relative to the specific communicator where it originated) :param original_sid: session ID, optional :param session: the actual session with the original_sid, if it exists; the processor may swap it for a new session :param data: the request data from the client :return: new instance """ self._tid = tid self._original_sid = original_sid self.session = session self.data = data self.is_finished = False def reply(self, data): """Reply to this request. :param data: the dictionary representing the reply, that will be converted to JSON :return: None """ if data is None: self.end() return if self._original_sid is not None: sid = data.get('sid') if sid is None: data['sid'] = self._original_sid elif sid != self._original_sid: data['sid'] = self._original_sid data['new_sid'] = sid # it is important to reply with the same tid and sid parameters as were in the request, so message accounting doesn't get confused send(self._tid, self._original_sid, data) self.is_finished = True def end(self): """Conclude the request, without sending a response. This is to acknowledge that the response has been received. :return: None """ send(self._tid, self._original_sid, None) self.is_finished = True ########## low-level machinery, subject to change to support more than the single socket communicator ########## _executor = ThreadPoolExecutor(max_workers=100) def _invoke_handler(handler, request): try: logging.debug('Worker thread processing data={}'.format(str(request.data))) handler.process(request) if not request.is_finished: logging.error('ERROR: the request was not concluded!') request.reply({'code': -1, 'message': 'Request processing did not provide a reply'}) logging.debug('Processing finished') except NotLoggedIn: if request.is_finished: logging.error('Caught the NotLoggedIn exception, but the request has already been replied to!') else: request.reply({'code': -1, 'message': 'Cannot execute the request: not logged in'}) except Exception as e: logging.critical('ERROR: data processing failed: ' + str(e)) logging.critical(traceback.format_exc()) if request.is_finished: logging.critical('The request has already been replied to, the client will not be aware of the error!') else: request.reply({'code': -1, 'message': 'Internal error: ' + str(e)}) def serve_request(json_obj): if not isinstance(json_obj, dict): raise RequestProcessingError('Require a request represented as a dict, instead got: ' + str(type(json_obj))) tid = json_obj.get('tid') # session ID and transaction ID uniquely identify a transaction sid = json_obj.get('sid') action = json_obj.get('action') if action is None: raise RequestProcessingError('Request does not contain an action') if not isinstance(action, str): raise RequestProcessingError('Requested action must be a string, got: ' + str(type(action))) handler = incoming_handlers.get(action) if handler is None: raise RequestProcessingError('No handler for ' + action) logging.debug("Attempting to serve action={}".format(action)) session = None if sid is None: if not handler.session_is_optional: raise RequestProcessingError('Request is missing a session-ID') else: del json_obj['sid'] try: session = server.user_session.get_session_by_id(sid) except NoSuchSession: if not handler.session_is_optional: raise RequestProcessingError('This user session has expired. Please log-in again.') _executor.submit(_invoke_handler, handler, Request(tid, sid, session, json_obj)) def send(tid, sid, json_obj): # just a proxy function for now logging.debug('Sending reply: {}'.format(str(json_obj))) server.socket.sendPacket(tid, sid, json_obj) def stop(): global _executor _executor.shutdown() _executor = None