First push
This commit is contained in:
commit
6276ac11f1
|
@ -0,0 +1,3 @@
|
||||||
|
__pycache__
|
||||||
|
secret.py
|
||||||
|
venv
|
|
@ -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,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")
|
|
@ -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()
|
|
@ -0,0 +1,2 @@
|
||||||
|
pyotp==2.7.0
|
||||||
|
selenium==4.5.0
|
Loading…
Reference in New Issue