- Published on
Automating Captive Portal Login at SAP
- Authors
- Name
- Teddy Xinyuan Chen
Table of Contents
Background
I'm doing my internship at SAP SE, one of the world's largest software company where the DX of people inside doesn't matter (for those of you who think JIRA is awful, you should learn SAP :) (or maybe learn some German first)).
One of the thing I had to do to connect my personal Mac to the office's SAP-Guest
Wi-Fi involes these steps:
- Quit my proxy software (which messes with the DNS of the captive portal)
- Connect to the Wi-Fi
- Dismiss the macOS's captive portal prompt (because password autofill doesn't work there), and open the captive portal
http://www.msftconnecttest.com/redirect
in my web browser instead. - Log in with my SAP credentials, the I number and a very long password.
- Wait til I was redirected to the company's homepage and I know the the connection was successful.
- Re-enable my proxy software.
The browser submits my MAC address to the captive portal and I'm good for 8 hours, but doing this every day is a PITA so I had to hack together a dirty script to do the job for me.
How
Selenium
Selenium with Python for browser automation FTW:
# ~/config/scripts/captive-portal/sap_guest_autologin.py
# btw Surge is the proxy software I use
class TestSAPGuestWifiCaptivePortalLogin:
# ...
def test_login(self):
# Test name: login
# Step # | name | target | value
cp_url = 'http://www.msftconnecttest.com/redirect'
cp_login_page_domain = 'wlan.sap.com'
cp_logged_in_redirecting_url_pattern = r'^https?://sap\.(cn|com)(/.*)$'
# cSpell:disable
# 1 | open | /guest/sap_guest_register_v3_login.php?cmd=login&switchip=10.130.64.6&mac=my_mac&ip=10.207.185.23&essid=SAP%2DGuest&apname=the-ap-at-sap-building&apgroup=SAP_cgi_vht_hbr&url=http%3A%2F%2Fwww%2Emsftconnecttest%2Ecom%2Fredirect&_browser=1 |
# cSpell:enable
self.driver.get(cp_url)
try:
WebDriverWait(self.driver, 10).until(
# lambda driver: cp_login_page_domain in driver.current_url
# or 'msn' in driver.current_url
lambda driver: cp_login_page_domain in self.get_href_with_js()
or 'msn' in self.get_href_with_js()
)
finally:
redirected_url = self.driver.current_url
if 'msn' in redirected_url:
print('You\'re being redirected to msn site')
print(
'looks like you\'ve already logged in to SAP-Guest Wi-Fi network today.'
)
if cp_login_page_domain not in redirected_url:
print('check if surge is quit')
print('if yes, you\'ve already logged in to SAP-Guest Wi-Fi network today.')
return
# ...
# ...
Utility functions (Wi-Fi operations, Surge.app control, conneciton checking)
# ...
# ...
SAP_GUEST_WIFI_SSID = 'SAP-Guest'
# SURGE_PROCESS_NAMES_TO_KILL = ['Surge', 'com.nssurge.surge-mac.helper']
SURGE_PROCESS_NAMES_TO_KILL = ['Surge']
PROXY_TEST_URL = 'http://cp.cloudflare.com/generate_204'
async def check_internet_connection() -> bool:
async def check() -> bool:
from urllib.request import urlopen
try:
r = urlopen(PROXY_TEST_URL)
return r.status == 204
except Exception:
return False
try:
result = await asyncio.wait_for(check(), timeout=1.0)
return result
except asyncio.TimeoutError:
print(f'GETTING {PROXY_TEST_URL} timed out.')
return False
def turn_on_wifi():
run(['networksetup', '-setairportpower', 'en0', 'on'])
def connect_wifi(ssid: str):
run(['networksetup', '-setairportnetwork', 'en0', ssid])
def get_connected_wifi_ssid() -> str:
p = run(['networksetup', '-getairportnetwork', 'en0'], capture_output=True)
output = p.stdout.decode()
ssid = output.strip().partition(': ')[-1]
return ssid
# quit Surge.app with osascript and subprocess
def quit_surge():
print('Quitting Surge.app')
for process_name in SURGE_PROCESS_NAMES_TO_KILL:
run(['killall', process_name])
run(['osascript', '-e', 'tell app "Surge" to quit'])
def is_surge_running() -> bool:
return any(
(run(['pgrep', process_name], capture_output=True).returncode == 0)
for process_name in SURGE_PROCESS_NAMES_TO_KILL
)
main()
# ...
# ...
# ...
def main() -> None:
if get_connected_wifi_ssid() != SAP_GUEST_WIFI_SSID:
print('You are not connected to SAP-Guest Wi-Fi network.')
# print('Please connect to SAP-Guest Wi-Fi network and run this script again.')
# quit()
print('connecting to SAP-Guest Wi-Fi network...')
turn_on_wifi()
connect_wifi(SAP_GUEST_WIFI_SSID)
if (
asyncio.run(check_internet_connection())
and get_connected_wifi_ssid() == SAP_GUEST_WIFI_SSID
):
print(
'You\'re already connected to the Internet from the SAP-Guest Wi-Fi network.'
)
return
if is_surge_running():
quit_surge()
sleep(3)
if is_surge_running():
print('Detected running Surge.app, quitting')
raise RuntimeError('Surge is running, captive portal won\'t work.')
pytest_args = [f'{__file__}::{TestSAPGuestWifiCaptivePortalLogin.__name__}', '-s']
print('running pytest with args:', pytest_args)
pytest.main(pytest_args)
start_surge()
What I learned
- I can invoke
pytest
in a subprocess like this to launch the selenium automation:
pytest_args = [f'{__file__}::{TestSAPGuestWifiCaptivePortalLogin.__name__}', '-s']
pytest.main(pytest_args)
Which is kinda ugly, reminds me of the non-existent pip
Python API (the official supported way to use it is via the command line), but it works.
Since many steps are involved, and things can go wrong at each step, the branching of the script gets quite complicated.
To make things worse, re-running the script takes a lot of time since a browser instance needs to be launched.
And the login cannot be re-tested more than once in a day, unless I use randomized MAC address to convince the auth server that I'm not logged in yet. The most complicated branching logic is where you gets redirected in different status and stages of the captive portal login process.Running macOS system utilities like
networksetup
and Apple Script from Python to avoid installing yet another library to abstract them away.Using
asyncio.wait_for
& timeout to check Internet connection.