class TestFirstrun(unittest.TestCase): """TODO: Doku.""" def setUp(self) -> None: """TODO: Doku.""" prepare_configuration() self.config = Config(WEB_CONFIG_PATH) self.config.set_value("WEB", "username", "") self.config.set_value("WEB", "password", "") self.client = prepare_client() def tearDown(self) -> None: """TODO: Doku.""" restore_configuration() def test_firstrun_logged_out(self) -> None: """TODO: Doku.""" response = self.client.get('/') self.assertIn(b"username and password", response.data) def test_firstrun_save_logged_out(self) -> None: """TODO: Doku.""" response = self.client.post('/firstrun', data={ "username": "******", "password": "******", "password-confirm": "demo" }, follow_redirects=True) self.assertIn(b"Username and password have been successfully saved", response.data)
class Passwd(object): """the class contains the password generation and saving""" def __init__(self): """the init function creates the config variable and calls the user input""" self.config = Config("config/web.ini") self.ask_user() def savepasswd(self, password): """the function saves the password into the config file using the config class""" hostname = platform.node().encode("utf-8") salt = hashlib.sha512(hostname).hexdigest() pw_hash = hashlib.sha512( str(salt + password).encode("utf-8")).hexdigest() self.config.set_value("WEB", "password", pw_hash) print("Password successfully saved.") def saveuser(self, username): """the function saves the username into the config file using the config class""" self.config.set_value("WEB", "username", username) print("Username successfully saved.") def ask_user(self): """the function asks the user for the username and password""" username = input("easywall Web Username: "******"easywall Web Password: ") self.savepasswd(password)
class TestLogin(unittest.TestCase): """ TODO: Doku """ def setUp(self): prepare_configuration() self.client = prepare_client() self.config = Config(CONFIG_PATH) def tearDown(self): restore_configuration() def test_login(self): """ TODO: Doku """ self.client.get('/login') def test_login_post(self): """ TODO: Doku """ self.log_in(self.client) def test_logout(self): """ TODO: Doku """ self.log_in(self.client) self.client.get('/logout', follow_redirects=True) def set_username_password(self): """ TODO: Doku """ self.config = Config(CONFIG_PATH) self.config.set_value("WEB", "username", "test") hostname = platform.node().encode("utf-8") salt = hashlib.sha512(hostname).hexdigest() pw_hash = hashlib.sha512(str(salt + "test").encode("utf-8")).hexdigest() self.config.set_value("WEB", "password", pw_hash) def log_in(self, client): """ TODO: Doku """ self.set_username_password() return client.post('/login', data=dict(username="******", password="******"), follow_redirects=True)
def prepare_configuration() -> None: """ TODO: Doku """ if file_exists(CONFIG_PATH): rename_file(CONFIG_PATH, CONFIG_BACKUP_PATH) content = """[LOG] level = info to_files = no to_stdout = yes filepath = log filename = easywall-web.log [WEB] username = demo password = xxx bindip = 0.0.0.0 bindport = 12227 login_attempts = 10 login_bantime = 1800 [VERSION] version = 0.0.0 sha = 12345 date = 2020-01-01T00:00:00Z timestamp = 1234 [uwsgi] https-socket = 0.0.0.0:12227,easywall.crt,easywall.key processes = 5 threads = 2 callable = APP master = false wsgi-file = easywall_web/__main__.py need-plugin = python3 """ create_file_if_not_exists(CONFIG_PATH) write_into_file(CONFIG_PATH, content) config = Config(CONFIG_PATH) config.set_value("VERSION", "timestamp", str(int(time())))
class Passwd(object): """the class contains the password generation and saving""" def __init__(self) -> None: """the init function creates the config variable and calls the user input""" self.config = Config(CONFIG_PATH) parser = argparse.ArgumentParser() parser.add_argument("--username", "-u", help="set username") parser.add_argument("--password", "-p", help="set password") args = parser.parse_args() if args.username and args.password: self.saveuser(args.username) self.savepasswd(args.password) else: self.ask_user() def savepasswd(self, password: str) -> None: """the function saves the password into the config file using the config class""" hostname = platform.node().encode("utf-8") salt = hashlib.sha512(hostname).hexdigest() pw_hash = hashlib.sha512(str(salt + password).encode("utf-8")).hexdigest() self.config.set_value("WEB", "password", pw_hash) print("Password successfully saved.") def saveuser(self, username: str) -> None: """the function saves the username into the config file using the config class""" self.config.set_value("WEB", "username", username) print("Username successfully saved.") def ask_user(self) -> None: """the function asks the user for the username and password""" username = input("easywall Web Username: "******"easywall Web Password: "******"easywall Web Password repeat: ") if password == password_repeat: self.savepasswd(password) else: print("password is not equal. password not saved!")
class TestConfig(unittest.TestCase): """ this class contains all test functions for the config module """ def setUp(self): content = """[TEST] teststring = string testboolean = true testint = 1 testfloat = 1.1 """ create_file_if_not_exists("test.ini") write_into_file("test.ini", content) self.config = Config("test.ini") def tearDown(self): delete_file_if_exists("test.ini") def test_get_value_error(self): self.assertEqual(self.config.get_value("TEST", "notexistent"), "") def test_get_value_bool(self): self.assertEqual(self.config.get_value("TEST", "testboolean"), True) def test_get_value_int(self): self.assertEqual(self.config.get_value("TEST", "testint"), 1) def test_get_value_float(self): self.assertEqual(self.config.get_value("TEST", "testfloat"), float(1.1)) def test_get_sections(self): self.assertIn("TEST", self.config.get_sections()) def test_set_value_success(self): self.assertEqual(self.config.set_value("TEST", "teststring", "erfolg"), True) def test_set_value_fail_section(self): self.assertEqual(self.config.set_value("TEST2", "asd", "asd"), False)
class TestAcceptance(unittest.TestCase): """TODO: Doku.""" def setUp(self) -> None: """TODO: Doku.""" prepare_configuration() self.config = Config(CONFIG_PATH) self.acceptance = Acceptance(self.config) def tearDown(self) -> None: """TODO: Doku.""" restore_configuration() def test_disabled(self) -> None: """TODO: Doku.""" self.config = Config(CONFIG_PATH) self.config.set_value("ACCEPTANCE", "enabled", "no") self.acceptance = Acceptance(self.config) self.assertEqual(self.acceptance.status(), "disabled") def test_not_accepted(self) -> None: """TODO: Doku.""" self.acceptance.start() self.acceptance.wait() self.assertEqual(self.acceptance.status(), "not accepted") def test_accepted(self) -> None: """TODO: Doku.""" self.acceptance.start() self.acceptance.wait() write_into_file(self.acceptance.filename, "true") self.assertEqual(self.acceptance.status(), "accepted") def test_accepted_early(self) -> None: """TODO: Doku.""" self.acceptance.start() write_into_file(self.acceptance.filename, "true") self.acceptance.wait() self.assertEqual(self.acceptance.status(), "accepted")
class Webutils(object): """Create a couple of shared functions used in the route functions.""" def __init__(self) -> None: """TODO: Doku.""" self.cfg = Config("config/web.ini") self.cfg_easywall = Config(CONFIG_PATH) self.cfg_log = Config(LOG_CONFIG_PATH) def check_login(self, request: Request) -> bool: """Check if the user/session is logged in.""" if not session.get('logged_in'): return False if request.remote_addr != session.get('ip_address'): return False return True def check_first_run(self) -> bool: """Check if the webinterface is run for the first time.""" username = self.cfg.get_value("WEB", "username") password = self.cfg.get_value("WEB", "password") if username == "" or password == "": return True return False # ------------------------- # Payload Operations def get_default_payload(self, title: str, css: str = "easywall") -> DefaultPayload: """Create a object of information that are needed on every page.""" payload = DefaultPayload() payload.year = datetime.today().year payload.title = title payload.customcss = css payload.machine = self.get_machine_infos() payload.latest_version = str(self.cfg.get_value("VERSION", "version")) payload.current_version = file_get_contents(".version") payload.commit_sha = str(self.cfg.get_value("VERSION", "sha")) payload.commit_date = self.get_commit_date( str(self.cfg.get_value("VERSION", "date"))) payload.config_mismatch = self.get_config_version_mismatch("core") payload.web_config_mismatch = self.get_config_version_mismatch("web") return payload def get_machine_infos(self) -> dict: """Retrieve some information about the host and returns them as a list.""" infos = {} infos["Machine"] = platform.machine() infos["Hostname"] = platform.node() infos["Platform"] = platform.platform() infos["Python Build"] = "".join(platform.python_build()) infos["Python Compiler"] = platform.python_compiler() infos["Python Implementation"] = platform.python_implementation() infos["Python Version"] = platform.python_version() infos["Release"] = platform.release() infos["Libc Version"] = "".join(platform.libc_ver()) return infos def get_config_version_mismatch(self, cfgtype: str) -> bool: """TODO: Doku.""" if cfgtype == "core": cfg1 = Config("config/easywall.sample.ini") cfg2 = Config("config/easywall.ini") elif cfgtype == "web": cfg1 = Config("config/web.sample.ini") cfg2 = Config("config/web.ini") for section in cfg1.get_sections(): if section not in cfg2.get_sections(): return True for key in cfg1.get_keys(section): if key not in cfg2.get_keys(section): return True return False # ------------------------- # Update Info Operations def get_commit_date(self, datestring: str) -> str: """ Compare a datetime with the current date. for comparing the datestring parameter is in UTC timezone """ date1 = datetime.strptime(str(datestring), "%Y-%m-%dT%H:%M:%SZ") date1 = date1.replace(tzinfo=timezone.utc).astimezone(tz=None).replace( tzinfo=None) date2 = datetime.now() return time_duration_diff(date1, date2) def update_last_commit_infos(self) -> None: """ Retrieve the last commit information after a specific waiting time. after retrieving the information they are saved into the config file """ currtime = int(time.time()) lasttime = int(self.cfg.get_value("VERSION", "timestamp")) waitseconds = 3600 # 60 minutes × 60 seconds if currtime > (lasttime + waitseconds): commit = self.get_latest_commit() self.cfg.set_value("VERSION", "version", self.get_latest_version()) self.cfg.set_value("VERSION", "sha", commit["sha"]) self.cfg.set_value("VERSION", "date", commit["commit"]["author"]["date"]) self.cfg.set_value("VERSION", "timestamp", str(currtime)) def get_latest_commit(self) -> Any: """ Retrieve the informations of the last commit from github as json. also converts the information into a python object for example the object contains the last commit date and the last commit sha This function should not be called very often, because GitHub has a rate limit implemented """ url = "https://api.github.com/repos/jpylypiw/easywall/commits/master" req = urllib.request.Request( url, data=None, headers={'User-Agent': 'easywall by github.com/jpylypiw/easywall'}) if req.get_full_url().lower().startswith("https"): response = urllib.request.urlopen(req) else: raise ValueError from None return json.loads(response.read().decode('utf-8')) def get_latest_version(self) -> str: """Retrieve the latest version from github and returns the version string.""" url = "https://raw.githubusercontent.com/jpylypiw/easywall/master/.version" req = urllib.request.Request( url, data=None, headers={'User-Agent': 'easywall by github.com/jpylypiw/easywall'}) if req.get_full_url().lower().startswith("https"): response = urllib.request.urlopen(req) else: raise ValueError from None data = response.read() return str(data.decode('utf-8')) # ------------------------- # Acceptance Operations def get_last_accept_time(self) -> str: """ Retrieve the modify time of the acceptance file. also compares the time to the current time """ timestamp = str(self.cfg_easywall.get_value("ACCEPTANCE", "timestamp")) if timestamp == "": return "never" timestamp_datetime = datetime.strptime(timestamp, '%Y-%m-%d %H:%M:%S.%f') now = datetime.now() return time_duration_diff(timestamp_datetime, now) def get_acceptance_status(self) -> str: """Get the status of the current acceptance.""" filepath = ".acceptance_status" if file_exists(filepath): return file_get_contents(filepath) return ""
class Webutils(object): """the class is called in the route modules and contains non route-specific functions""" def __init__(self): self.cfg = Config("config/web.ini") def check_login(self): """the function checks if the user/session is logged in""" if not session.get('logged_in'): return False return True # ------------------------- # Payload Operations def get_default_payload(self, title, css="easywall"): """the function creates a object of information that are needed on every page""" payload = DefaultPayload() payload.year = datetime.today().year payload.title = title payload.customcss = css payload.machine = self.get_machine_infos() payload.latest_version = self.cfg.get_value("VERSION", "version") payload.current_version = file_get_contents("{}/../.version".format(get_abs_path_of_filepath(__file__))) payload.commit_sha = self.cfg.get_value("VERSION", "sha") payload.commit_date = self.get_commit_date(self.cfg.get_value("VERSION", "date")) return payload def get_machine_infos(self): """the function retrieves some information about the host and returns them as a list""" infos = {} infos["Machine"] = platform.machine() infos["Hostname"] = platform.node() infos["Platform"] = platform.platform() infos["Python Build"] = platform.python_build() infos["Python Compiler"] = platform.python_compiler() infos["Python Implementation"] = platform.python_implementation() infos["Python Version"] = platform.python_version() infos["Release"] = platform.release() infos["Libc Version"] = platform.libc_ver() return infos # ------------------------- # Update Info Operations def get_commit_date(self, datestring): """ the function compares a datetime with the current date for comparing the datestring parameter is in UTC timezone """ date1 = datetime.strptime(str(datestring), "%Y-%m-%dT%H:%M:%SZ") date1 = date1.replace( tzinfo=timezone.utc).astimezone( tz=None).replace( tzinfo=None) date2 = datetime.now() return time_duration_diff(date1, date2) def update_last_commit_infos(self): """ the function retrieves the last commit information after a specific waiting time after retrieving the information they are saved into the config file """ currtime = int(time.time()) lasttime = int(self.cfg.get_value("VERSION", "timestamp")) waitseconds = 3600 # 60 minutes × 60 seconds if currtime > (lasttime + waitseconds): commit = self.get_latest_commit() self.cfg.set_value("VERSION", "version", self.get_latest_version()) self.cfg.set_value("VERSION", "sha", commit["sha"]) self.cfg.set_value("VERSION", "date", commit["commit"]["author"]["date"]) self.cfg.set_value("VERSION", "timestamp", currtime) def get_latest_commit(self): """ retrieves the informations of the last commit from github as json and converts the information into a python object for example the object contains the last commit date and the last commit sha This function should not be called very often, because GitHub has a rate limit implemented """ url = "https://api.github.com/repos/jpylypiw/easywall-web/commits/master" req = urllib.request.Request( url, data=None, headers={ 'User-Agent': 'easywall by github.com/jpylypiw/easywall-web' } ) response = urllib.request.urlopen(req) return json.loads(response.read().decode('utf-8')) def get_latest_version(self): """ the function retrieves the latest version from github and returns the version string """ url = "https://raw.githubusercontent.com/jpylypiw/easywall-web/master/.version" req = urllib.request.Request( url, data=None, headers={ 'User-Agent': 'easywall by github.com/jpylypiw/easywall-web' } ) response = urllib.request.urlopen(req) data = response.read() return data.decode('utf-8') # ------------------------- # Rule Operations def get_rule_status(self, ruletype): """ the function checks if a custom / temporary rulefile exists and returns "custom" when a temporary rulefile exists or "production" when no file exists """ filepath = self.get_rule_file_path(ruletype, True) if not os.path.exists(filepath): filepath = self.get_rule_file_path(ruletype) if not os.path.exists(filepath): return "error" return "production" return "custom" def get_rule_file_path(self, ruletype, tmp=False): """ the function reads the configuration and returns the relative or absolute path to the rulefile for the ruletype """ filename = self.cfg.get_value("RULES", ruletype) if tmp: filepath = self.cfg.get_value("WEB", "rules_tmp_path") else: filepath = self.cfg.get_value("RULES", "filepath") # workaround because the easywall dir is one dir up - this is not pretty if filepath.startswith("."): filepath = "../" + filepath create_folder_if_not_exists(filepath) return filepath + "/" + filename def get_rule_list(self, ruletype): """ the function reads a file into the ram and returns a list of all rows in a list for example you get all the ip addresses of the blacklist in a array """ rule_list = [] status = self.get_rule_status(ruletype) filepath = self.get_rule_file_path(ruletype) if status == "custom": filepath = self.get_rule_file_path(ruletype, True) with open(filepath, 'r') as rulesfile: for rule in rulesfile.read().split('\n'): if rule.strip() != "": rule_list.append(rule) return rule_list def save_rule_list(self, ruletype, rulelist, to_production=False): """ the function writes a list of strings into a rulesfile for example it saves the blacklist rules into the blacklist temporary rulesfile """ filepath = self.get_rule_file_path(ruletype, True) state = self.get_rule_status(ruletype) if to_production: filepath = self.get_rule_file_path(ruletype) try: rulelist = list(filter(None, rulelist)) if not to_production or to_production and state == "custom": with open(filepath, mode='wt', encoding='utf-8') as rulesfile: rulesfile.write('\n'.join(rulelist)) except Exception as exc: print("{}".format(exc)) return False return True # ------------------------- # Acceptance Operations def apply_rule_list(self, ruletype): """ the function copys the rulefile from the temporary path to the permanent path this is used to copy the rules from web to easywall folder """ rule_list = self.get_rule_list(ruletype) self.save_rule_list(ruletype, rule_list, True) def get_last_accept_time(self): """ the function retrieves the modify time of the acceptance file and compares the time to the current time """ filepath = "../" + self.cfg.get_value("ACCEPTANCE", "filename") if os.path.exists(filepath): mtime = os.path.getmtime(filepath) mtime = datetime.utcfromtimestamp(mtime) mtime = mtime.replace( tzinfo=timezone.utc).astimezone( tz=None).replace( tzinfo=None) now = datetime.now() return time_duration_diff(mtime, now) else: return "never" def check_acceptance_running(self): """ the function checks if there is a running file. """ filepath = "../.running" if os.path.exists(filepath): return True return False
class TestConfig(unittest.TestCase): """ TODO: Doku """ def setUp(self) -> None: content = """[TEST] teststring = string testboolean = true testint = 1 testfloat = 1.1 """ create_file_if_not_exists("test.ini") write_into_file("test.ini", content) self.config = Config("test.ini") def tearDown(self) -> None: delete_file_if_exists("test.ini") def test_constructor_file_not_found(self) -> None: """ TODO: Doku """ with self.assertRaises(FileNotFoundError): Config("test2.ini") def test_constructor_file_not_read(self) -> None: """ TODO: Doku """ create_file_if_not_exists("test.ini") content = """[DEFAULT] goodcontent = test badcontent """ write_into_file("test.ini", content) with self.assertRaises(ParsingError): Config("test.ini") def test_get_value_error(self) -> None: """ TODO: Doku """ self.assertEqual(self.config.get_value("TEST", "notexistent"), "") def test_get_value_bool(self) -> None: """ TODO: Doku """ self.assertEqual(self.config.get_value("TEST", "testboolean"), True) def test_get_value_int(self) -> None: """ TODO: Doku """ self.assertEqual(self.config.get_value("TEST", "testint"), 1) def test_get_value_float(self) -> None: """ TODO: Doku """ self.assertEqual(self.config.get_value( "TEST", "testfloat"), float(1.1)) def test_get_sections(self) -> None: """ TODO: Doku """ self.assertIn("TEST", self.config.get_sections()) def test_set_value_success(self) -> None: """ TODO: Doku """ self.assertEqual(self.config.set_value( "TEST", "teststring", "erfolg"), True) def test_set_value_fail_section(self) -> None: """ TODO: Doku """ self.assertEqual(self.config.set_value("TEST2", "asd", "asd"), False)
def prepare_configuration() -> None: """TODO: Doku.""" if file_exists(EASYWALL_CONFIG_PATH): rename_file(EASYWALL_CONFIG_PATH, EASYWALL_CONFIG_BACKUP_PATH) if file_exists(WEB_CONFIG_PATH): rename_file(WEB_CONFIG_PATH, WEB_CONFIG_BACKUP_PATH) if file_exists(LOG_CONFIG_PATH): rename_file(LOG_CONFIG_PATH, LOG_CONFIG_BACKUP_PATH) content = """[IPTABLES] log_blocked_connections = yes log_blocked_connections_log_limit = 60 log_blacklist_connections = yes log_blacklist_connections_log_limit = 60 drop_broadcast_packets = yes drop_multicast_packets = yes drop_anycast_packets = yes ssh_brute_force_prevention = yes ssh_brute_force_prevention_log = yes ssh_brute_force_prevention_connection_limit = 5 ssh_brute_force_prevention_log_limit = 60 icmp_flood_prevention = yes icmp_flood_prevention_log = yes icmp_flood_prevention_connection_limit = 5 icmp_flood_prevention_log_limit = 60 drop_invalid_packets = yes drop_invalid_packets_log = yes drop_invalid_packets_log_limit = 60 port_scan_prevention = yes port_scan_prevention_log = yes port_scan_prevention_log_limit = 60 [IPV6] enabled = yes icmp_allow_router_advertisement = yes icmp_allow_neighbor_advertisement = yes [ACCEPTANCE] enabled = yes duration = 1 timestamp = [EXEC] iptables = /sbin/iptables ip6tables = /sbin/ip6tables iptables-save = /sbin/iptables-save ip6tables-save = /sbin/ip6tables-save iptables-restore = /sbin/iptables-restore ip6tables-restore = /sbin/ip6tables-restore """ create_file_if_not_exists(EASYWALL_CONFIG_PATH) write_into_file(EASYWALL_CONFIG_PATH, content) content = """[WEB] username = demo password = xxx bindip = 0.0.0.0 bindport = 12227 login_attempts = 100 login_bantime = 1800 [VERSION] version = 0.0.0 sha = 12345 date = 2020-01-01T00:00:00Z timestamp = 1234 [uwsgi] ssl-option = 268435456 https-socket = 0.0.0.0:12227,ssl/easywall.crt,ssl/easywall.key,HIGH processes = 5 threads = 2 callable = APP master = yes die-on-term = yes wsgi-file = easywall/web/__main__.py need-plugin = python3 buffer-size = 16384 """ create_file_if_not_exists(WEB_CONFIG_PATH) write_into_file(WEB_CONFIG_PATH, content) config = Config(WEB_CONFIG_PATH) config.set_value("VERSION", "timestamp", str(int(time()))) content = """[LOG] level = info to_files = no to_stdout = yes filepath = /var/log filename = easywall.log """ create_file_if_not_exists(LOG_CONFIG_PATH) write_into_file(LOG_CONFIG_PATH, content)
class Webutils(object): """the class is called in the route modules and contains non route-specific functions""" def __init__(self): self.cfg = Config("config/web.ini") self.cfg_easywall = Config(CONFIG_PATH) def check_login(self): """the function checks if the user/session is logged in""" if not session.get('logged_in'): return False return True # ------------------------- # Payload Operations def get_default_payload(self, title, css="easywall"): """the function creates a object of information that are needed on every page""" payload = DefaultPayload() payload.year = datetime.today().year payload.title = title payload.customcss = css payload.machine = self.get_machine_infos() payload.latest_version = self.cfg.get_value("VERSION", "version") payload.current_version = file_get_contents(".version") payload.commit_sha = self.cfg.get_value("VERSION", "sha") payload.commit_date = self.get_commit_date( self.cfg.get_value("VERSION", "date")) return payload def get_machine_infos(self): """the function retrieves some information about the host and returns them as a list""" infos = {} infos["Machine"] = platform.machine() infos["Hostname"] = platform.node() infos["Platform"] = platform.platform() infos["Python Build"] = platform.python_build() infos["Python Compiler"] = platform.python_compiler() infos["Python Implementation"] = platform.python_implementation() infos["Python Version"] = platform.python_version() infos["Release"] = platform.release() infos["Libc Version"] = platform.libc_ver() return infos # ------------------------- # Update Info Operations def get_commit_date(self, datestring): """ the function compares a datetime with the current date for comparing the datestring parameter is in UTC timezone """ date1 = datetime.strptime(str(datestring), "%Y-%m-%dT%H:%M:%SZ") date1 = date1.replace(tzinfo=timezone.utc).astimezone(tz=None).replace( tzinfo=None) date2 = datetime.now() return time_duration_diff(date1, date2) def update_last_commit_infos(self): """ the function retrieves the last commit information after a specific waiting time after retrieving the information they are saved into the config file """ currtime = int(time.time()) lasttime = self.cfg.get_value("VERSION", "timestamp") waitseconds = 3600 # 60 minutes × 60 seconds if currtime > (lasttime + waitseconds): commit = self.get_latest_commit() self.cfg.set_value("VERSION", "version", self.get_latest_version()) self.cfg.set_value("VERSION", "sha", commit["sha"]) self.cfg.set_value("VERSION", "date", commit["commit"]["author"]["date"]) self.cfg.set_value("VERSION", "timestamp", str(currtime)) def get_latest_commit(self): """ retrieves the informations of the last commit from github as json and converts the information into a python object for example the object contains the last commit date and the last commit sha This function should not be called very often, because GitHub has a rate limit implemented """ url = "https://api.github.com/repos/jpylypiw/easywall/commits/master" req = urllib.request.Request( url, data=None, headers={'User-Agent': 'easywall by github.com/jpylypiw/easywall'}) response = urllib.request.urlopen(req) return json.loads(response.read().decode('utf-8')) def get_latest_version(self) -> str: """ the function retrieves the latest version from github and returns the version string """ url = "https://raw.githubusercontent.com/jpylypiw/easywall/master/.version" req = urllib.request.Request( url, data=None, headers={'User-Agent': 'easywall by github.com/jpylypiw/easywall'}) response = urllib.request.urlopen(req) data = response.read() return data.decode('utf-8') # ------------------------- # Acceptance Operations def get_last_accept_time(self): """ the function retrieves the modify time of the acceptance file and compares the time to the current time """ timestamp = self.cfg_easywall.get_value("ACCEPTANCE", "timestamp") if timestamp == "": return "never" timestamp = datetime.strptime(timestamp, '%Y-%m-%d %H:%M:%S.%f') now = datetime.now() return time_duration_diff(timestamp, now) def get_acceptance_status(self): """ get the status of the current acceptance """ filepath = ".acceptance_status" if file_exists(filepath): return file_get_contents(filepath) return ""