Published on

Automating Captive Portal Login at SAP

Authors
  • avatar
    Name
    Teddy Xinyuan Chen
    Twitter
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)).

SAP Logon, AKA SAP GUI, the worst piece of software I ever had to deal with

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

  1. 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.

  1. 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.

  2. Running macOS system utilities like networksetup and Apple Script from Python to avoid installing yet another library to abstract them away.

  3. Using asyncio.wait_for & timeout to check Internet connection.