def warn(self, method): print_error("Cannot {} {}: test is encrypted".format( method, self.name)) keys_string = input("Please paste the key to decrypt this test: ") keys = keys_string.strip().split() if keys: raise ex.ForceDecryptionException(keys)
def attempt_decryption(self, keys): if self.decryption_keypage: try: response = requests.get(self.decryption_keypage) response.raise_for_status() keys_data = response.content.decode('utf-8') keys = keys + encryption.get_keys(keys_data) except Exception as e: print_error("Could not load decryption page {}: {}.".format( self.decryption_keypage, e)) print_error( "You can pass in a key directly by running python3 ok --decrypt [KEY]" ) decrypted_files = [] undecrypted_files = [] for file in self._get_files(): with open(file) as f: if not encryption.is_encrypted(f.read()): continue for key in keys: success = self._decrypt_file(file, key) if success: decrypted_files.append(file) break else: undecrypted_files.append(file) return decrypted_files, undecrypted_files
def save(self, data): file_name = data['fileName'] file_name = file_name.strip() if file_name not in self.assignment.src or file_name.endswith('.ok'): if file_name != 'submit': logging.warning("Unknown filename {}".format(file_name)) print_error("Unknown file - Not saving {}".format(file_name)) return if not os.path.isfile(file_name): log.warning('File {} does not exist. Not backing up'.format(file_name)) backup_dst = file_name else: # Backup the file log.debug("Backing up file") backup_dst = self.backup_file(file_name) print_success("Backed up file to {}".format(backup_dst)) log.debug("Beginning overwriting file") contents = data['file'] with open(file_name, 'w') as f: f.write(contents) log.debug("Done replacing file") # Update file contents for backup self.file_contents[file_name] = contents return backup_dst
def main(): """Run the LockingProtocol.""" args = parse_input() args.lock = True args.question = [] args.all = False args.timeout = 0 args.verbose = False args.interactive = False args.ignore_empty = False args.parsons = False try: assign = assignment.load_assignment(args.config, args) msgs = messages.Messages() lock.protocol(args, assign).run(msgs) except (ex.LoadingException, ex.SerializeException) as e: log.warning('Assignment could not instantiate', exc_info=True) print_error('Error: ' + str(e).strip()) exit(1) except (KeyboardInterrupt, EOFError): log.info('Quitting...') else: assign.dump_tests()
def run(self, messages): if not self.args.style: log.info("Autostyle not enabled.") return elif self.args.local: log.info("Autostyle requires network access.") return if not messages.get('analytics'): log.warning("Autostyle needs to be after analytics") return if not messages.get('grading'): log.warning("Autostyle needs to be after grading") return if not self.args.question: log.warning("Autostyle requires a specific question") return messages['autostyle'] = {} grading = messages['grading'] for question in self.args.question: if question in AutoStyleProtocol.ALLOW_QUESTIONS: # Ensure that all tests have passed results = grading.get(question) if not results: log.warning("No grading info") return elif results['failed'] or results['locked']: log.warning("Has not passed all tests") print_error( "To use AutoStyle you must have a correct solution for {0}!" .format(question)) return else: log.info("Not an autostyle question") print_error( "Make sure the question you are using is an AutoStyle question!" ) return print( "Once you begin you must finish the experiment in one sitting. This will take at most 2 hours." ) confirm = input("Do you wish to continue to AutoStyle? (y/n): ") if confirm.lower().strip() != 'y': return messages['analytics']['identifier'] = self.assignment.get_identifier() # Send data to autostyle response_url = self.send_messages(messages, self.SHORT_TIMEOUT) # Parse response_url if response_url: webbrowser.open_new(response_url) else: log.error( "There was an error with AutoStyle. Please try again later!")
def autobackup(self, run_sync): backup = self._get_protocol("BackupProtocol") get_contents = self._get_protocol("FileContentsProtocol") if backup is None: print_error("Error: autobackup specified by backup protocol not found") return def messages_fn(): msgs = messages.Messages() get_contents.run(msgs) return msgs backup.run_in_loop(messages_fn, timedelta(minutes=1), synchronous=run_sync)
def check_version(server, version, filename, timeout=SHORT_TIMEOUT): """Check for the latest version of OK and update accordingly.""" address = VERSION_ENDPOINT.format(server=server) log.info('Checking for software updates...') log.info('Existing OK version: %s', version) log.info('Checking latest version from %s', address) try: response = requests.get(address, timeout=timeout) response.raise_for_status() except (requests.exceptions.RequestException, requests.exceptions.BaseHTTPError) as e: print_error('Network error when checking for updates.') log.warning('Network error when checking version from %s: %s', address, str(e), stack_info=True) return False response_json = response.json() if not _validate_api_response(response_json): print_error('Error while checking updates: malformed server response') log.info('Malformed response from %s: %s', address, response.text) return False current_version = response_json['data']['results'][0]['current_version'] if current_version == version: print_success('OK is up to date') return True download_link = response_json['data']['results'][0]['download_link'] log.info('Downloading version %s from %s', current_version, download_link) try: response = requests.get(download_link, timeout=timeout) response.raise_for_status() except (requests.exceptions.RequestException, requests.exceptions.BaseHTTPError) as e: print_error('Error when downloading new version of OK') log.warning('Error when downloading new version of OK: %s', str(e), stack_info=True) return False log.info('Writing new version to %s', filename) zip_binary = response.content try: _write_zip(filename, zip_binary) except IOError as e: print_error('Error when downloading new version of OK') log.warning('Error writing to %s: %s', filename, str(e)) return False else: print_success('Updated to version: {}'.format(current_version)) log.info('Successfully wrote to %s', filename) return True
def send_messages(self, data, timeout=30, endpoint='/collab/start/'): """Send messages to server, along with user authentication.""" address = 'https://{}{}'.format(self.COLLAB_SERVER, endpoint) params = { 'client_name': 'ok-client', 'client_version': client.__version__, } log.info('Sending messages to %s', address) try: r = requests.post(address, params=params, json=data, timeout=timeout) r.raise_for_status() return r.json() except (requests.exceptions.RequestException, requests.exceptions.BaseHTTPError, Exception) as ex: message = '{}: {}'.format(ex.__class__.__name__, str(ex)) log.warning(message) print_error("There was an error connecting to the server." "Run with --debug for more details") return
def main(): """Run GradingProtocol and ScoringProtocol.""" args = ok.parse_input() log.setLevel(logging.DEBUG if args.debug else logging.ERROR) log.debug(args) try: assign = assignment.load_assignment(args.config, args) msgs = messages.Messages() grading.protocol(args, assign).run(msgs) scoring.protocol(args, assign).run(msgs) except (ex.LoadingException, ex.SerializeException) as e: log.warning('Assignment could not instantiate', exc_info=True) print_error('Error: ' + str(e).strip()) exit(1) except (KeyboardInterrupt, EOFError): log.info('Quitting...')
def decrypt(self, keys): decrypted_files, undecrypted_files = self.attempt_decryption(keys) if not undecrypted_files + decrypted_files: print_success("All files are decrypted") elif undecrypted_files: if keys: print_error("Unable to decrypt some files with the keys", ", ".join(keys)) else: print_error("No keys found, could not decrypt any files") print_error(" Non-decrypted files:", *undecrypted_files)
def run(self, messages, nointeract=False): if not self.assignment.endpoint: log.info('No assignment endpoint, skipping backup') return if self.args.local: print_warning("Cannot backup when running ok with --local.") return if not self.args.insecure: network.check_ssl() if self.args.revise: action = 'revision' elif self.args.submit: action = 'submission' else: action = 'backup' message_list = self.load_unsent_messages() access_token = self.assignment.authenticate(nointeract=nointeract) log.info('Authenticated with access token') log.info('Sending unsent messages') if not access_token: print_error( "Not authenticated. Cannot send {} to server".format(action)) self.dump_unsent_messages(message_list) return # Messages from the current backup to send first is_send_first = self.args.submit or self.args.revise subm_messages = [messages] if is_send_first else [] if is_send_first: response = self.send_all_messages(access_token, subm_messages, current=True) if message_list: self.send_all_messages(access_token, message_list, current=False) else: message_list.append(messages) response = self.send_all_messages(access_token, message_list, current=False) base_url = self.assignment.server_url + '/{}/{}/{}' if isinstance(response, dict): print_success('{action} successful for user: {email}'.format( action=action.title(), email=response['data']['email'])) submission_type = 'submissions' if self.args.submit else 'backups' url = base_url.format(response['data']['assignment'], submission_type, response['data']['key']) if self.args.submit or self.args.backup: print_success('URL: {0}'.format(url)) if self.args.backup: print('NOTE: this is only a backup. ' 'To submit your assignment, use:\n' '\tpython3 ok --submit') self.dump_unsent_messages(message_list + subm_messages) print()
def send_all_messages(self, access_token, message_list, current=False): if not self.args.insecure: ssl = network.check_ssl() else: ssl = None if current and self.args.revise: action = "Revise" elif current and self.args.submit: action = "Submit" else: action = "Backup" num_messages = len(message_list) send_all = self.args.submit or self.args.backup retries = self.RETRY_LIMIT if send_all: timeout = None stop_time = datetime.datetime.max retries = self.RETRY_LIMIT * 2 else: timeout = self.SHORT_TIMEOUT stop_time = datetime.datetime.now() + datetime.timedelta( seconds=timeout) log.info('Setting timeout to %d seconds', timeout) first_response = None error_msg = '' log.info("Sending {0} messages".format(num_messages)) while retries > 0 and message_list and datetime.datetime.now( ) < stop_time: log.info('Sending messages...%d left', len(message_list)) print('{action}... {percent}% complete'.format( action=action, percent=100 - round(len(message_list) * 100 / num_messages, 2)), end='\r') # message_list is assumed to be ordered in chronological order. # We want to send the most recent message first, and send older # messages after. message = message_list[-1] try: response = self.send_messages(access_token, message, timeout, current) except requests.exceptions.Timeout as ex: log.warning("HTTP request timeout: %s", str(ex)) retries -= 1 error_msg = 'Connection timed out after {} seconds. '.format(timeout) + \ 'Please check your network connection.' except (requests.exceptions.RequestException, requests.exceptions.BaseHTTPError) as ex: log.warning('%s: %s', ex.__class__.__name__, str(ex)) retries -= 1 if getattr(ex, 'response', None) is None: error_msg = 'Please check your network connection.' continue try: response_json = ex.response.json() except ValueError as ex: log.warning("Invalid JSON Response", exc_info=True) retries -= 1 error_msg = 'The server did not provide a valid response. Try again soon.' continue log.warning('%s error message: %s', ex.__class__.__name__, response_json['message']) if ex.response.status_code == 401: # UNAUTHORIZED (technically authorization != authentication, but oh well) raise exceptions.AuthenticationException( response_json.get( 'message')) # raise this for the caller elif ex.response.status_code == 403 and 'download_link' in response_json[ 'data']: retries = 0 error_msg = 'Aborting because OK may need to be updated.' else: retries -= 1 error_msg = response_json['message'] except Exception as ex: if ssl and isinstance(ex, ssl.CertificateError): retries = 0 log.warning("SSL Error: %s", str(ex)) error_msg = 'SSL Verification Error: {}\n'.format(ex) + \ 'Please check your network connection and SSL configuration.' else: retries -= 1 log.warning(error_msg, exc_info=True) error_msg = "Unknown Error: {}".format(ex) else: if not first_response: first_response = response message_list.pop() if current and error_msg: print() # Preserve progress bar. print_error('Could not', action.lower() + ':', error_msg) elif not message_list: print('{action}... 100% complete'.format(action=action)) due_date = self.get_due_date(access_token, timeout) if due_date is not None and action != "Revise": now = datetime.datetime.now(tz=datetime.timezone.utc) time_to_deadline = due_date - now if time_to_deadline < datetime.timedelta(0): print_error( "{action} past deadline by".format(action=action), display_timedelta(-time_to_deadline)) elif time_to_deadline < datetime.timedelta(hours=10): print_warning("Assignment is due in", display_timedelta(time_to_deadline)) return first_response elif not send_all: # Do not display any error messages if --backup or --submit are not # used. print() elif not error_msg: # No errors occurred, but could not complete request within TIMEOUT. print() # Preserve progress bar. print_error('Could not {} within {} seconds.'.format( action.lower(), timeout)) else: # If not all messages could be backed up successfully. print() # Preserve progress bar. print_error('Could not', action.lower() + ':', error_msg)
def run(self, messages): """Determine if a student is elgible to recieve a hint. Based on their state, poses reflection questions. After more attempts, ask if students would like hints. If so, query the server. """ if self.args.local: return # Only run hinting protocol on supported assignments. if self.assignment.endpoint not in self.SUPPORTED_ASSIGNMENTS: message = "{0} does not support hinting".format( self.assignment.endpoint) log.info(message) if self.args.hint: print(message) return if 'analytics' not in messages: log.info('Analytics Protocol is required for hint generation') return if 'file_contents' not in messages: log.info('File Contents needed to generate hints') return if self.args.no_experiments: messages['hinting'] = {'disabled': 'user'} return messages['hinting'] = {} history = messages['analytics'].get('history', {}) questions = history.get('questions', []) current_q = history.get('question', {}) messages['hinting']['flagged'] = self.args.hint for question in current_q: if question not in questions: continue stats = questions[question] is_solved = stats['solved'] == True messages['hinting'][question] = {'prompts': {}, 'reflection': {}} hint_info = messages['hinting'][question] # Determine a users elgibility for a prompt # If the user just solved this question, provide a reflection prompt if is_solved: hint_info['elgible'] = False hint_info['disabled'] = 'solved' if self.args.hint: print("This question has already been solved.") continue elif stats['attempts'] < self.SMALL_EFFORT: log.info( "Question %s is not elgible: Attempts: %s, Solved: %s", question, stats['attempts'], is_solved) hint_info['elgible'] = False if self.args.hint: hint_info['disabled'] = 'attempt-count' print( "You need to make a few more attempts before the hint system is enabled" ) continue else: # Only prompt every WAIT_ATTEMPTS attempts to avoid annoying user if stats['attempts'] % self.WAIT_ATTEMPTS != 0: hint_info['disabled'] = 'timer' hint_info['elgible'] = False log.info('Waiting for %d more attempts before prompting', stats['attempts'] % self.WAIT_ATTEMPTS) else: hint_info['elgible'] = not is_solved if not self.args.hint: if hint_info['elgible']: with format.block("-"): print( "To get hints, try using python3 ok --hint -q {}". format(question)) hint_info['suggested'] = True continue hint_info['accept'] = True with format.block("-"): print(("Thinking of a hint for {}".format(question) + "... (This could take up to 30 seconds)")) pre_hint = random.choice(PRE_HINT_MESSAGES) print("In the meantime, consider: \n{}".format(pre_hint)) hint_info['pre-prompt'] = pre_hint log.info('Prompting for hint on %s', question) try: response = self.query_server(messages, question) except (requests.exceptions.RequestException, requests.exceptions.BaseHTTPError): log.debug("Network error while fetching hint", exc_info=True) hint_info['fetch_error'] = True print_error( "\r\nNetwork Error while generating hint. Try again later" ) response = None continue if response: hint_info['response'] = response hint = response.get('message') pre_prompt = response.get('pre-prompt') post_prompt = response.get('post-prompt') system_error = response.get('system-error') log.info("Hint server response: {}".format(response)) if not hint: if system_error: print("{}".format(system_error)) else: print( "Sorry. No hints found for the current code. Try again making after some changes" ) continue # Provide padding for the the hint print("\n{}".format(hint.rstrip())) if post_prompt: results['prompts'][query] = prompt.explanation_msg( post_prompt)
def main(): """Run all relevant aspects of ok.py.""" args = parse_input() log.setLevel(logging.DEBUG if args.debug else logging.ERROR) log.debug(args) # Checking user's Python bit version bit_v = (8 * struct.calcsize("P")) log.debug("Python {} ({}bit)".format(sys.version, bit_v)) if args.version: print("okpy=={}".format(client.__version__)) exit(0) elif args.update: print("Current version: {}".format(client.__version__)) did_update = software_update.check_version(args.server, client.__version__, client.FILE_NAME, timeout=10) exit(not did_update) # exit with error if ok failed to update assign = None try: if args.get_token: if args.nointeract: print_error( "Cannot pass in --get-token and --nointeract, the only way to get a token is by interaction" ) exit(1) access_token = auth.authenticate(args, force=True) print("Token: {}".format(access_token)) exit(not access_token) # exit with error if no access_token # Instantiating assignment assign = assignment.load_assignment(args.config, args) if assign.decryption_keypage: # do not allow running locally if decryption keypage is provided args.local = False if args.autobackup_actual_run_sync: assign.autobackup(run_sync=True) # do not dump tests back out, this overwrites any changes that may have been made assign = None exit(0) if args.generate_encryption_key: assign.generate_encryption_key(args.generate_encryption_key) exit(0) if args.encrypt: assign.encrypt(args.encrypt, args.encrypt_padding) # do not dump tests back out, this overwrites any changes that may have been made assign = None exit(0) if args.decrypt is not None: raise ex.ForceDecryptionException(args.decrypt) if args.tests: print('Available tests:') for name in assign.test_map: print(' ' + name) exit(0) if args.autobackup: assign.autobackup(run_sync=False) exit(0) force_authenticate = args.authenticate retry = True while retry: retry = False if force_authenticate: if args.nointeract: print_error( "Cannot pass in --authenticate and --nointeract") exit(1) # Authenticate and check for success if not assign.authenticate(force=True): exit(1) try: msgs = messages.Messages() for name, proto in assign.protocol_map.items(): log.info('Execute {}.run()'.format(name)) proto.run(msgs) msgs['timestamp'] = str(datetime.now()) except ex.AuthenticationException as e: if not force_authenticate: force_authenticate = True retry = True elif not args.no_browser: args.no_browser = True retry = True if retry: msg = "without a browser" if args.no_browser else "with a browser" log.warning( 'Authentication exception occurred; will retry {0}'. format(msg), exc_info=True) print_error( 'Authentication error; will try to re-authenticate {0}...' .format(msg)) else: raise # outer handler will be called except ex.ForceDecryptionException as e: assign.decrypt(e.keys) # begin an autobackup assign.autobackup(run_sync=False) # do not dump tests back out, this could overwrite any changes that may have been made assign = None exit(0) except ex.LoadingException as e: log.warning('Assignment could not load', exc_info=True) print_error('Error loading assignment: ' + str(e)) except ex.AuthenticationException as e: log.warning('Authentication exception occurred', exc_info=True) print_error('Authentication error: {0}'.format(e)) except ex.EarlyExit as e: log.warning('OK exited early (non-error)') print_error(str(e)) except ex.OkException as e: log.warning('General OK exception occurred', exc_info=True) print_error('Error: ' + str(e)) except KeyboardInterrupt: log.info('KeyboardInterrupt received.') finally: if not args.no_update and not args.local: try: software_update.check_version(args.server, client.__version__, client.FILE_NAME) except KeyboardInterrupt: pass if assign: assign.dump_tests()
def start_firebase(self, messages): access_token = self.assignment.authenticate() email = self.assignment.get_student_email() identifier = self.assignment.get_identifier() firebase = pyrebase.initialize_app(self.FIREBASE_CONFIG) self.fire_auth = firebase.auth() self.fire_db = firebase.database() self.user_email = email self.hostname = platform.node() data = { 'access_token': access_token, 'email': email, 'identifier': identifier, 'assignment': self.assignment.endpoint, 'file_contents': messages.get('file_contents'), 'analytics': messages.get('analytics'), } # Check for existing sessions first - TBD Future # existing_sessions = self.send_messages(data, endpoint='/collab/list') # response = self.prompt_for_existing_session(existing_sessions.get('sessions')) # if response: # data['desired_session'] = response # Send data to collaborate server response_data = self.send_messages(data, self.LONG_TIMEOUT) if 'error' in response_data or 'session' not in response_data: print_error("There was an error while starting the session: {} Try again later" .format(response_data.get('error'))) log.warning("Error: {}".format(response_data.get('error'))) return self.session_id = response_data['session'] self.short_url = response_data['short_url'] self.login_user = response_data.get('login_user') # Login as the firebase user email, password = response_data.get('login_user'), response_data.get('password') try: self.fire_user = self.fire_auth.sign_in_with_email_and_password(email, password) self.fire_uid = self.fire_user['localId'] except (ValueError, KeyError) as e: log.warning("Could not login", exc_info=True) print_error("Could not login to the collaboration server.") return self.stream = (self.get_firebase() .child('actions').stream(self.stream_listener, self.fire_user['idToken'])) self.presence = (self.get_firebase() .child('clients').push({'computer': platform.node(), 'uid': self.fire_uid, 'owner': self.user_email, 'email': self.user_email}, self.fire_user['idToken'])) # Parse response_url if response_data: open_url = response_data['url'] if 'access_token' not in open_url: open_url = open_url + "?access_token={}".format(access_token) could_open = webbrowser.open_new(open_url) if not could_open: print("Could not open browser. Go to {}".format(open_url)) else: log.error("There was an error with the server. Please try again later!") return print_success("Tell your group members or course staff to go to {}" .format(self.short_url)) while True: data = input("[{}] Type exit to disconnect: ".format(self.short_url)) if data.strip().lower() == 'exit': raise ValueError('Done with session')