Project avaliable at GitHub.
This project was created for educational purposes only. It is not intended to be used for malicious purposes, and it may violate the terms of service of the captive portal. Use it at your own risk.
The scripts have been tested on a Raspberry Pi 2 Model B running Raspberry Pi OS, with a WiFi USB adapter. It may work on other models and operating systems with minor modifications.
It is recommended to have a basic understanding of the Linux terminal and Puppeteer programming language.
The initial connection to the captive portal is done manually, and it may differ from system to system. The scripts will only work after the initial connection has been made.
There are two scripts, one written in Puppeteer (JavaScript) and the other in Shell (Bash). The Puppeteer script is more reliable, but it requires more resources. The Shell script is less reliable, but it is faster and requires fewer resources.
Puppeteer
The first iteration of this project was done with Puppeteer, so it is necessary to have Node.js and npm installed to run it. Although this script is not the most efficient, it is the easiest to use. It is divided into two scripts, one that is executed at system startup and another that is executed periodically to keep the connection active.
cfe-boot.sh and cfe-boot.js
-
Importing Required Modules: The script starts by importing the necessary modules:
puppeteerfor browser automation,fsfor file system operations, andpathfor handling file paths.const puppeteer = require('puppeteer'); const fs = require('fs'); const path = require('path'); -
Defining File Path: The
urlFilePathvariable is defined to store the path of the file where the captive portal URL will be saved.const urlFilePath = path.resolve(__dirname, 'captive_portal_url.txt'); -
Timestamp Function: A helper function
getCurrentTimestampis defined to get the current timestamp in ISO format.function getCurrentTimestamp() { return new Date().toISOString(); } -
Launching Browser: The script launches a new browser instance in headless mode with a custom user data directory. It specifies the path to the Chromium executable and includes necessary arguments.
const browser = await puppeteer.launch({ headless: true, executablePath: '/usr/bin/chromium-browser', // Path to the Chromium executable args: [ '--no-sandbox', '--disable-setuid-sandbox', '--user-data-dir=/home/pi/cfe/puppeteer-data-dir' // Custom user data directory ] }); -
Opening New Page: A new page is opened in the browser.
const page = await browser.newPage(); -
Navigating to Known URL: The script navigates to a known URL (
http://example.com) to trigger the captive portal redirection.const knownUrl = 'http://example.com'; await page.goto(knownUrl); -
Waiting for Redirection: It waits for the redirection to the captive portal by waiting for network activity to idle.
await page.waitForNavigation({ waitUntil: 'networkidle0' }); -
Capturing Captive Portal URL: The redirected URL (captive portal URL) is captured and logged with a timestamp.
const loginUrl = page.url(); console.log(`[${getCurrentTimestamp()}] Detected captive portal URL: ${loginUrl}`); -
Saving URL to File: The captured URL is saved to a file specified by
urlFilePath.fs.writeFileSync(urlFilePath, loginUrl); -
Navigating to Captive Portal: The script navigates to the captive portal login URL.
await page.goto(loginUrl); -
Waiting for Checkbox Label: It waits for the label associated with the checkbox to be present and visible.
await page.waitForSelector('label[for="option-aceptar"]', { visible: true }); -
Clicking Checkbox: The script ensures the label is clickable and clicks it.
const label = await page.$('label[for="option-aceptar"]'); const isLabelClickable = await label.boundingBox(); if (isLabelClickable) { await label.click(); } else { throw new Error('Label is not clickable'); } -
Waiting for Login Button: It waits for the login button to be enabled and visible.
await page.waitForSelector('#annoyBtn:not([disabled])', { visible: true }); -
Logging Success: A success message is logged with a timestamp.
console.log(`[${getCurrentTimestamp()}] Login successful!`); -
Closing Browser: Finally, the browser instance is closed.
await browser.close();
cfe-puppeteer.sh and cfe.js
The bash script is responsible for clearing the cache generated by Puppeteer and running the Node.js script. The Node.js script opens a new tab in the browser with the URL obtained by the first script and navigates to the CFE TEIT login page and clicks the “Aceptar” button.
cfe-puppeteer.sh
-
Navigating to Script Directory: Change the current directory to where the Node.js script is located.
cd /home/pi/cfe -
Deleting Puppeteer Data Directory: Print a message indicating that the Puppeteer data directory is about to be deleted.
echo "Deleting the puppeteer data directory" -
Removing Puppeteer Data Directory: Forcefully remove the Puppeteer data directory and its contents to ensure a clean state.
rm -rf /home/pi/cfe/puppeteer-data-dir -
Running Node.js Script: Execute the Node.js script
cfe.js.node cfe.js -
Deleting Puppeteer Data Directory Again: Print the same message again, indicating that the Puppeteer data directory will be deleted once more after the Node.js script has run.
echo "Deleting the puppeteer data directory" -
Removing Puppeteer Data Directory Again: Forcefully remove the Puppeteer data directory to clean up any data generated during the execution of the Node.js script.
rm -rf /home/pi/cfe/puppeteer-data-dir
Bash
The second iteration of this project was done with bash scripts, as Puppeteer is not the best option for this type of task. A bash script is created to make a POST request to the CFE TEIT login page and a systemd service to run the script periodically.
cfe.sh
The script is designed to work with the systemd service to run periodically and keep the connection active while creating a log with the date and time of each request.
To configure it, it is necessary to change the cURL request with the data obtained from manually connecting through the browser.
If the device is being used in headless mode, it is necessary to obtain a bridge connection to the device to capture the request, either through an SSH tunnel or by bridging through the local network.
For the Raspberry Pi 2 with Raspberry Pi OS, the network adapter bridge connection can be configured to the ethernet port with the following command:
sudo nmcli c add con-name custom-shared-con type ethernet ifname eth0 ipv4.method shared ipv6.method ignore
sudo nmcli c up custom-shared-con
Once the bridge is configured, it is necessary to start the login process manually.
On the login screen:

It is necessary to accept the Terms and Conditions to activate the “Aceptar” button:

Open the browser console, enable log preservation, right-click the POST method called “login”, and navigate to “Copy as cURL”:

It is necessary to change the cURL request data in the cfe.sh script with the data obtained from the request.
cfe.sh
-
Initializing Variables: Initialize the attempt counter, set the maximum number of attempts, and set the delay between attempts in seconds.
attempt=0 # Initialize the attempt counter max_attempts=5 # Set the maximum number of attempts delay=30 # Set the delay between attempts in seconds -
Starting Login Attempts: Print a message indicating the start of login attempts.
echo "$(date '+%Y-%m-%d %H:%M:%S') Starting login attempts..." -
Login Loop: Loop through the login attempts until the maximum number of attempts is reached.
while [ $attempt -lt $max_attempts ]; do -
Making cURL Request: Replace the following curl command with the one you captured from the browser.
response=$( # Replace the following curl command with the one you captured from the browser curl -s 'https://acs.cfeteit.net:19008/portalauth/login' \ -H 'Accept: application/json, text/javascript, */*; q=0.01' \ -H 'Accept-Language: es-419,es;q=0.9' \ -H 'Cache-Control: no-cache' \ -H 'Connection: keep-alive' \ -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' \ -H 'Origin: https://acs.cfeteit.net:19008' \ -H 'Pragma: no-cache' \ -H 'Referer: https://acs.cfeteit.net:19008/portalpage/435dc728e5eb446aac41b5feee0acb44/20231108192230/pc/auth.html?apmac=c8b6d3b90460&uaddress=10.1.1.27&umac=6083e73b8cf7&authType=2&lang=en_US&ssid=Q0ZFIEludGVybmV0&pushPageId=509759ea-fa17-438f-9cf7-34b82401c9a4' \ -H 'Sec-Fetch-Dest: empty' \ -H 'Sec-Fetch-Mode: cors' \ -H 'Sec-Fetch-Site: same-origin' \ -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' \ -H 'X-Requested-With: XMLHttpRequest' \ -H 'X-XSRF-TOKEN: 529ba70983be7afe514bdb9efbe0c4540107430ae7d68666f1023eda3bbd7604' \ -H 'sec-ch-ua: "Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"' \ -H 'sec-ch-ua-mobile: ?0' \ -H 'sec-ch-ua-platform: "Windows"' \ --data-raw 'pushPageId=509759ea-fa17-438f-9cf7-34b82401c9a4&userPass=~anonymous&esn=&apmac=c8b6d3b90460&armac=&authType=2&ssid=Q0ZFIEludGVybmV0&uaddress=10.1.1.27&umac=6083e73b8cf7&businessType=&acip=&agreed=1®isterCode=&questions=&dynamicValidCode=&dynamicRSAToken=&userName=~anonymous' ) -
Checking Success: Check if the login was successful by searching for
"success":truein the response.success=$(echo "$response" | grep -o '"success":true') -
Handling Success: If the login was successful, print a success message and exit the script.
if [ "$success" ]; then echo "$(date '+%Y-%m-%d %H:%M:%S') Login successful" exit 0 -
Handling Failure: If the login failed, print a failure message, increment the attempt counter, and wait for the specified delay before retrying.
else echo "$(date '+%Y-%m-%d %H:%M:%S') Login failed, attempt $((attempt + 1))/$max_attempts" attempt=$((attempt + 1)) sleep $delay fi -
Exiting After All Attempts: If all login attempts failed, print a message indicating that all attempts failed.
echo "$(date '+%Y-%m-%d %H:%M:%S') All attempts failed." -
End of Script: Close the login loop.
done
Conclusion
I hope this guide helps you set up your Raspberry Pi 2 to automatically sign in to the captive portal of CFE Internet. Remember to use this project responsibly and at your own risk. If you have any questions or suggestions, feel free to reach out to me.