connect.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. import time
  2. from dataclasses import dataclass
  3. from typing import Optional
  4. from otppy import OTP
  5. from selenium import webdriver
  6. from selenium.common.exceptions import ElementClickInterceptedException
  7. from selenium.common.exceptions import NoSuchElementException
  8. from selenium.common.exceptions import StaleElementReferenceException
  9. from selenium.common.exceptions import TimeoutException
  10. from selenium.webdriver.common.by import By
  11. from selenium.webdriver.firefox.options import Options as FirefoxOptions
  12. from selenium.webdriver.support import expected_conditions as EC
  13. from selenium.webdriver.support.ui import WebDriverWait
  14. USERNAME_INPUT_NAME = "loginfmt"
  15. PASSWORD_INPUT_NAME = "passwd"
  16. MFA_INPUT_ID = "otc"
  17. CONTINUE_BUTTON_ID = "idSIButton9"
  18. MFA_CONTINUE_BUTTON_ID = "idSubmit_SAOTCC_Continue"
  19. MFA_ERROR_TEXT_ID = "idSpan_SAOTCC_Error_OTC"
  20. VPN_INSTALL_PAGE_EXCLUSIVE_ELEMENT_ID = "provisioning_action_label"
  21. MFA_MAX_RETRY_COUNT = 6
  22. ELEMENT_CHECK_DELAY = 0.5
  23. @dataclass
  24. class VPNCookie:
  25. domain: str
  26. cookie: str
  27. def login(
  28. username: str,
  29. password: str,
  30. mfa_secret: Optional[str] = None,
  31. vpn_site: str = "https://vpn.fhnw.ch",
  32. headless: bool = True,
  33. ) -> VPNCookie:
  34. options = FirefoxOptions()
  35. if headless:
  36. options.add_argument("--headless")
  37. driver = webdriver.Firefox(options=options)
  38. driver.get(vpn_site)
  39. _fill_login(driver, username, password)
  40. _fill_mfa(driver, mfa_secret)
  41. _confirm_stay_signed_in(driver)
  42. _is_on_install_page(driver)
  43. return _get_webvpn_cookie(driver)
  44. def _fill_login(driver: webdriver.Firefox, username: str, password: str) -> None:
  45. if not _find_element(driver, By.NAME, USERNAME_INPUT_NAME):
  46. raise ValueError("Could not find login page!")
  47. driver.find_element(By.NAME, USERNAME_INPUT_NAME).send_keys(username)
  48. driver.find_element(By.NAME, PASSWORD_INPUT_NAME).send_keys(password)
  49. click_continue(driver) # Confirm username
  50. click_continue(driver) # Confirm password
  51. def _fill_mfa(driver: webdriver.Firefox, mfa_secret: Optional[str]) -> None:
  52. # Check if we have to enter a MFA code
  53. if mfa_secret is None:
  54. return
  55. for _ in range(MFA_MAX_RETRY_COUNT):
  56. if not _find_element(driver, By.ID, MFA_INPUT_ID):
  57. time.sleep(ELEMENT_CHECK_DELAY)
  58. continue
  59. mfa_code = get_mfa_code(mfa_secret)
  60. driver.find_element(By.NAME, MFA_INPUT_ID).send_keys(mfa_code)
  61. click_continue(driver, MFA_CONTINUE_BUTTON_ID) # Confirm otp
  62. # Check for any errors
  63. if _find_element(driver, By.ID, MFA_ERROR_TEXT_ID, 2):
  64. # Found an error
  65. break
  66. def _confirm_stay_signed_in(driver: webdriver.Firefox) -> None:
  67. click_continue(driver) # Confirm stay signed in
  68. def _find_element(driver: webdriver.Firefox, by: str, item: str, wait: int = 8) -> bool:
  69. try:
  70. w = WebDriverWait(driver, wait)
  71. w.until(EC.presence_of_element_located((by, item)))
  72. return True
  73. except TimeoutException:
  74. return False
  75. def _is_on_install_page(driver: webdriver.Firefox) -> None:
  76. if not _find_element(driver, By.ID, VPN_INSTALL_PAGE_EXCLUSIVE_ELEMENT_ID):
  77. raise ValueError("Could not find install page!")
  78. def _get_webvpn_cookie(driver: webdriver.Firefox) -> VPNCookie:
  79. webvpn_cookie = driver.get_cookie("webvpn")
  80. driver.close()
  81. if webvpn_cookie is None:
  82. raise ValueError(
  83. "Failed to find the webvpn cookie. Maybe the authentication has failed?"
  84. )
  85. webvpn_domain = webvpn_cookie["domain"]
  86. webvpn_value = webvpn_cookie["value"]
  87. return VPNCookie(domain=webvpn_domain, cookie=webvpn_value)
  88. def click_continue(driver: webdriver.Firefox, btn_id: str = CONTINUE_BUTTON_ID) -> bool:
  89. """Returns true if still on login page, false if not"""
  90. for _ in range(16):
  91. try:
  92. driver.find_element(By.ID, btn_id).click()
  93. return True
  94. except (ElementClickInterceptedException, StaleElementReferenceException):
  95. pass
  96. except NoSuchElementException:
  97. if ".fhnw.ch" in driver.current_url:
  98. return False
  99. time.sleep(ELEMENT_CHECK_DELAY)
  100. return ".fhnw.ch" not in driver.current_url
  101. def get_mfa_code(secret: str) -> str:
  102. otp = OTP.fromb32(secret)
  103. code: str = otp.TOTP()[0]
  104. return code