202 lines
6.7 KiB
Python
202 lines
6.7 KiB
Python
#!/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-<appid>]
|
|
- [data-app-secret-<appid>]
|
|
- [data-app-next-secret-<appid>]
|
|
Construct a dict with :
|
|
{
|
|
"uid": "<app-uid>",
|
|
"secret": "<app-secret>",
|
|
"next": "<app-next-secret>" (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()
|