First push

This commit is contained in:
Starthur 2022-10-22 23:34:58 +02:00
commit 6276ac11f1
Signed by: Starthur
GPG Key ID: 68AD34A2CF658E32
6 changed files with 264 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
__pycache__
secret.py
venv

37
README.md Normal file
View File

@ -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="<OTP_secret>" # Or None
APP_URL="https://profile.intra.42.fr/oauth/applications/<app_id>"
```
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

0
__init__.py Normal file
View File

21
default.py Normal file
View File

@ -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")

201
fortytwo_auto_api.py Normal file
View File

@ -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-<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()

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
pyotp==2.7.0
selenium==4.5.0