commit 6276ac11f1a53b3dde4687f1509cee1c6a0b38e9 Author: Starthur Date: Sat Oct 22 23:34:58 2022 +0200 First push diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..480a5be --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__ +secret.py +venv diff --git a/README.md b/README.md new file mode 100644 index 0000000..6012eb5 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# Get your 42 API creds automaticly + +## Requierements +You need a browser (Chrome/Firefox) and its associated driver. +This two executable need to be in PATH. + +Chrome example: +``` +wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb +sudo dkpg -i google-chrome-stable_current_amd64.deb +sudo apt --fix-broken install +mkdir -p /opt/web_drivers; cd /opt/web_drivers/; wget https://chromedriver.storage.googleapis.com/107.0.5304.18/chromedriver_linux64.zip +unzip chromedriver_linux64.zip +``` +And add /opt/web_drivers to your path. If you have any problems, please refer to selenium docs : https://selenium-python.readthedocs.io/installation.html + +After that : +``` +python3 -m pip install -r requirements.txt +``` + +## Usage +You will need to create a file named `secret.py` containing : +```python +LOGIN_42="<42_login>" +PASSWORD_42="<42_password>" +OTPSECRET_42="" # Or None +APP_URL="https://profile.intra.42.fr/oauth/applications/" +``` + +The class with all operations is in `fortytwo_auto_api`. You will find a `default.py` containing an example. + +## Problems +- Take care of the `secret.py` file, configure ACL correctly +- Can't test with Firefox because i'm on Windows and WSL (sorry 😒) +- If 42 change its HTML code, it will certainly break i will try to update it + diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/default.py b/default.py new file mode 100644 index 0000000..364335e --- /dev/null +++ b/default.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +"""Sample code +Get API Keys and write this to an env file +""" + +from fortytwo_auto_api import fortytwo_auto_keys +import secret + +fortytwo = fortytwo_auto_keys( + login=secret.LOGIN_42, + password=secret.PASSWORD_42, + app_url=secret.APP_URL, + otp_secret=secret.OTPSECRET_42, +) + +fortytwo.auto() + +api_keys = fortytwo.keys +with open("env_file", "w+") as f: + f.write(f"OAUTH2_INTRA42_CLIENT_ID={api_keys['uid']}\n") + f.write(f"OAUTH2_INTRA42_CLIENT_SECRET={api_keys['secret']}\n") diff --git a/fortytwo_auto_api.py b/fortytwo_auto_api.py new file mode 100644 index 0000000..1aa190c --- /dev/null +++ b/fortytwo_auto_api.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +"""Auto retrieve API Keys from forty-two account + +This class use headless browser to log into your intranet +account and retrieve Client UID and Secret. +This class can also handle auto-regenerate and auto-replace your +current secret +""" + +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.chrome.options import Options +from selenium.common.exceptions import NoSuchElementException +from time import sleep +from datetime import datetime +import pyotp + +__author__ = "Arthur Trouillet" +__credits__ = ["Arthur Trouillet"] +__license__ = "The Unlicense" +__version__ = "1.0.0" +__maintainer__ = "Arthur Trouillet" +__email__ = "atrouill@student.42.fr" +__status__ = "Production" + + +class fortytwo_auto_keys: + def __init__( + self, + login: str, + password: str, + app_url: str, + otp_secret: str = None, + force_renew: bool = False, + day_before_renew: int = 3, + use_chrome: bool = True, + ): + """Initialize a browser and set parameters for current sessions + + Args: + login (str): login of your 42 account + password (str): password of you 42 account + app_url (str): Intra url of the application. Format like https://profile.intra.42.fr/oauth/applications/[app_id] + otp_secret (str, optional): OTP secret, use for generate TOTP. Defaults to None. + force_renew (bool, optional): Force renew of secret. Defaults to False. + day_before_renew (int, optional): Renew n days before end of validation. + Ignored if [force_renew == True]. Defaults to 3. + use_chrome (bool, optional): If true will use chrome driver, otherwise Firefox. Defaults to True. + """ + self.login = login + self.password = password + self.app_url = app_url + self.otp_secret = otp_secret + self.force_renew = force_renew + self.day_before_renew = day_before_renew + self.__keys = dict() + + options = Options() + options.headless = True + if use_chrome: + self.browser = webdriver.Chrome(options=options) + else: + self.browser = webdriver.Firefox(options=options) + + self.browser.get(self.app_url) + + def __del__(self): + """Destructor + Close browser + """ + self.browser.close() + + @property + def keys(self): + self.__parse_keys() + return self.__keys + + def handle_login(self) -> None: + """Handle fortytwo intra login + Fill field login/password and click on login button + """ + login_field = self.browser.find_element(By.NAME, "user[login]") + password_field = self.browser.find_element(By.NAME, "user[password]") + login_field.clear() + password_field.clear() + login_field.send_keys(self.login) + password_field.send_keys(self.password) + self.browser.find_element(By.NAME, "commit").click() + + def handle_totp(self) -> None: + """Handle TOTP login + Fill totp field and click on login button + """ + code_generator = pyotp.TOTP(self.otp_secret) + totp_field = self.browser.find_element(By.NAME, "users[code]") + totp_field.clear() + totp_field.send_keys(code_generator.now()) + self.browser.find_element(By.NAME, "commit").click() + + def get_validity_date(self) -> datetime: + """Get date validity of the api secret + + Returns: + datetime: Date time of end of validity + """ + fields = self.browser.find_elements(By.CLASS_NAME, "rotation-actions") + for field in fields: + if "Valid until" in field.text: + day, month, year = map( + int, field.text.split(' ')[2].split('/')) + hour, minute = 10, 0 + iso_time = datetime(year, month, day, hour, minute) + return iso_time + + def __can_replace(self) -> bool: + """Test if "Replace now" button is present + + Returns: + bool: True if button is present, false otherwise + """ + try: + self.replace_button = self.browser.find_element( + By.LINK_TEXT, "Replace now") + except NoSuchElementException: + self.replace_button = None + return False + return True + + def __can_generate(self) -> bool: + """Test if "Generate now" button is present + + Returns: + bool: True is button is present + """ + try: + self.generate_button = self.browser.find_element( + By.LINK_TEXT, "Generate now") + except NoSuchElementException: + self.generate_button = None + return False + return True + + def __time_to_renew(self) -> bool: + """Test if it's time to renew the secret + Use the parameter of the constructor. By default 5 + + Returns: + bool: True if it's time to generate new secret + """ + validity = self.get_validity_date() + delta = validity - datetime.now() + if delta.days > self.day_before_renew: + return False + return True + + def __parse_keys(self) -> None: + """Parse page to obtain API keys + Use value in `data-copy`, can be (atm): + - [data-app-uid-] + - [data-app-secret-] + - [data-app-next-secret-] + Construct a dict with : + { + "uid": "", + "secret": "", + "next": "" (may not be present) + } + """ + keys = self.browser.find_elements(By.CLASS_NAME, 'copy') + for key in keys: + type = key.get_attribute("data-copy").split('-')[2] + self.__keys[type] = key.get_attribute("data-clipboard-text") + + def generate_new_secret(self) -> None: + """Generate new secret. + Emulate click on "Generate now" and "Replace now" + """ + self.__can_generate() + if self.generate_button: + self.generate_button.click() + sleep(2) + self.__can_replace() + if self.replace_button: + self.replace_button.click() + sleep(2) + + def auto(self) -> None: + """Automatic operations for defaults operation. + Will do: + 1/ Handle login + a/ If TOTP, handle totp + 2/ Cehck if a renew is needed + a/ Generate if needed + 3/ Parse keys, will be accessible with keys propreties + """ + self.handle_login() + while "Otp" in self.browser.title: + self.handle_totp() + if self.force_renew or self.__time_to_renew(): + self.generate_new_secret() + self.__parse_keys() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c0973a6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pyotp==2.7.0 +selenium==4.5.0