diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..b0af80f
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,28 @@
+name: CI - ShellCheck & Tests
+
+on:
+ pull_request:
+
+jobs:
+ ci:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Install dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y shellcheck bats
+
+ - name: Run ShellCheck on Backend scripts
+ run: |
+ cd Backend
+ shellcheck -x *.sh test/*.sh
+
+ - name: Run test runner
+ run: |
+ chmod +x Backend/test/run.sh
+ bash Backend/test/run.sh
+
diff --git a/Backend/CyberCafe_Daemon.sh b/Backend/CyberCafe_Daemon.sh
deleted file mode 100644
index a198f4c..0000000
--- a/Backend/CyberCafe_Daemon.sh
+++ /dev/null
@@ -1,204 +0,0 @@
-#!/bin/bash
-
-### GLOBAL VARIABLES ###
-# The status of the hotspot functionality (not necessarily the CyberCafe
-# infrastructure. Either 'up' or 'down'
-HS_STATUS='down'
-
-# This should be set to the hotspot's IP once the hotspot has been enabled. When
-# disabled, this variable should be blank again.
-LOCAL_IP=''
-
-# Between increments of this value we will not perform a more expensive check of
-# the status of the CyberCafe infrastructure. Once passed, we perform the checks
-# then reset the counter. In seconds.
-REFRESH_TIME=3600
-
-# Path to the file used to indicate the time we last did an expensive check of
-# the hotspot/CyberCafe infrastructure status. If the file doesn't exist, it's
-# assumed we're not ready to act as a CyberCafe router.
-STATUS_PATH="/data/data/com.android.myapplication/files/tmp/cybercafe.confirmed"
-
-# When 'true', we perform the expensive CyberCafe infrastructure check.
-TIME_TO_REFRESH=false
-
-
-### FUNCTIONS ###
-function check_hotspot_status {
- # Does it appear the hotspot is active?
- ip add show dev wlan0 | grep 192\.168\.43\. > /dev/null
- wlan_ip_status=$?
-
- if [[ $wlan_ip_status -eq 0 ]]; then
- HS_STATUS='up'
-
- elif [[ $wlan_ip_status -ne 0 && $HS_STATUS == 'up' ]]; then
- # Assume hotspot has recently gone done.
- HS_STATUS='down'
- TIME_TO_REFRESH=true
- else
- HS_STATUS='down'
- TIME_TO_REFRESH=false
- fi
-
- # HS_STATUS is down: TIME_TO_REFRESH is false, HS_STATUS=down
- # HS_STATUS is up, but the STATUS_PATH does not exist: Indicate refresh
- # HS_STATUS is up, and STATUS_PATH exists: Check STATUS_PATH age
- # STATUS_PATH age > REFRESH_TIME: Indicate refresh
- # STATUS_PATH age < REFRESH_TIME: Indicate no refresh
-
-
- if [[ $HS_STATUS == 'up' && ! -e $STATUS_PATH ]]; then
- TIME_TO_REFRESH=true
-
- elif [[ $HS_STATUS == 'up' && -e $STATUS_PATH ]]; then
- # If the CyberCafe status file exists and the hotspot is up...
- # ... calculate how old it is in seconds...
- cf_status_path_age=$(echo "$(date +%s) - $(date -r ${STATUS_PATH} +%s)" | bc)
-
- # ... and if it's older than what we allow (REFRESH_TIME), indicate it's time to refresh
- if [[ $cf_status_path_age -gt $REFRESH_TIME ]]; then
- TIME_TO_REFRESH=true
- else
- TIME_TO_REFRESH=false
- fi
- fi
-}
-
-
-function setup_infra {
- # We're in hotspot mode but unsure if we're properly configured for CyberCafe operation
- LOCAL_IP=$(ip -4 addr show dev wlan0 | grep inet | awk '{print $2}' | cut -d '/' -f 1)
- # iptmon tables?
- ## iptmon_tx
- iptables -t mangle -C FORWARD -j iptmon_tx > /dev/null 2>&1
- if [[ $? -ne 0 ]]; then
- echo "Creating table ipmon_tx"
- iptables -t mangle -N iptmon_tx
-
- echo "Adding iptmon_tx to FORWARD chain, 'mangle' table"
- iptables -t mangle -A FORWARD -j iptmon_tx
- fi
-
- ## iptmon_rx
- iptables -t mangle -C FORWARD -j iptmon_rx > /dev/null 2>&1
- if [[ $? -ne 0 ]]; then
- echo "Creating table ipmon_rx"
- iptables -t mangle -N iptmon_rx
-
- echo "Adding iptmon_rx to FORWARD chain, 'mangle' table"
- iptables -t mangle -A FORWARD -j iptmon_rx
- fi
-
- # Default iptables redirect
-
- iptables -t nat -C PREROUTING -j DNAT --to-destination ${LOCAL_IP} > /dev/null 2>&1
- if [[ $? -ne 0 ]]; then
- echo "Creating default redirect rule"
- iptables -t nat -A PREROUTING -p udp -d ${LOCAL_IP} --dport 53 -j RETURN
- iptables -t nat -A PREROUTING -p udp -s 0.0.0.0/32 -d 255.255.255.255/32 --dport 67 -j RETURN
- iptables -t nat -A PREROUTING -j DNAT --to-destination ${LOCAL_IP}
- fi
-
- # Traffic Control
- tc qdisc show dev wlan0 | grep htb > /dev/null
- if [[ $? -ne 0 ]]; then
- echo "Creating tc qdisc HTB queues"
- tc qdisc add dev wlan0 root handle 1: htb default 10
- tc class add dev wlan0 parent 1: classid 1:1 htb rate 15mbps ceil 15mbps
- tc class add dev wlan0 parent 1:1 classid 1:10 htb rate 15mbps ceil 15mbps
- tc class add dev wlan0 parent 1:1 classid 1:20 htb rate 100kbps ceil 100kbps
- fi
-
- # Update the STATUS_PATH file
- rm -f ${STATUS_PATH} && touch ${STATUS_PATH}
-
- # Check captive portal HTTPD server
- if ! pgrep lighttpd > /dev/null; then
- start_captive_webserver
- fi
-}
-
-
-function shutdown_infra {
- printf "%s" "$(date +%T)" && echo ": Shutdown, I suppose"
- echo "Stopping captive portal webserver"
- pkill lighttpd
-
- echo "Putting variables back to default values"
- LOCAL_IP=''
- TIME_TO_REFRESH=false
- HS_STATUS='down'
-
- echo "Removing status path"
- rm -f $STATUS_PATH
-
- echo "Cleaning up iptables"
- iptables -t mangle -D FORWARD -j iptmon_tx
- iptables -t mangle -D FORWARD -j iptmon_rx
- while true; do
- iptables -t mangle -D iptmon_rx 1
- if [[ $? == 1 ]]; then
- break
- fi
- done
- iptables -t mangle -X iptmon_rx
-
- while true; do
- iptables -t mangle -D iptmon_tx 1
- if [[ $? == 1 ]]; then
- break
- fi
- done
- iptables -t mangle -X iptmon_tx
-
- while true; do
- iptables -t nat -D PREROUTING 2
- if [[ $? == 1 ]]; then
- break
- fi
- done
-
- echo "Cleaning up wlan0 qdisc"
- tc qdisc delete dev wlan0 root
-
- echo
- echo "Done."
-}
-
-
-function start_captive_webserver {
- set -o allexport
- source /data/data/com.android.myapplication/files/conf/lighttpd.env
- /data/data/com.android.myapplication/files/bin/lighttpd -f /data/data/com.android.myapplication/files/conf/lighttpd.conf
-}
-
-### START OF PROGRAM EXECUTION ###
-
-# I'm undecided if this is a hack or not. The problem was if this script terminated
-# without cleaning up we'd have a current $STATUS_PATH which, as currently configured,
-# would let the script go from "Hotspot down" to "Everything good."
-rm $STATUS_PATH
-
-while true; do
- check_hotspot_status
-
- if [[ $HS_STATUS == 'up' ]] && ! $TIME_TO_REFRESH; then
- printf "%s" "$(date +%T)" \
- && echo ": Everything is fine, carry on."
- elif [[ $HS_STATUS == 'up' ]] && $TIME_TO_REFRESH; then
- # In hotspot mode, but status file has expired
- printf "%s" "$(date +%T)" \
- && echo ": In hotspot mode, but status file has expired." \
- && echo "Checking CyberCafe infrastructure."
- setup_infra
- elif [[ $HS_STATUS == 'down' ]] && $TIME_TO_REFRESH; then
- # Assume hotspot has recently gone down. Clean up.
- shutdown_infra
- else
- printf "%s" "$(date +%T)" \
- && echo ": Hotspot down. We wait..."
- fi
-
- sleep 60
-done
diff --git a/Backend/CyberCafe_Database.db b/Backend/CyberCafe_Database.db
new file mode 100644
index 0000000..e69de29
diff --git a/Backend/Cybercafe_daemon.sh b/Backend/Cybercafe_daemon.sh
new file mode 100755
index 0000000..976cadc
--- /dev/null
+++ b/Backend/Cybercafe_daemon.sh
@@ -0,0 +1,44 @@
+#!/data/data/com.termux/files/usr/bin/bash
+#Organization: Grey-box
+#Project: Cybercafe
+#File: daemon
+#Description: Main script that is run at start. This script is invoked by Cybercafe_commandLine.sh
+
+###INCLUDES###
+BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+. "$BASE_DIR/cybercafe.conf"
+. "$BASE_DIR/Cybercafe_setupFunctions.sh"
+. "$BASE_DIR/Cybercafe_internetSessionFunctions.sh"
+
+### START OF PROGRAM EXECUTION ###
+while true; do
+ trap 'echo -e "$(date) Error in Cybercafe_daemon.sh: Line ${LINENO}\n" >> error.log' ERR > /dev/null 2>> error.log
+ check_hotspot_status
+
+ #Case 1: Hotspot up and was up in previous check
+ if [[ $HS_STATUS == 'up' ]] && [[ $HS_STATUS_PREV == 'up' ]]; then
+ check_internet_sessions
+
+ #Case 2: Hotspot up and was down in previous check
+ elif [[ $HS_STATUS == 'up' ]] && [[ $HS_STATUS_PREV == 'down' ]]; then
+ setup_infrastructure
+
+ #Case 3: Hotspot down and was up in previous check
+ elif [[ $HS_STATUS == 'down' ]] && [[ $HS_STATUS_PREV == 'up' ]]; then
+ # Assume hotspot has recently gone down. Clean up.
+ clear_internet_sessions
+ shutdown_infrastructure
+
+ #Case 4: Hotspot down and was down in previous check
+ else
+ :
+ fi
+
+ if [[ -f ./shutdown.confirmed ]]; then
+ clear_internet_sessions
+ shutdown_infrastructure
+ exit
+ fi
+
+ sleep 2
+done
diff --git a/Backend/Cybercafe_internetSessionFunctions.sh b/Backend/Cybercafe_internetSessionFunctions.sh
new file mode 100755
index 0000000..ff7487c
--- /dev/null
+++ b/Backend/Cybercafe_internetSessionFunctions.sh
@@ -0,0 +1,338 @@
+#!/usr/bin/env bash
+#Organization: Grey-box
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+source "${SCRIPT_DIR}/test/utils/logging.sh"
+source "${SCRIPT_DIR}/test/utils/net_helpers.sh"
+#Project: Cybercafe
+#File: internetSessionFunctions
+#Description: Contains all internet session functions that are necessary for updating and managing the system and website
+
+# The below STATUS_PATH variable may not be needed in future integration. This seems to be a temporary path used to indicate the hotspot status.
+#STATUS_PATH="/data/data/com.android.myapplication/files/tmp/cybercafe.confirmed"
+
+##FUNCTIONS##
+##Removal Functions##
+function clear_internet_sessions
+#Removes all existing internet sessions stored in the data based when the system shutsdown (invoked by shutdown_infra)
+{
+ trap 'echo -e "$(date) Error in Cybercafe_internetSessionFunctions.sh: Line ${LINENO}\n" >> error.log' ERR > /dev/null 2>> error.log
+ I=0 #iterator value
+ INDEX_LIMIT=$(($(sqlite3 "${DATABASE_PATH}" "SELECT MAX(table_index) FROM internet_sessions;")+1)) > /dev/null 2>> error.log #the maximum number of internet session entries
+ while [ $I -lt $INDEX_LIMIT ]
+ do
+ remove_session $I #remove_session contains all the necessary steps to correctly remove a internet session
+ I=$(( I+1 )) #increment iterator
+ done
+}
+
+function delete_user_iptable_rules
+#subfunction of remove_session used to remove user access
+{
+ # Argument1: $1 -> user_ip
+ # FIX: Changed all rules to use $1 (passed argument) instead of mixing $1 and $USER_IP (global).
+ # This ensures all 5 iptables rules are deleted for the correct IP address.
+ # Previously, the last 2 filter rules used $USER_IP which could be stale/empty.
+
+ {
+ iptables -t mangle -D iptmon_rx -o "${HS_INTERFACE}" -d "${1}" > /dev/null # delete rules since this user is effectively logged out
+ iptables -t mangle -D iptmon_tx -i "${HS_INTERFACE}" -s "${1}" > /dev/null # same as above
+ iptables -t nat -D PREROUTING -p all -s "${1}" -i "${HS_INTERFACE}" -j RETURN > /dev/null
+ iptables -t filter -D FORWARD -p all -s "${1}" -i "${HS_INTERFACE}" -j ACCEPT > /dev/null # Fixed: was ${USER_IP}
+ iptables -t filter -D FORWARD -p all -d "${1}" -o "${HS_INTERFACE}" -j ACCEPT > /dev/null # Fixed: was ${USER_IP}
+ } 2>>error.log
+}
+
+function shutdown_infra {
+ printf "%s" "$(date +%T)" && echo ": Shutdown, I suppose"
+ echo "Stopping captive portal webserver"
+ pkill lighttpd
+
+ ###############
+ #### NEW ######
+ ###############
+ # CAPTURE IP: We need this to delete the specific NAT rules created in setupFunctions
+ local local_ip_cache
+ local_ip_cache=$(ip -4 addr show dev "${HS_INTERFACE}" | grep inet | awk '{print $2}' | cut -d '/' -f 1)
+ ###############
+
+ echo "Putting variables back to default values"
+ export LOCAL_IP=''
+ export TIME_TO_REFRESH=false
+ export HS_STATUS='down'
+
+ echo "Removing status path"
+ #rm -f $STATUS_PATH -> This is not going to be needed for current implementation of shutdown_infra (Unique to Chris' implementation)
+
+ echo "Cleaning up iptables"
+ #iptables -t mangle -D FORWARD -j iptmon_tx
+ #iptables -t mangle -D FORWARD -j iptmon_rx
+ iptables -t mangle -D FORWARD -i "${HS_INTERFACE}" -j iptmon_tx #changed to POSTROUTING to match John's setup
+ iptables -t mangle -D POSTROUTING -o "${HS_INTERFACE}" -j iptmon_rx #changed to PREROUTING to match John's setup
+
+ # Remove all rules in iptmon_rx
+ while true; do
+ iptables -t mangle -D iptmon_rx 1
+ if [[ $? == 1 ]]; then
+ break
+ fi
+ done
+ iptables -t mangle -X iptmon_rx
+
+ # Remove all rules in iptmon_tx
+ while true; do
+ iptables -t mangle -D iptmon_tx 1
+ if [[ $? == 1 ]]; then
+ break
+ fi
+ done
+ iptables -t mangle -X iptmon_tx
+
+ # Remove all PREROUTING rules
+ while true; do
+ iptables -t nat -D PREROUTING 2
+ if [[ $? == 1 ]]; then
+ break
+ fi
+ done
+
+ # Cleanup specific NAT and Filter rules created by setup_infrastructure
+ iptables -t nat -D PREROUTING -p tcp -i "${HS_INTERFACE}" -j DNAT --to-destination "${local_ip_cache}:80" 2>/dev/null
+ iptables -t filter -D FORWARD -p all -i "${HS_INTERFACE}" -j DROP 2>/dev/null
+ iptables -t filter -D FORWARD -p all -o "${HS_INTERFACE}" ! -s "${local_ip_cache}" -j DROP 2>/dev/null
+
+ echo "Cleaning up wlan1 qdisc"
+ tc qdisc delete dev "${HS_INTERFACE}" root
+
+ echo
+ echo "Done."
+}
+
+
+function remove_session
+#cleanly remove a given session and save all necessary data
+{
+ trap 'echo -e "$(date) Error in Cybercafe_internetSessionFunctions.sh: Line ${LINENO}\n" >> error.log' ERR > /dev/null 2>> error.log
+ # Argument1: $1 -> internet_session table index (i.e. what internet session are we saving/deleting)
+ RESPONSE=$(sqlite3 "${DATABASE_PATH}" "SELECT * FROM internet_sessions WHERE table_index=${1}") > /dev/null 2>> error.log
+ if [[ "$RESPONSE" == '' ]]; then
+ return 1 #session doesn't exists
+ else
+
+ #1. Get all the data
+ #Necessary data from internet_session row
+ {
+ RESPONSE=$(sqlite3 "${DATABASE_PATH}" "SELECT * FROM internet_sessions WHERE table_index=${1}") > /dev/null 2>> error.log
+ DATETIME=$(date '+%Y-%m-%d %H:%M:%S') > /dev/null
+ SESSION_TX=$(echo "$RESPONSE" | cut -f 5 -d '|') > /dev/null
+ SESSION_RX=$(echo "$RESPONSE" | cut -f 6 -d '|') > /dev/null
+ USER_ID=$(echo "$RESPONSE" | cut -f 2 -d '|') > /dev/null
+ USER_IP=$(echo "$RESPONSE" | cut -f 4 -d '|') > /dev/null
+ } 2>>error.log
+ #
+ #Calculate what session number this will be saved under in user_data_usage for this user
+ RESPONSE=$(sqlite3 "${DATABASE_PATH}" "SELECT MAX(session_number) FROM user_data_usage WHERE user_id='${USER_ID}'") > /dev/null 2>> error.log
+ if [[ "$RESPONSE" != '' ]]; then
+ #**Note: php creates the first entry for any given session**
+ SESSION_NUMBER=$((RESPONSE))
+ #Calculate what entry number this will be saved under in user_data_usage for this user on this session
+ RESPONSE=$(sqlite3 "${DATABASE_PATH}" "SELECT MAX(session_entry_index) FROM user_data_usage WHERE user_id='${USER_ID}' AND session_number='${SESSION_NUMBER}'") > /dev/null 2>> error.log
+ ENTRY_INDEX=$((RESPONSE + 1))
+ RESPONSE=$(sqlite3 "${DATABASE_PATH}" "SELECT SUM(interval_bytes_tx) FROM user_data_usage WHERE user_id='${USER_ID}' AND session_number='${SESSION_NUMBER}'") > /dev/null 2>> error.log
+ INTERVAL_TX=$((SESSION_TX - RESPONSE)) #The next interval entry is [total usage for this session - sum of previous entries associated with this user's session]
+ RESPONSE=$(sqlite3 "${DATABASE_PATH}" "SELECT SUM(interval_bytes_rx) FROM user_data_usage WHERE user_id='${USER_ID}' AND session_number='${SESSION_NUMBER}'") > /dev/null 2>> error.log
+ INTERVAL_RX=$((SESSION_RX - RESPONSE)) #same as above
+
+ #2. Create entry
+ sqlite3 "${DATABASE_PATH}" "INSERT INTO user_data_usage (user_id,session_number,session_entry_index,entry_datetime,interval_bytes_tx,interval_bytes_rx) VALUES ('${USER_ID}','${SESSION_NUMBER}','${ENTRY_INDEX}','${DATETIME}',${INTERVAL_TX},${INTERVAL_RX})" > /dev/null 2>> error.log
+ fi
+ #3. Delete internet session
+ sqlite3 "${DATABASE_PATH}" "DELETE FROM internet_sessions WHERE table_index=${1}" > /dev/null 2>> error.log # delete entry from internet_sessions so that the session doesn't exist anymore
+ #4. Delete iptable rules
+ delete_user_iptable_rules "$USER_IP"
+ fi
+}
+
+##Check Internet Sessions Functions##
+function check_against_usage_limits
+#subfunction of check_internet_sessions used to determine if user is 'over limits' (returns a boolean)
+{
+ # Argument1: $1 -> internet_session table index
+ #Note: as this is a subfunction of check_internet_sessions USER_ID is already defined correctly
+ USER_LEVEL=$(sqlite3 "${DATABASE_PATH}" "SELECT user_level FROM users WHERE user_id='${USER_ID}'") > /dev/null 2>> error.log
+ STATUS=$(sqlite3 "${DATABASE_PATH}" "SELECT status FROM users WHERE user_id='${USER_ID}'") > /dev/null 2>> error.log
+ if [[ "$STATUS" == 'DISABLED' ]]; then #if the user is disabled then skip all other checks and return 1 (i.e. over limits)
+ return 1
+ fi
+ if [[ "$USER_LEVEL" == '0' ]]; then #if the user is an admin then skip all other checks and return 0 (i.e. not over limits)
+ return 0 #Note due to the ordering of this and the above if statement admins are able to be disabled but they can re-enable themseleves if needed
+ else
+ #1. Sum of usage today, this week, this month
+ {
+ TOTAL_TX_DAY=$(($(sqlite3 "${DATABASE_PATH}" "SELECT SUM(interval_bytes_tx) FROM user_data_usage WHERE user_id='${USER_ID}' AND entry_datetime>=datetime(datetime(),'localtime','-1 days')"))) > /dev/null
+ TOTAL_RX_DAY=$(($(sqlite3 "${DATABASE_PATH}" "SELECT SUM(interval_bytes_rx) FROM user_data_usage WHERE user_id='${USER_ID}' AND entry_datetime>=datetime(datetime(),'localtime','-1 days')"))) > /dev/null
+ TOTAL_TX_WEEK=$(($(sqlite3 "${DATABASE_PATH}" "SELECT SUM(interval_bytes_tx) FROM user_data_usage WHERE user_id='${USER_ID}' AND entry_datetime>=datetime(datetime(),'localtime','-7 days')"))) > /dev/null
+ TOTAL_RX_WEEK=$(($(sqlite3 "${DATABASE_PATH}" "SELECT SUM(interval_bytes_rx) FROM user_data_usage WHERE user_id='${USER_ID}' AND entry_datetime>=datetime(datetime(),'localtime','-7 days')"))) > /dev/null
+ TOTAL_TX_MONTH=$(($(sqlite3 "${DATABASE_PATH}" "SELECT SUM(interval_bytes_tx) FROM user_data_usage WHERE user_id='${USER_ID}' AND entry_datetime>=datetime(datetime(),'localtime','-30 days')"))) > /dev/null
+ TOTAL_RX_MONTH=$(($(sqlite3 "${DATABASE_PATH}" "SELECT SUM(interval_bytes_rx) FROM user_data_usage WHERE user_id='${USER_ID}' AND entry_datetime>=datetime(datetime(),'localtime','-30 days')"))) > /dev/null
+ } 2>> error.log
+ if [[ "$TOTAL_TX_DAY" == '' ]]; then
+ TOTAL_TX_DAY=0
+ fi
+ if [[ "$TOTAL_RX_DAY" == '' ]]; then
+ TOTAL_RX_DAY=0
+ fi
+ if [[ "$TOTAL_TX_WEEK" == '' ]]; then
+ TOTAL_TX_WEEK=0
+ fi
+ if [[ "$TOTAL_RX_WEEK" == '' ]]; then
+ TOTAL_RX_WEEK=0
+ fi
+ if [[ "$TOTAL_TX_MONTH" == '' ]]; then
+ TOTAL_TX_MONTH=0
+ fi
+ if [[ "$TOTAL_RX_MONTH" == '' ]]; then
+ TOTAL_RX_MONTH=0
+ fi
+ #2. Check the lane of the user
+ LANE_ID=$(sqlite3 "${DATABASE_PATH}" "SELECT lane_id FROM users WHERE user_id='${USER_ID}'") > /dev/null 2>> error.log
+ if [[ "$LANE_ID" == '' ]]; then
+ LANE_ID=0
+ fi
+ #3. Do mathematical comparisions
+ {
+ RESPONSE2=$(sqlite3 "${DATABASE_PATH}" "SELECT bytelimit_daily,bytelimit_weekly,bytelimit_monthly FROM data_lanes WHERE lane_id=${LANE_ID}") > /dev/null
+ LANE_DAILY_LIMIT=$(($(echo "${RESPONSE2}" | cut -f 1 -d '|'))) > /dev/null
+ LANE_WEEKLY_LIMIT=$(($(echo "${RESPONSE2}" | cut -f 2 -d '|'))) > /dev/null
+ LANE_MONTHLY_LIMIT=$(($(echo "${RESPONSE2}" | cut -f 3 -d '|'))) > /dev/null
+ } 2>> error.log
+ if ((TOTAL_TX_DAY + TOTAL_RX_DAY > LANE_DAILY_LIMIT)); then
+ return 1
+ elif ((TOTAL_TX_WEEK + TOTAL_RX_WEEK > LANE_WEEKLY_LIMIT)); then
+ return 1
+ elif ((TOTAL_TX_MONTH + TOTAL_RX_MONTH > LANE_MONTHLY_LIMIT)); then
+ return 1
+ else
+ return 0
+ fi
+ #4. Return boolean value 1 (over limits) 0 (not over limits)
+ fi
+}
+
+function check_internet_sessions
+#main function used to periodically update internet session data in the sqlite database and remove sessions based certain criteria.
+{
+ trap 'echo -e "$(date) Error in Cybercafe_internetSessionFunctions.sh: Line ${LINENO}\n" >> error.log' ERR > /dev/null 2>> error.log
+
+ I=0 #iterator value
+ INDEX_LIMIT=$(($(sqlite3 "${DATABASE_PATH}" "SELECT MAX(table_index) FROM internet_sessions")+1)) > /dev/null 2>> error.log #the maximum number of internet session entries
+ while [ "$I" -lt "$INDEX_LIMIT" ]
+ do
+ # 1. Get user session data from sqlite database if it doesn't exist then skip
+ RESPONSE=$(sqlite3 "${DATABASE_PATH}" "SELECT * FROM internet_sessions WHERE table_index=${I}") > /dev/null 2>> error.log
+ if [[ "$RESPONSE" == '' ]]; then
+ I=$((I + 1))
+ continue
+ fi
+
+ # 2. If deletion pending bit is 1 then skip to remove_session
+ PENDING_DELETION=$(echo "$RESPONSE" | cut -f 10 -d '|') > /dev/null 2>> error.log
+ if [[ "$PENDING_DELETION" == '1' ]]; then
+ remove_session $I
+ I=$((I + 1))
+ continue
+ fi
+
+ # 3. Check that a rule exists to record data usage (iptables)
+ USER_IP=$(echo "$RESPONSE" | cut -f 4 -d '|') > /dev/null 2>> error.log
+ iptables -t mangle -C iptmon_rx -o "${HS_INTERFACE}" -d "${USER_IP}" > /dev/null 2>> error.log
+ if [[ $? -eq 1 ]]; then
+ #rule doesn't exist but should
+ iptables -t mangle -A iptmon_rx -o "${HS_INTERFACE}" -d "${USER_IP}" > /dev/null 2>> error.log
+ iptables -t mangle -A iptmon_tx -i "${HS_INTERFACE}" -s "${USER_IP}" > /dev/null 2>> error.log
+ fi
+
+ # 4. Update the current session tx and rx based on the associated iptables rule (for this user) on the database
+ SESSION_ACCESS=$(echo "$RESPONSE" | cut -f 7 -d '|')
+ # shellcheck disable=SC2129
+ {
+ SESSION_TX=$(($(iptables -t mangle -L iptmon_tx -vxn | grep "${USER_IP}" | awk '{print $2}')))
+ SESSION_RX=$(($(iptables -t mangle -L iptmon_rx -vxn | grep "${USER_IP}" | awk '{print $2}')))
+ sqlite3 "${DATABASE_PATH}" "UPDATE internet_sessions SET session_tx='${SESSION_TX}',session_rx='${SESSION_RX}' WHERE table_index=${I}"
+ } > /dev/null 2>> error.log
+
+ #5. Send data entry for this check to user_data_usage
+ #Necessary data from internet_session row
+ RESPONSE=$(sqlite3 "${DATABASE_PATH}" "SELECT * FROM internet_sessions WHERE table_index=${I}") > /dev/null 2>> error.log
+ DATETIME=$(date '+%Y-%m-%d %H:%M:%S') > /dev/null 2>> error.log
+ SESSION_TX=$(echo "$RESPONSE" | cut -f 5 -d '|') > /dev/null 2>> error.log
+ SESSION_RX=$(echo "$RESPONSE" | cut -f 6 -d '|') > /dev/null 2>> error.log
+ USER_ID=$(echo "$RESPONSE" | cut -f 2 -d '|') > /dev/null 2>> error.log
+ USER_IP=$(echo "$RESPONSE" | cut -f 4 -d '|') > /dev/null 2>> error.log
+ #
+ #Calculate what session number this will be saved under in user_data_usage for this user
+ SESSION_NUMBER=$(sqlite3 "${DATABASE_PATH}" "SELECT MAX(session_number) FROM user_data_usage WHERE user_id='${USER_ID}'") > /dev/null 2>> error.log
+ if [[ "$SESSION_NUMBER" != '' ]]; then
+ #**Note: php creates the first entry for any given session**
+ #Calculate what entry number this will be saved under in user_data_usage for this user on this session
+ RESPONSE=$(sqlite3 "${DATABASE_PATH}" "SELECT MAX(session_entry_index) FROM user_data_usage WHERE user_id='${USER_ID}' AND session_number=${SESSION_NUMBER}") > /dev/null 2>> error.log
+ ENTRY_INDEX=$((RESPONSE + 1))
+ RESPONSE=$(sqlite3 "${DATABASE_PATH}" "SELECT SUM(interval_bytes_tx) FROM user_data_usage WHERE user_id='${USER_ID}' AND session_number='${SESSION_NUMBER}'") > /dev/null 2>> error.log
+ INTERVAL_TX=$((SESSION_TX - RESPONSE)) #The next interval entry is [total usage for this session - sum of previous entries associated with this user's session]
+ RESPONSE=$(sqlite3 "${DATABASE_PATH}" "SELECT SUM(interval_bytes_rx) FROM user_data_usage WHERE user_id='${USER_ID}' AND session_number='${SESSION_NUMBER}'") > /dev/null 2>> error.log
+ INTERVAL_RX=$((SESSION_RX - RESPONSE)) #same as above
+ #create entry for this check in database
+ sqlite3 "${DATABASE_PATH}" "INSERT INTO user_data_usage (user_id,session_number,session_entry_index,entry_datetime,interval_bytes_tx,interval_bytes_rx) VALUES ('${USER_ID}','${SESSION_NUMBER}','${ENTRY_INDEX}','${DATETIME}','${INTERVAL_TX}','${INTERVAL_RX}')" > /dev/null 2>> error.log
+ fi
+
+ # 6. update 'datetime_sinceLastRequest' based on metrics from user_data_usage table
+ if [[ $ENTRY_INDEX -ne 0 && $USER_ID != '' ]]; then #no need to check on the first entry for a session
+ RESPONSE=$(sqlite3 "${DATABASE_PATH}" "SELECT MAX(entry_datetime) FROM user_data_usage WHERE user_id='${USER_ID}' AND interval_bytes_tx+interval_bytes_rx!=0 AND (SELECT datetime_sinceLastRequest FROM internet_sessions WHERE user_id=${USER_ID}) /dev/null 2>> error.log
+ if [[ "$RESPONSE" != '' ]]; then
+ sqlite3 "${DATABASE_PATH}" "UPDATE internet_sessions SET datetime_sinceLastRequest='${RESPONSE}' WHERE table_index=${I}" > /dev/null 2>> error.log
+ else
+ # 7. If session age or session idle time becomes to great then save that sessions data and delete the session
+ #Note: if the previous if statment triggers then we can assume it isn't idle or aged out because it was just updated
+ RESPONSE=$(sqlite3 "${DATABASE_PATH}" "SELECT strftime('%s','now') - strftime('%s',(SELECT datetime_created FROM internet_sessions WHERE table_index=${I}))")
+ RESPONSE2=$(sqlite3 "${DATABASE_PATH}" "SELECT strftime('%s','now') - strftime('%s',(SELECT datetime_sinceLastRequest FROM internet_sessions WHERE table_index=${I}))")
+ if [[ "$RESPONSE" != '' && "$RESPONSE2" != '' ]]; then
+ SESSION_AGE=$RESPONSE
+ SESSION_IDLETIME=$RESPONSE2
+ if [[ "$SESSION_AGE" -gt "$SESSION_MAX_AGE" ]] || [[ "$SESSION_IDLETIME" -gt "$SESSION_MAX_IDLETIME" ]]; then #if session is older than 12 hours or has been idle for more than 1hr then delete session
+ remove_session $I #session has aged or idled out
+ fi
+ fi
+ fi
+ fi
+
+ #8. Calculate current usage and determine if given user is outside their limits
+ check_against_usage_limits $I
+ if [[ $? -eq 1 ]]; then #user is over limits, so do necessary updates
+ sqlite3 "${DATABASE_PATH}" "UPDATE internet_sessions SET session_access='0' WHERE table_index=${I}" > /dev/null 2>> error.log
+ else
+ sqlite3 "${DATABASE_PATH}" "UPDATE internet_sessions SET session_access='1' WHERE table_index=${I}" > /dev/null 2>> error.log
+ fi
+
+ #9. Check user access entry in the database and add or remove user exception rules if they have access
+ SESSION_ACCESS=$(($(sqlite3 "${DATABASE_PATH}" "SELECT session_access FROM internet_sessions WHERE user_id=${USER_ID}")))
+ iptables -t nat -C PREROUTING -s "${USER_IP}" -i "${HS_INTERFACE}" -j RETURN > /dev/null 2>> error.log
+ if [[ $? -eq 0 && "$SESSION_ACCESS" == '0' ]]; then #rules exists but shouldn't
+ {
+ iptables -t nat -D PREROUTING -p all -s "${USER_IP}" -i "${HS_INTERFACE}" -j RETURN > /dev/null
+ iptables -t filter -D FORWARD -p all -s "${USER_IP}" -i "${HS_INTERFACE}" -j ACCEPT > /dev/null
+ iptables -t filter -D FORWARD -p all -d "${USER_IP}" -o "${HS_INTERFACE}" -j ACCEPT > /dev/null
+ } 2>> error.log
+ fi
+ iptables -t nat -C PREROUTING -s "${USER_IP}" -i "${HS_INTERFACE}" -j RETURN > /dev/null 2>> error.log
+ if [[ $? -eq 1 && "$SESSION_ACCESS" == '1' ]]; then #rule doesn't exist but should
+ {
+ iptables -t nat -I PREROUTING 1 -p all -s "${USER_IP}" -i "${HS_INTERFACE}" -j RETURN > /dev/null
+ iptables -t filter -I FORWARD 1 -p all -s "${USER_IP}" -i "${HS_INTERFACE}" -j ACCEPT > /dev/null #this rule will allow requests outside of the network for this user
+ iptables -t filter -I FORWARD 1 -p all -d "${USER_IP}" -o "${HS_INTERFACE}" -j ACCEPT > /dev/null
+ } 2>> error.log
+ fi
+
+
+ I=$((I + 1)) #increment iterator
+ done
+}
diff --git a/Backend/Cybercafe_orchestrator.sh b/Backend/Cybercafe_orchestrator.sh
new file mode 100644
index 0000000..8d1b718
--- /dev/null
+++ b/Backend/Cybercafe_orchestrator.sh
@@ -0,0 +1,286 @@
+#!/usr/bin/env bash
+
+# Organization: Grey-box
+# Project: Cybercafe
+# File: Cybercafe_orchestrator.sh
+# Description: One-command lifecycle runner for Cybercafe.
+# Wraps cybercafe CLI: build → run → status → shutdown
+# Target: T95 device via Termux (root required)
+# Usage: bash Cybercafe_orchestrator.sh [--rebuild] [COMMAND]
+
+# Prerequisites:
+# - ADB connected to T95 (adb connect :5555)
+# - Termux installed on T95 with bash, make, sqlite3
+# - Root access confirmed (whoami returns 'root')
+# - cybercafe.conf present in the same directory as this script
+# - preflight_t95.sh passed before running this script
+
+# Commands:
+# all Full lifecycle: preflight → build → run → status (default)
+# build Compile cybercafe.sh into the runnable cybercafe binary
+# start Start the Cybercafe infrastructure (daemon + hotspot check)
+# status Print current running state of the Cybercafe daemon
+# shutdown Send graceful shutdown signal and wait for clean stop
+#
+# Flags:
+# --rebuild Force a fresh build even if the binary already exists
+#
+# Examples:
+# bash cybercafe_orchestrator.sh # runs full lifecycle
+# bash cybercafe_orchestrator.sh build # build only
+# bash cybercafe_orchestrator.sh start # start only
+# bash cybercafe_orchestrator.sh --rebuild all # force rebuild then full run
+# bash cybercafe_orchestrator.sh shutdown # graceful shutdown
+#
+# Logs:
+# All output is written to orchestrator.log in the same directory.
+# For Cybercafe-specific errors, see error.log (generated by cybercafe.sh).
+#
+# Recovery:
+# If the T95 loses network access during a run:
+# 1. Reboot the T95
+# 2. Reconnect ADB: adb connect :5555
+# 3. Run: bash Backend/test/integration/restore_t95_baseline.sh
+# 4. Re-run: bash Backend/test/integration/preflight_t95.sh
+
+# Exit immediately if any command fails, treat unset variables as errors,
+# and propagate errors through pipes. This prevents silent failures
+# from cascading into later stages.
+set -euo pipefail
+
+# Resolving the absolute path of the directory this scripts lives.
+# Ensures all file references work regardless of where script is called from.
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+UTIL_PATH="/data/data/com.termux/files/usr/bin"
+
+# Path to the compiled cybercafe binary produced by 'make all'.
+# Main CLI (Command Line Interface) all lifecycle commands are routed through.
+BINARY="$SCRIPT_DIR/cybercafe"
+
+# Path to the cybercafe config. file
+# Environmental variables contained (HS_INTERFACE and DATABASE_PATH)
+# required by the backend scripts and must exist before running.
+CONF="$SCRIPT_DIR/cybercafe.conf"
+
+# Path to the log file where every stage logs automatically.
+# Use for Validation evidence and debugging.
+LOG="$SCRIPT_DIR/orchestrator.log"
+
+# These functions standardize output format across all stages.
+# All messages are timestamped and written to both terminal and log file.
+
+# INFO — normal progress messages
+log() { echo "[$(date '+%H:%M:%S')] [INFO] $*" | tee -a "$LOG"; }
+
+# WARN — non-fatal issues that should be noted but won't stop execution
+warn() { echo "[$(date '+%H:%M:%S')] [WARN] $*" | tee -a "$LOG"; }
+
+# ERROR — fatal issues that immediately halt the script with exit code 1
+error() { echo "[$(date '+%H:%M:%S')] [ERROR] $*" | tee -a "$LOG" >&2; exit 1; }
+
+# Preflight validates the environment before any lifecycle stage runs.
+# Acted as a safety gate. If anything here fails, the system is not in state for
+# orchestrator running safely.
+
+# Checks performed:
+# - cybercafe.conf exists (required by cybercafe.sh at runtime)
+# - make is available (required for the build stage)
+# - sqlite3 is available (required for user/session operations)
+# - bash is available (required to execute all lifecycle scripts)
+#
+# If any check fails, the script exits with an error before
+# any changes are made to the device.
+preflight() {
+ log "Running preflight checks..."
+
+ # Verify config file exists. Needed at startup for cybercafe.sh and will fail silently without it
+ [[ -f "$CONF" ]] || error "cybercafe.conf not found in $SCRIPT_DIR"
+
+ # Confirm make is available — needed to compile cybercafe.sh into binary
+ command -v make || command -v "$UTIL_PATH/make" &>/dev/null || error "make is not installed"
+
+ # Confirm sqlite3 is available — needed for list/user operations
+ command -v sqlite3 || command -v "$UTIL_PATH/sqlite3" &>/dev/null || error "sqlite3 is not installed"
+
+ # Confirm bash is available — all scripts in the project require bash
+ command -v bash || command -v "$UTIL_PATH/bash" &>/dev/null || error "bash is not installed"
+
+ log "Preflight passed."
+}
+
+# Build runs 'make all' to compile cybercafe.sh into the executable binary.
+# What does make all do (from Makefile):
+# cp cybercafe.sh cybercafe - copies the script
+# chmod +x cybercafe - makes it executable
+
+# Idemopotent: skips the build if binary already exists and --rebuild was not passed.
+# Preventing unnecessary rebuilds on repeated runs of orchestrator.
+run_build() {
+ log "Stage: BUILD"
+ cd "$SCRIPT_DIR"
+
+ # Check if binary already exists and --rebuild was not requested.
+ # Makefile only copies and chmods. Skipping an existing build is safe and
+ # no compilation that could go stale.
+ if [[ -x "$BINARY" ]] && [[ "${REBUILD:-false}" != "true" ]]; then
+ warn "Binary already exists at $BINARY. Use --rebuild to force a fresh build."
+ else
+ # Run Makefile build target producing the cybercafe binary
+ "$UTIL_PATH/make" all || error "make all failed - check Makefile in $SCRIPT_DIR"
+ log "Build complete. Binary ready at $BINARY"
+ fi
+}
+
+# Run/Start will start the infrastructure by calling 'cybercafe run'.
+# What 'cybercafe run' does internally:
+# - Checks if the daemon is already running (idempotent guard)
+# - Starts Cybercafe_daemon.sh as a background nohup process
+# - Warns if the hotspot interface (HS_INTERFACE) is not up
+
+# cybercafe.sh already guards against duplicate starts
+# via a ps check, calling this stage twice is safe.
+run_start() {
+ log "Stage: RUN"
+ "$BINARY" run || error "cybercafe run failed. Check error.log for details."
+ log "Infrastructure start command issued."
+}
+
+# Status checks the current state of the Cybercafe daemon and hotspot.
+# What 'cybercafe status' returns:
+# - Timestamp
+# - Status: Running / Stopped
+# - Process info (user, PID, start time) if running
+# - Hotspot interface details (ip add show dev $HS_INTERFACE)
+
+# This stage also acts as a post-start validation:
+# if the daemon is not running after 'run', it logs a warning
+# so the operator knows to investigate before proceeding.
+run_status() {
+ log "Stage: STATUS"
+
+ # Capture full status output. Write to the terminal and log file
+ STATUS_OUTPUT=$("$BINARY" status 2>&1) || true
+ echo "$STATUS_OUTPUT" | tee -a "$LOG"
+
+ # Check for the expected "Status: Running" string.
+ # If missing, warn — don't hard error, as the daemon may take
+ # a moment to fully start on the T95.
+ if echo "$STATUS_OUTPUT" | grep -q "Status: Running"; then
+ log "Confirmed: Cybercafe daemon is running."
+ else
+ warn "Daemon may not be running yet — review status output above."
+ fi
+
+ # --- Full Functionality Checks ---
+ # Query all core data models and log their current state.
+ # These run regardless of daemon status so we always capture
+ # what is in the database at time of test. Failures are warnings
+ # only — they do not halt the lifecycle.
+
+ log "--- Listing Sessions ---"
+ "$BINARY" list sessions 2>&1 | tee -a "$LOG" || warn "list sessions failed"
+
+ log "--- Listing Users ---"
+ "$BINARY" list users 2>&1 | tee -a "$LOG" || warn "list users failed"
+
+ log "--- Listing Lanes ---"
+ "$BINARY" list lanes 2>&1 | tee -a "$LOG" || warn "list lanes failed"
+
+ log "--- Listing Rules ---"
+ "$BINARY" list rules 2>&1 | tee -a "$LOG" || warn "list rules failed"
+
+ log "Full functionality check complete. Review orchestrator.log for details."
+}
+
+# Shutdown sends graceful shutdown signal to the daemon.
+# What 'cybercafe shutdown' does internally:
+# - Creates a 'shutdown.confirmed' lockfile as the stop signal
+# - Polls every second until the daemon process exits
+# - Removes the lockfile once the daemon has stopped cleanly
+
+# Using || true here because if the daemon is already stopped,
+# cybercafe shutdown exits non-zero — that is not a failure
+# state for the orchestrator.
+run_shutdown() {
+ log "Stage: SHUTDOWN"
+ "$BINARY" shutdown || true
+ log "Shutdown signal sent. Daemon has stopped."
+}
+
+# Full Lifecycle
+# Runs all stages in sequence as a single command.
+# Primary deliverable. One command to execute full Cybercafe lifecycle on T95.
+# Order:
+# 1. preflight — validate environment is ready
+# 2. build — compile cybercafe binary via make
+# 3. start — launch daemon + hotspot
+# 4. status — confirm running state
+
+# Note: shutdown is intentionally not included in 'all'.
+# The system is meant to stay running after 'all' completes.
+# Call shutdown explicitly when you want to stop the system.
+run_all() {
+ preflight
+ run_build
+ run_start
+ run_status
+
+ log "Cybercafe is running."
+ log "To stop: /data/data/com.termux/files/usr/bin/bash cybercafe_orchestrator.sh shutdown"
+ log "Logs: $LOG"
+}
+
+# Flag Parsing
+# Separates flags (--rebuild) from positional arguments (commands).
+# This allows flags to be passed in any position, e.g.:
+# bash cybercafe_orchestrator.sh --rebuild all
+# bash cybercafe_orchestrator.sh all --rebuild (also valid)
+
+REBUILD=false
+POSITIONAL=()
+for arg in "$@"; do
+ case $arg in
+ --rebuild) REBUILD=true ;; # set rebuild flag
+ *) POSITIONAL+=("$arg") ;; # collect all other args
+ esac
+done
+
+# Re-set positional parameters to the non-flag arguments
+set -- "${POSITIONAL[@]:-}"
+
+# Usage prints when an unrecognized command is passed or help is requested.
+usage() {
+ echo ""
+ echo "Usage: bash cybercafe_orchestrator.sh [--rebuild] [COMMAND]"
+ echo ""
+ echo "Commands:"
+ echo " all Full lifecycle: preflight → build → run → status (default)"
+ echo " build Compile cybercafe.sh into the runnable cybercafe binary"
+ echo " start Start the Cybercafe infrastructure"
+ echo " status Show current running status of the daemon"
+ echo " shutdown Send graceful shutdown signal and wait for clean stop"
+ echo ""
+ echo "Flags:"
+ echo " --rebuild Force a fresh build even if the binary already exists"
+ echo ""
+ echo "Examples:"
+ echo " bash cybercafe_orchestrator.sh # full lifecycle"
+ echo " bash cybercafe_orchestrator.sh build # build only"
+ echo " bash cybercafe_orchestrator.sh --rebuild all # force rebuild + run"
+ echo " bash cybercafe_orchestrator.sh shutdown # graceful stop"
+ echo ""
+ exit 1
+}
+
+# Main Entry Point
+# Routes the command argument to the desired stage function.
+# Defaults to 'all' if no command is provided, so it can run the full lifecycle as expected
+case "${1:-all}" in
+ all) run_all ;;
+ build) preflight && run_build ;;
+ start) preflight && run_start ;;
+ status) run_status ;;
+ shutdown) run_shutdown ;;
+ help|--help|-h) usage ;;
+ *) echo "Unknown command: ${1:-}"; usage ;;
+esac
diff --git a/Backend/Cybercafe_setupFunctions.sh b/Backend/Cybercafe_setupFunctions.sh
new file mode 100755
index 0000000..0c3e453
--- /dev/null
+++ b/Backend/Cybercafe_setupFunctions.sh
@@ -0,0 +1,338 @@
+#!/usr/bin/env bash
+#Organization: Grey-box
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+source "${SCRIPT_DIR}/test/utils/logging.sh"
+source "${SCRIPT_DIR}/test/utils/net_helpers.sh"
+#Project: Cybercafe
+#File: setupFunctions
+#Description: Contains all the bash functions necessary to setup the Cybercafe architecture. This script is included by the daemon script which actually calls the setup.
+
+##VARIABLES##
+LOCAL_IP=''
+# The status of the hotspot interface on the device
+HS_STATUS='down'
+HS_STATUS_PREV='down'
+
+# Between increments of this value we will not perform a more expensive check of
+# the status of the CyberCafe infrastructure. Once passed, we perform the checks
+# then reset the counter. In seconds.
+REFRESH_TIME=3600
+
+# Path to the file used to indicate the time we last did an expensive check of
+# the hotspot/CyberCafe infrastructure status. If the file doesnt exist, its
+# assumed we're not ready to act as a CyberCafe router.
+# Android Test
+# STATUS_PATH="/data/data/com.android.myapplication/files/tmp/cybercafe.confirmed"
+
+# Windows Test
+STATUS_PATH="./tmp/cybercafe.confirmed"
+
+# When 'true', we perform the expensive CyberCafe infrastructure check.
+TIME_TO_REFRESH=false
+
+##FUNCTIONS##
+function check_hotspot_status
+#Checks whether the device's hotspot feature has been turned on or off
+{
+ # Save previous status from last check so we capture previous state for Daemon to know change (From John's)
+ HS_STATUS_PREV="$HS_STATUS"
+
+ # From Chris's with better probing method to grab a specific IP pattern for the hotspot
+ ip add show dev wlan0 | grep 192\.168\.43\. > /dev/null 2>> error.log # Does it appear the hotspot is active?
+ wlan_ip_status=$?
+
+ # Set current status of the infrastructrure
+ if [[ $wlan_ip_status -eq 0 ]]; then
+ HS_STATUS='up'
+
+ #To detect any drift/small crashes or glitches when hotspot goes down, but our state var. is still 'up'.
+ elif [[ $wlan_ip_status -ne 0 && $HS_STATUS == 'up' ]]; then
+ # Assume hotspot has recently gone done.
+ HS_STATUS='down'
+ TIME_TO_REFRESH=true
+ else
+ HS_STATUS='down'
+ TIME_TO_REFRESH=false
+ fi
+
+ # Timestamp Guard (From Chris's) to indicate refreshing if the file is not found or stale
+ # Stale: If the File's age > REFRESH_TIME, then set TIME_TO_REFRESH=true
+
+ # HS_STATUS is down: TIME_TO_REFRESH is false, HS_STATUS=down
+ # HS_STATUS is up, but the STATUS_PATH does not exist: Indicate refresh
+ # HS_STATUS is up, and STATUS_PATH exists: Check STATUS_PATH age
+ # STATUS_PATH age > REFRESH_TIME: Indicate refresh
+ # STATUS_PATH age < REFRESH_TIME: Indicate no refresh
+
+
+ if [[ "$HS_STATUS" == 'up' && ! -e "$STATUS_PATH" ]]; then
+ TIME_TO_REFRESH=true
+
+ elif [[ "$HS_STATUS" == 'up' && -e "$STATUS_PATH" ]]; then
+ # If the CyberCafe status file exists and the hotspot is up
+ # calculate how old it is (in s)
+ cf_status_path_age=$(( $(date +%s) - $(date -r "${STATUS_PATH}" +%s) ))
+
+ # if it's older than REFRESH_TIME, indicate time to refresh
+ if (( cf_status_path_age > REFRESH_TIME )); then
+ TIME_TO_REFRESH=true
+ else
+ TIME_TO_REFRESH=false
+ fi
+ else
+ # Hotspot is down, no need for refresh to save resources
+ export TIME_TO_REFRESH=false
+ fi
+}
+
+
+
+
+function setup_infrastructure
+#Setup any necessary infrastructure for the Cybercafe system
+{
+ {
+ #trap will catch any errors that occur and write the line number to the error.log file
+ trap 'echo -e "$(date '+%Y-%m-%dT%H:%M:%S%z') Error in Cybercafe_setupFunctions.sh: Line ${LINENO}\n" >> error.log' ERR
+ #This will grab the ip address of the given hotspot interface so that it can be used for setup
+ LOCAL_IP=$(ifconfig "$HS_INTERFACE" | grep 'inet addr' | awk '{print $2}' | cut -d: -f2)
+ } > /dev/null 2>> error.log
+
+ #check if iptmon_tx table exists
+ #this table will be used to record all transmitted data by adding rules for each user
+ iptables -t mangle -L iptmon_tx > /dev/null 2>> error.log
+ # shellcheck disable=SC2181
+ if [[ $? -ne 0 ]]; then
+ iptables -t mangle -N iptmon_tx > /dev/null 2>> error.log
+ fi
+
+ iptables -t mangle -C FORWARD -i "${HS_INTERFACE}" -j iptmon_tx > /dev/null 2>> error.log
+ # shellcheck disable=SC2181
+ if [[ $? -ne 0 ]]; then
+ #packets that come in on the hotspot interface (-i flag) and are destined for a different host than the hotspot host will be forwarded to iptmon_tx
+ iptables -t mangle -A FORWARD -i "${HS_INTERFACE}" -j iptmon_tx > /dev/null 2>> error.log
+ fi
+
+ #create iptmon_rx
+ iptables -t mangle -L iptmon_rx > /dev/null 2>> error.log
+ # shellcheck disable=SC2181
+ if [[ $? -ne 0 ]]; then
+ {
+ iptables -t mangle -N iptmon_rx
+ #if data is coming from the host itself then don't count it towards the rx total
+ iptables -t mangle -I iptmon_rx 1 -s "${LOCAL_IP}" -j RETURN
+ } > /dev/null 2>> error.log
+ fi
+
+ iptables -t mangle -C POSTROUTING -o "${HS_INTERFACE}" -j iptmon_rx > /dev/null 2>> error.log
+ # shellcheck disable=SC2181
+ if [[ $? -ne 0 ]]; then
+ #if data is going out the hotspot interface (-o flag) and is not from the host itself then it will be forwared to iptmon_rx
+ iptables -t mangle -A POSTROUTING -o "${HS_INTERFACE}" -j iptmon_rx > /dev/null 2>> error.log
+ fi
+
+ #special rules that service the cybercafe system by blocking certain traffic over hotspot interface
+ iptables -t nat -C PREROUTING -p tcp -i "${HS_INTERFACE}" -j DNAT --to-destination "${LOCAL_IP}:80" > /dev/null 2>> error.log #checks to see that rules don't exist
+ # shellcheck disable=SC2181
+ if [[ $? -ne 0 ]]; then
+ {
+ #redirects all tcp traffic to captive webserver port so that 'sign-in' notification is displayed to user when device does http checks
+ #also blocks typically web navigation
+ #Note: Since the protocol is tcp it won't mess up any important DNS or other services
+ iptables -t nat -I PREROUTING 1 -p tcp -i "${HS_INTERFACE}" \
+ -m comment --comment "cybercafe-dnat" \
+ -j DNAT --to-destination "${LOCAL_IP}:80"
+
+ #deprecated rules
+ ##safeguard: allows solicitation to DNS server (note this will show up as rule #2 on PREROUTING if uncommeneted)
+ #iptables -t nat -I PREROUTING 1 -p all -i ${HS_INTERFACE} -s 0.0.0.0/32 -d 255.255.255.255/32 --dport 67 -j RETURN > /dev/null 2>> error.log
+ ##safeguard: allows domain name resolution for things like google.com (note this will show up as rule #1 on PREROUTING if uncommeneted)
+ #iptables -t nat -I PREROUTING 1 -p all -i ${HS_INTERFACE} -d ${LOCAL_IP} --dport 53 -j RETURN > /dev/null 2>> error.log
+ #
+
+ #blocks requests incoming on the hotspot interface that is not destined for the hotspot host (exceptions will be made on a user basis once they sign in)
+ iptables -t filter -I FORWARD 1 -p all -i "${HS_INTERFACE}" \
+ -m comment --comment "cybercafe-block-in" \
+ -j DROP
+ #in the uncommon event that traffic going out onto the hotspot interface that isn't from the host (exceptions will be made on a user basis once they sign in)
+ iptables -t filter -I FORWARD 1 -p all -o "${HS_INTERFACE}" ! -s "${LOCAL_IP}" \
+ -m comment --comment "cybercafe-block-out" \
+ -j DROP
+ } > /dev/null 2>> error.log
+ fi
+
+ # Ensure captive portal HTTPD server is running
+ if ! start_captive_webserver; then
+ echo "$(date '+%Y-%m-%dT%H:%M:%S%z') Error in Cybercafe_setupFunctions.sh: Line '${LINENO}' - Failed to start captive webserver" >> error.log
+ fi
+}
+
+
+function shutdown_infrastructure
+#remove any necessary infrastructure for running the CyberCafe system
+{
+ # preserve existing trap / error logging
+ trap 'echo -e "$(date '+%Y-%m-%dT%H:%M:%S%z') Error in Cybercafe_setupFunctions.sh: Line '${LINENO}' \n" >> error.log' ERR > /dev/null 2>> error.log
+
+ # dry-run support (export DRY_RUN=true to simulate)
+ DRY_RUN=${DRY_RUN:-false}
+ run_cmd() {
+ if [[ "${DRY_RUN}" == "true" ]]; then
+ echo "[DRY-RUN] $*"
+ else
+ # execute and append stderr to error.log so we keep original behavior
+ # shellcheck disable=SC2294
+ eval "$@" 2>> error.log || true
+ fi
+ }
+
+ log() { echo "[shutdown_infrastructure] $*"; }
+ warn() { echo "[shutdown_infrastructure][WARN] $*" >&2; }
+
+ #Get hotspot ip for dismantling iptable rules (best-effort)
+ LOCAL_IP=$(ifconfig "$HS_INTERFACE" | grep 'inet addr' | awk '{print $2}' | cut -d: -f2) > /dev/null 2>> error.log || LOCAL_IP=''
+
+ log "Beginning shutdown_infrastructure (dry-run=${DRY_RUN})"
+
+ # 1) Stop captive server (safe)
+ log "Stopping captive portal webserver (pkill lighttpd)"
+ run_cmd "pkill lighttpd > /dev/null || true"
+
+ # 2) Remove status file if present (if config defines STATUS_PATH)
+ : "${STATUS_PATH:=${STATUS_PATH:-}}"
+ if [[ -n "${STATUS_PATH}" ]] && [[ -e "${STATUS_PATH}" ]]; then
+ log "Removing status path: ${STATUS_PATH}"
+ run_cmd "rm -f -- '${STATUS_PATH}' || true"
+ else
+ log "No STATUS_PATH present or file missing; skipping"
+ fi
+
+ # 3) Remove mangle references (Chris: iptmon_tx, iptmon_rx)
+ if command -v iptables >/dev/null 2>&1; then
+ log "Attempting to remove mangle table references to iptmon_tx / iptmon_rx"
+
+ # remove FORWARD -j iptmon_tx (if present)
+ run_cmd "iptables -t mangle -D FORWARD -i '${HS_INTERFACE}' -j iptmon_tx > /dev/null 2>> error.log || true"
+ run_cmd "iptables -t mangle -D POSTROUTING -o '${HS_INTERFACE}' -j iptmon_rx > /dev/null 2>> error.log || true"
+
+ # flush & delete chains safely (will ignore if not present)
+ for c in iptmon_rx iptmon_tx; do
+ # flush chain if exists
+ if iptables -t mangle -L "${c}" > /dev/null 2>> error.log; then
+ log "Flushing and deleting chain: ${c}"
+ run_cmd "iptables -t mangle -F ${c} > /dev/null 2>> error.log || true"
+ run_cmd "iptables -t mangle -X ${c} > /dev/null 2>> error.log || true"
+ else
+ log "Chain ${c} not present; skipping"
+ fi
+ done
+ else
+ warn "iptables not found; skipping mangle cleanup"
+ fi
+
+ # 4) Remove NAT PREROUTING redirect rules (Chris)
+ if command -v iptables >/dev/null 2>&1; then
+ log "Cleaning NAT PREROUTING redirect rules (best-effort)"
+ # Attempt to find PREROUTING DNAT entries and delete them
+ # We parse iptables-save style output and convert -A to -D for deletion
+ iptables -t nat -S 2>> error.log | grep -i "PREROUTING" | grep -E "DNAT|--to-destination" 2>> error.log | while read -r r; do
+ delcmd="${r/-A/-D}"
+ log "Deleting nat rule: $delcmd"
+ run_cmd "iptables -t nat $delcmd > /dev/null 2>> error.log || true"
+ done
+
+ # also attempt to delete the specific rules added by John's setup (tcp redirect & FORWARD DROP) if present
+ run_cmd "iptables -t nat -D PREROUTING -p tcp -i '${HS_INTERFACE}' -j DNAT --to-destination '${LOCAL_IP}:80' > /dev/null 2>> error.log || true"
+ run_cmd "iptables -t filter -D FORWARD -p all -i '${HS_INTERFACE}' -j DROP > /dev/null 2>> error.log || true"
+ run_cmd "iptables -t filter -D FORWARD -p all -o '${HS_INTERFACE}' ! -s '${LOCAL_IP}' -j DROP > /dev/null 2>> error.log || true"
+ fi
+
+ # 5) Remove John's mirrored chains and per-user chains (best-effort)
+ if command -v iptables >/dev/null 2>&1; then
+ MIRROR_PREFIX="${MIRROR_PREFIX:-CYBERCAFE-MIRROR-}"
+ USER_CHAIN_PREFIX="${USER_CHAIN_PREFIX:-cybercafe-user-}"
+
+ log "Removing mirror chains with prefix ${MIRROR_PREFIX} (if any)"
+ iptables -S 2>> error.log | awk '{print $2}' | grep -E "^${MIRROR_PREFIX}" 2>> error.log | sort -u | while read -r ch; do
+ [[ -z "$ch" ]] && continue
+ log "Found mirror chain: $ch -- flushing & deleting"
+ run_cmd "iptables -F ${ch} > /dev/null 2>> error.log || true"
+ run_cmd "iptables -X ${ch} > /dev/null 2>> error.log || true"
+ done
+
+ log "Removing per-user chains with prefix ${USER_CHAIN_PREFIX} (if any)"
+ iptables -S 2>> error.log | awk '{print $2}' | grep -E "^${USER_CHAIN_PREFIX}" 2>> error.log | sort -u | while read -r uch; do
+ [[ -z "$uch" ]] && continue
+ log "Found user chain: $uch -- flushing & deleting"
+ run_cmd "iptables -F ${uch} > /dev/null 2>> error.log || true"
+ run_cmd "iptables -X ${uch} > /dev/null 2>> error.log || true"
+ done
+ fi
+
+ # 6) Remove qdisc on hotspot interface (Chris)
+ if command -v tc >/dev/null 2>&1; then
+ if ip link show "${HS_INTERFACE}" > /dev/null 2>> error.log; then
+ log "Deleting qdisc on ${HS_INTERFACE}"
+ run_cmd "tc qdisc del dev ${HS_INTERFACE} root > /dev/null 2>> error.log || true"
+ else
+ log "Interface ${HS_INTERFACE} not present; skipping tc cleanup"
+ fi
+ else
+ warn "tc not found; skipping qdisc cleanup"
+ fi
+
+ # 7) Remove all commented Cybercafe rules
+ # Remove Cybercafe filter FORWARD rules by comment
+ iptables -t filter -S FORWARD 2>> error.log | grep 'cybercafe-block-' | while read -r rule; do
+ del_rule="${rule/-A/-D}"
+ # shellcheck disable=SC2086
+ iptables -t filter $del_rule > /dev/null 2>> error.log || true
+ done
+
+ # Remove Cybercafe nat PREROUTING rules by comment
+ iptables -t nat -S PREROUTING 2>> error.log | grep 'cybercafe-dnat' | while read -r rule; do
+ del_rule="${rule/-A/-D}"
+ # shellcheck disable=SC2086
+ iptables -t nat $del_rule > /dev/null 2>> error.log || true
+ done
+
+ # 8) Reset runtime variables to safe defaults
+ export LOCAL_IP=''
+ export HS_STATUS='down'
+ export HS_STATUS_PREV='down'
+
+ log "shutdown_infrastructure completed (dry-run=${DRY_RUN})"
+}
+
+function start_captive_webserver
+#starts the lighttpd webserver that acts as a captive web portal for sign in and such
+{
+ #Make sure required variables are set
+ if [ -z "${LIGHTTPD_PATH:-}" ] || [ -z "${LIGHTTPD_CONF:-}" ]; then
+ echo "$(date '+%Y-%m-%dT%H:%M:%S%z') Error in Cybercafe_setupFunctions.sh: Line ${LINENO} - LIGHTTPD_PATH or LIGHTTPD_CONF_PATH variable not set" >> error.log
+ return 1
+ fi
+
+ #Make sure paths are valid
+ if [ ! -x "${LIGHTTPD_PATH}" ]; then
+ echo "$(date '+%Y-%m-%dT%H:%M:%S%z') Error in Cybercafe_setupFunctions.sh: Line ${LINENO} - lighttpd executable not found at LIGHTTPD_PATH: ${LIGHTTPD_PATH}" >> error.log
+ return 1
+ fi
+ if [ ! -f "${LIGHTTPD_CONF}" ]; then
+ echo "$(date '+%Y-%m-%dT%H:%M:%S%z') Error in Cybercafe_setupFunctions.sh: Line ${LINENO} - lighttpd configuration file not found at LIGHTTPD_CONF_PATH: ${LIGHTTPD_CONF}" >> error.log
+ return 1
+ fi
+
+ #Idempotency check: ensure server is not already running
+ if pgrep lighttpd > /dev/null 2>> error.log; then
+ echo "$(date '+%Y-%m-%dT%H:%M:%S%z') Captive portal webserver already running." >> error.log
+ return 0
+ fi
+
+ #Start webserver in background, minimal logging
+ echo "$(date '+%Y-%m-%dT%H:%M:%S%z') Starting captive portal webserver..." >> error.log
+ "${LIGHTTPD_PATH}" -f "${LIGHTTPD_CONF}" > /dev/null 2>> error.log &
+
+ echo "$(date '+%Y-%m-%dT%H:%M:%S%z') Captive portal webserver started." >> error.log
+ return 0
+}
diff --git a/Backend/Cybercafe_testbed.sh b/Backend/Cybercafe_testbed.sh
new file mode 100644
index 0000000..ef59216
--- /dev/null
+++ b/Backend/Cybercafe_testbed.sh
@@ -0,0 +1,52 @@
+#!/usr/bin/env bash
+#Organization: Grey-box
+#Project: Cybercafe
+#File: testbed
+#Description: Used to test functions contained in the different Cybercafe libraries for errors or other unwanted behaviour
+
+###INCLUDES###
+. ./Cybercafe_setupFunctions.sh
+. ./Cybercafe_internetSessionFunctions.sh
+
+PS4='Line[$LINENO]: '
+
+display_information()
+{
+ echo ""
+ echo "---Stage ${1}---"
+ echo -e "\t---Iptables---"
+ iptables -t nat -L PREROUTING -vxn
+ iptables -t mangle -L iptmon_tx -vxn
+ iptables -t mangle -L iptmon_rx -vxn
+ echo -e "\t---sqlite3---"
+ echo -e "\t\t---internet_sessions---"
+ sqlite3 "$DATABASE_PATH" -header "SELECT * FROM internet_sessions"
+ echo -e "\t\t---user_data_usage---"
+ sqlite3 "$DATABASE_PATH" -header "SELECT * FROM user_data_usage"
+ read -r -p "Press any key to continue..."
+}
+
+DATETIME=$(date '+%Y-%m-%d %H:%M:%S')
+DATETIME_LASTREQUEST=$(date '+%Y-%m-%d %H:%M:%S')
+sqlite3 "$DATABASE_PATH" "INSERT INTO internet_sessions VALUES (0,1,'a838fjdlkc908sdjfk3jnk2wjnef','192.168.1.45',100,100,1,'${DATETIME}','${DATETIME_LASTREQUEST}')"
+sqlite3 "$DATABASE_PATH" "INSERT INTO internet_sessions VALUES (1,2,'b838fjdlkc908sdjfk3jnk2wjnef','192.168.1.46',100,100,0,'${DATETIME}','${DATETIME_LASTREQUEST}')"
+sqlite3 "$DATABASE_PATH" "INSERT INTO internet_sessions VALUES (2,3,'c838fjdlkc908sdjfk3jnk2wjnef','192.168.1.47',100,100,0,'${DATETIME}','${DATETIME_LASTREQUEST}')"
+echo ""
+display_information '1'
+
+setup_infrastructure
+
+display_information '2'
+
+check_internet_sessions
+
+display_information '3'
+
+clear_internet_sessions
+
+display_information '4'
+
+shutdown_infrastructure
+
+sqlite3 "$DATABASE_PATH" "DELETE FROM internet_sessions WHERE 1=1"
+sqlite3 "$DATABASE_PATH" "DELETE FROM user_data_usage WHERE 1=1"
\ No newline at end of file
diff --git a/Backend/Makefile b/Backend/Makefile
new file mode 100644
index 0000000..1d42957
--- /dev/null
+++ b/Backend/Makefile
@@ -0,0 +1,7 @@
+all:
+ cp cybercafe.sh cybercafe
+ chmod +x cybercafe
+clean:
+ rm cybercafe
+ rm nohup.out
+
\ No newline at end of file
diff --git a/Backend/cybercafe.conf b/Backend/cybercafe.conf
new file mode 100644
index 0000000..3fb6b73
--- /dev/null
+++ b/Backend/cybercafe.conf
@@ -0,0 +1,12 @@
+BASE_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT_PATH="$(cd "$BASE_PATH/.." && pwd)"
+DATABASE_PATH="$PROJECT_ROOT_PATH/Database/CyberCafe_Database.db"
+#path to lighttpd software
+LIGHTTPD_PATH="/data/data/com.termux/files/usr/bin/lighttpd"
+LIGHTTPD_CONF="$BASE_PATH/lighttpd.conf"
+# Interface of the hotspot
+HS_INTERFACE='wlan0'
+#Maximum time that a session can exist in seconds
+SESSION_MAX_AGE=43200
+#Maximum time that a session can be idle in seconds
+SESSION_MAX_IDLETIME=1200
diff --git a/Backend/cybercafe.sh b/Backend/cybercafe.sh
new file mode 100644
index 0000000..43e9b14
--- /dev/null
+++ b/Backend/cybercafe.sh
@@ -0,0 +1,227 @@
+#!/data/data/com.termux/files/usr/bin/bash
+#Organization: Grey-box
+#Project: Cybercafe
+#File: Control File
+#Description: Acts as the interface between the administrator/developer and the Cybercafe backend.
+
+##VARIABLES##
+BASE_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+UTIL_PATH="/data/data/com.termux/files/usr/bin"
+
+
+##INCLUDES##
+. "$BASE_PATH/cybercafe.conf"
+. "$BASE_PATH/Cybercafe_setupFunctions.sh"
+. "$BASE_PATH/Cybercafe_internetSessionFunctions.sh"
+
+##COMMANDS##
+function command_run
+{
+ #test to see if daemon is already running
+ # shellcheck disable=SC2009
+ ps -eo name,cmdline | grep "${BASE_PATH}/Cybercafe_daemon.sh" | grep -v grep > /dev/null 2>&1 # if this returns 0 it implies that the script exists and is running
+ # shellcheck disable=SC2181
+ if [[ $? -eq 0 ]]; then
+ echo "Cybercafe infrastructure already running."
+ else
+ printf "%s" "$(date +%T)" \
+ && echo " Running Cybercafe..."
+ nohup $UTIL_PATH/bash "$BASE_PATH/Cybercafe_daemon.sh" & #run CyberCafe daemon as a seperate process
+ printf "%s" "$(date +%T)" \
+ && echo " Cybercafe started."
+
+ #Send warning if hotspot is down
+ ip add show dev $HS_INTERFACE | grep UP > /dev/null # Does it appear the hotspot is active?
+ # shellcheck disable=SC2181
+ if [[ $? -ne 0 ]]; then
+ echo "Warning the designated hotspot interface is not currently up."
+ fi
+ fi
+}
+
+function command_status
+{
+ echo ""
+ printf "%s" "$(date '+%Y-%m-%d %H:%M:%S')"
+ echo ""
+ # shellcheck disable=SC2009
+ ps -eo name,cmdline | grep "${BASE_PATH}/Cybercafe_daemon.sh" | grep -v grep > /dev/null 2>&1 # if this returns 0 it implies that the script exists and is running
+ # shellcheck disable=SC2181
+ if [[ $? -eq 0 ]]; then
+ echo "Status: Running"
+ echo ""
+ echo "Process Info:"
+ # shellcheck disable=SC2009
+ ps -o user,pid,ppid,uid,stime,stat,name,cmdline | head -n 1
+ # shellcheck disable=SC2009
+ ps -eo user,pid,ppid,uid,stime,stat,name,cmdline | grep "${BASE_PATH}/Cybercafe_daemon.sh" | grep -v grep
+ else
+ echo "Status: Stopped"
+ fi
+ echo ""
+ echo "Hotspot Interface Info:"
+ ip add show dev "$HS_INTERFACE"
+ echo ""
+}
+
+function command_list
+{
+ function command_list_sessions
+ {
+ sqlite3 "${DATABASE_PATH}" -header "SELECT * FROM internet_sessions"
+ }
+ function command_list_users
+ {
+ sqlite3 "${DATABASE_PATH}" -header "SELECT * FROM users"
+ }
+ function command_list_lanes
+ {
+ sqlite3 "${DATABASE_PATH}" -header "SELECT * FROM data_lanes"
+ }
+ function command_list_rules
+ {
+ echo "NAT:"
+ iptables -t nat -L PREROUTING -vxn
+ echo ""
+ echo "MANGLE:"
+ iptables -t mangle -L FORWARD -vxn
+ echo ""
+ iptables -t mangle -L POSTROUTING -vxn
+ echo ""
+ iptables -t mangle -L iptmon_tx -vxn
+ echo ""
+ iptables -t mangle -L iptmon_rx -vxn
+ echo ""
+ echo "FILTER:"
+ iptables -t filter -L FORWARD -vxn
+ echo ""
+ }
+ if [[ ${args[1]} == 'sessions' ]]; then
+ command_list_sessions
+ elif [[ ${args[1]} == 'users' ]]; then
+ command_list_users
+ elif [[ ${args[1]} == 'lanes' ]]; then
+ command_list_lanes
+ elif [[ ${args[1]} == 'rules' ]]; then
+ command_list_rules
+ else
+ echo "Invalid list reference use 'help' for more information"
+ fi
+}
+function command_errorlog
+{
+ # shellcheck disable=SC2002
+ cat "$BASE_PATH/error.log" | tail -n 30
+}
+
+function command_shutdown
+{
+ # shellcheck disable=SC2009
+ ps -eo stat,name,cmdline | grep "${BASE_PATH}/Cybercafe_daemon.sh" | grep -v grep > /dev/null 2>&1 # if this returns 0 it implies that the script exists and is running
+ # shellcheck disable=SC2181
+ if [[ $? -eq 0 ]]; then
+ printf "%s" "$(date +%T)"
+ echo ""
+ echo "Shutting down"
+ touch shutdown.confirmed
+ while true; do
+ sleep 1
+ # shellcheck disable=SC2009
+ ps -eo stat,name,cmdline | grep "${BASE_PATH}/Cybercafe_daemon.sh" | grep -v grep > /dev/null 2>&1 # if this returns 1 it implies that the script has stopped
+ # shellcheck disable=SC2181
+ if [[ $? -eq 1 ]]; then
+ rm shutdown.confirmed
+ printf "%s" "$(date +%T)"
+ echo " Shutdown complete"
+ break
+ fi
+ done
+ else
+ echo "Cybercafe Daemon isn't running"
+ fi
+}
+
+function command_kill
+{
+ pkill -f "$BASE_PATH/Cybercafe_daemon.sh"
+ clear_internet_sessions
+ shutdown_infrastructure
+}
+
+function command_help
+{
+ echo "Valid commands:"
+ echo "run - Starts Cybercafe infrastructure."
+ echo "status - Show status info on Cybercafe infrastructure."
+ echo "list (info) - List database info such as (sessions,users,lanes,rules)"
+ echo "errorlog - Prints the most recent errors recorded to errorlog.txt"
+ echo "shutdown - Sends the signal to system to shutdown cleanly."
+ echo "kill - Kills all Cybercafe scripts (not recommended)."
+ echo "exit - Leave this command line."
+ echo "help - Display this page."
+}
+
+##FUNCTIONS##
+function welcome_text
+{
+ echo -e "Grey-box Cybercafe Prototype 2025\n"
+}
+
+##PROGRAM##
+if [[ $# -gt 0 ]]; then
+ trap 'echo "Error: Line ${LINENO}" >> error.log' ERR
+ #run non-interactive mode
+ args=("$@")
+ #argc=$(($#))
+ if [[ ${args[0]} == 'run' ]]; then
+ command_run
+ elif [[ ${args[0]} == 'status' ]]; then
+ command_status
+ elif [[ ${args[0]} == 'list' ]]; then
+ command_list
+ elif [[ ${args[0]} == 'errorlog' ]]; then
+ command_errorlog
+ elif [[ ${args[0]} == 'shutdown' ]]; then
+ command_shutdown
+ elif [[ ${args[0]} == 'kill' ]]; then
+ command_kill
+ elif [[ ${args[0]} == 'exit' ]]; then
+ :
+ elif [[ ${args[0]} == 'help' ]]; then
+ command_help
+ else
+ echo "Unrecognized command use 'help' for more information."
+ fi
+else
+ #run interactive mode
+ welcome_text
+ while true; do
+ trap 'echo "Error: Line ${LINENO}" >> error.log' ERR
+ echo -n ">> "
+ read -r user_input
+ args=()
+ for i in $user_input; do
+ args+=("$i")
+ done
+ #argc=$((${#args[@]}))
+ if [[ ${args[0]} == 'run' ]]; then
+ command_run
+ elif [[ ${args[0]} == 'status' ]]; then
+ command_status
+ elif [[ ${args[0]} == 'list' ]]; then
+ command_list
+ elif [[ ${args[0]} == 'errorlog' ]]; then
+ command_errorlog
+ elif [[ ${args[0]} == 'shutdown' ]]; then
+ command_shutdown
+ elif [[ ${args[0]} == 'kill' ]]; then
+ command_kill
+ elif [[ ${args[0]} == 'exit' ]]; then
+ break;
+ elif [[ ${args[0]} == 'help' ]]; then
+ command_help
+ else
+ echo "Unrecognized command use 'help' for more information."
+ fi
+ done
+fi
diff --git a/Backend/lighttpd.conf b/Backend/lighttpd.conf
new file mode 100644
index 0000000..6665343
--- /dev/null
+++ b/Backend/lighttpd.conf
@@ -0,0 +1,65 @@
+#Variables
+var.log_root = "/data/data/com.termux/files/usr/var/log/lighttpd"
+var.server_root = "/data/data/com.termux/files/usr/var/www/"
+var.state_dir = "/data/data/com.termux/files/usr/var/run"
+var.home_dir = "/data/data/com.termux/files/usr/var/lib/lighttpd"
+var.conf_dir = "/data/data/com.termux/files/usr/etc/lighttpd"
+var.vhosts_dir = server_root + "/vhosts"
+var.cache_dir = "/data/data/com.termux/files/usr/var/cache/lighttpd"
+var.socket_dir = home_dir + "/sockets"
+
+
+#Includes
+include conf_dir + "/modules.conf"
+include conf_dir + "/conf.d/access_log.conf"
+include conf_dir + "/conf.d/debug.conf"
+include conf_dir + "/conf.d/mime.conf"
+include conf_dir + "/conf.d/dirlisting.conf"
+include conf_dir + "/conf.d/fastcgi.conf"
+
+
+#Basic Configuration
+#server.document-root = server_root + "/htdocs/"
+server.document-root = "/data/data/com.termux/files/home/Project_Cybercafe/Website_ASU2024"
+index-file.names = ( "page.php" )
+#server.bind = "localhost"
+server.port = 80
+#server.username = "u0_a234"
+#server.groupname = "u0_a234"
+
+
+#File Config
+server.pid-file = state_dir + "/lighttpd.pid"
+server.errorlog = log_root + "/error.log"
+server.upload-dirs = ("/data/data/com.termux/files/usr/tmp")
+
+
+#Other Config
+server.max-fds = 16384
+url.access-deny = ( "~", ".inc" )
+static-file.exclude-extensions = ( ".fcgi", ".rb", "~", ".inc" )
+
+#Needed to load php instead of push php files
+fastcgi.server += ( ".php" =>
+ ((
+ "bin-path" => "/data/data/com.termux/files/usr/bin/php-cgi",
+ "socket" => "/data/data/com.termux/files/usr/tmp/php.socket",
+ "max-procs" => 1,
+ "bin-environment" => (
+ "PHP_FCGI_CHILDREN" => "1",
+ "PHP_FCGI_MAX_REQUESTS" => "1000"
+ )
+ ))
+)
+
+#This variables need to be dynamically updated (automation for this still needs to be added)
+var.allowedHostIPs = "10\.18\.120\.52|192\.168\.43\.239|127\.0\.0\.1"
+var.hotspotNetwork = "192.168.43.0/24"
+
+#Redirect HTTP traffic to webserver index page
+#this is necessary for captive portal functionallity since iptables will redirect all hotspot traffic to lighttpd
+$HTTP["host"] !~ allowedHostIPs {
+ $HTTP["remoteip"] == hotspotNetwork {
+ url.redirect = ("" => "http://192.168.43.239/")
+ }
+}
diff --git a/Backend/test/check_hotspot_status_test.bats b/Backend/test/check_hotspot_status_test.bats
new file mode 100644
index 0000000..7e58967
--- /dev/null
+++ b/Backend/test/check_hotspot_status_test.bats
@@ -0,0 +1,386 @@
+#!/usr/bin/env bats
+
+# Automated testing for check_hotspot_status function, where test cases consider state tracking, transition detection
+# and timestamp guard logic.
+
+setup() {
+
+###INCLUDES###
+ TEST_DIR="$(cd "$(dirname "${BATS_TEST_FILENAME}")" && pwd)"
+ BACKEND_DIR="$(cd "$TEST_DIR/.." && pwd)"
+ . "$TEST_DIR/cybercafe.conf"
+ . "$BACKEND_DIR/Cybercafe_setupFunctions.sh"
+
+ # Create temp directories for test files
+ export STATUS_PATH="$TEST_DIR/tmp/cybercafe.confirmed"
+ export REFRESH_TIME="${REFRESH_TIME:-3600}" # 1 Hour Resets as default
+
+ # Validate directory exists
+ mkdir -p "$(dirname "$STATUS_PATH")"
+
+ # Initializing State Variables
+ export HS_STATUS='down'
+ export HS_STATUS_PREV='down'
+ export TIME_TO_REFRESH='false'
+ export LOCAL_IP=''
+}
+
+teardown() {
+
+ # Clean up test file
+ rm -f "$STATUS_PATH"
+
+ # Reset state variables by unsetting
+ unset HS_STATUS
+ unset HS_STATUS_PREV
+ unset TIME_TO_REFRESH
+ unset LOCAL_IP
+}
+
+# Consider 14 Test Cases
+
+@test "example works" {
+ run echo "hi"
+ [ "$status" -eq 0 ]
+}
+
+# ============================================================
+# TEST 1: Initial State (Hotspot Down)
+# ============================================================
+@test "[TEST 1] Initial state - hotspot down" {
+ run check_hotspot_status
+
+ [[ "$HS_STATUS" == "down" ]]
+ [[ "$HS_STATUS_PREV" == "down" ]]
+ [[ "$TIME_TO_REFRESH" == "false" ]]
+
+}
+
+# ============================================================
+# TEST 2: State Persistence (No Change)
+# ============================================================
+@test "[TEST 2] Call function again - state should stay same" {
+ run check_hotspot_status
+ run check_hotspot_status
+
+ [[ "$HS_STATUS" == "down" ]]
+ [[ "$HS_STATUS_PREV" == "down" ]]
+ [[ "$TIME_TO_REFRESH" == "false" ]]
+}
+
+# ============================================================
+# TEST 3: Simulate Hotspot Coming Up (Transition Detection)
+# ============================================================
+@test "[TEST 3] Simulate hotspot UP - should detect transition" {
+ # Begin with Hotspot UP
+ HS_STATUS='up'
+ HS_STATUS_PREV='up'
+ TIME_TO_REFRESH='false'
+
+ wlan_ip_status=1 # Non-zero means IP not found
+ HS_STATUS_PREV=$HS_STATUS
+
+ if [[ $wlan_ip_status -eq 0 ]]; then
+ HS_STATUS='up'
+
+ #To detect any drift/small crashes or glitches when hotspot goes down, but our state var. is still 'up'.
+ elif [[ $wlan_ip_status -ne 0 && $HS_STATUS == 'up' ]]; then
+ # Assume hotspot has recently gone done.
+ HS_STATUS='down'
+ TIME_TO_REFRESH=true
+ else
+ HS_STATUS='down'
+ TIME_TO_REFRESH=false
+ fi
+
+ # Then put hotspot back up -> should set refresh to true and update prev
+ [[ "$HS_STATUS" == "down" ]]
+ [[ "$HS_STATUS_PREV" == "up" ]]
+ [[ "$TIME_TO_REFRESH" == "true" ]]
+}
+
+# ============================================================
+# TEST 4: No State Change (again) DOWN -> DOWN
+# ============================================================
+@test "[TEST 4] No stage change, should keep REFRESH to false" {
+ HS_STATUS='down'
+ HS_STATUS_PREV='down'
+
+ run check_hotspot_status
+
+ [[ "$HS_STATUS" == "down" ]]
+ [[ "$HS_STATUS_PREV" == "down" ]]
+ [[ "$TIME_TO_REFRESH" == "false" ]]
+}
+
+# ============================================================
+# TEST 5: Fresh Setup File (Guard Logic - Fresh Config)
+# ============================================================
+@test "[TEST 5] Fresh STATUS_PATH file - guard should not trigger refresh" {
+ HS_STATUS='up'
+ HS_STATUS_PREV='up'
+
+ # Create fresh status file
+ touch "$STATUS_PATH"
+
+ # Manually run guard logic (behavior simulation when hotspot = UP)
+ if [[ $HS_STATUS == 'up' && ! -e $STATUS_PATH ]]; then
+ TIME_TO_REFRESH=true
+ elif [[ $HS_STATUS == 'up' && -e $STATUS_PATH ]]; then
+ cf_status_path_age=$(echo "$(date +%s) - $(date -r ${STATUS_PATH} +%s)" | bc 2>> error.log)
+ if [[ $cf_status_path_age -gt $REFRESH_TIME ]]; then
+ TIME_TO_REFRESH=true
+ else
+ TIME_TO_REFRESH=false
+ fi
+ else
+ TIME_TO_REFRESH=false
+ fi
+
+ [[ "$TIME_TO_REFRESH" == "false" ]]
+}
+
+# ============================================================
+# TEST 6: Stale Setup File (Guard Logic - Trigger Refresh)
+# ============================================================
+@test "[TEST 6] Guard logic - stale STATUS_PATH file triggers refresh when UP" {
+ HS_STATUS='up'
+ HS_STATUS_PREV='up'
+
+ # Create status file, but 2+ hours old backdating with -t seconds
+ touch "$STATUS_PATH"
+ touch -t 202301010000 "$STATUS_PATH"
+
+ # Manually run guard logic (behavior simulation when hotspot = UP)
+ if [[ $HS_STATUS == 'up' && ! -e $STATUS_PATH ]]; then
+ TIME_TO_REFRESH=true
+ elif [[ $HS_STATUS == 'up' && -e $STATUS_PATH ]]; then
+ cf_status_path_age=$(echo "$(date +%s) - $(date -r ${STATUS_PATH} +%s)" | bc 2>> error.log)
+ if [[ $cf_status_path_age -gt $REFRESH_TIME ]]; then
+ TIME_TO_REFRESH=true
+ else
+ TIME_TO_REFRESH=false
+ fi
+ else
+ TIME_TO_REFRESH=false
+ fi
+
+ [[ "$TIME_TO_REFRESH" == "true" ]]
+}
+
+# ============================================================
+# TEST 7: Missing STATUS_PATH file (Guard Logic)
+# ============================================================
+@test "[TEST 7] Guard logic - missing STATUS_PATH file triggers refresh when UP" {
+ HS_STATUS='up'
+ HS_STATUS_PREV='down'
+
+ # Ensure STATUS_PATH file is removed
+ rm -f "$STATUS_PATH"
+
+
+ # Manually run guard logic (behavior simulation when hotspot = UP)
+ if [[ $HS_STATUS == 'up' && ! -e $STATUS_PATH ]]; then
+ TIME_TO_REFRESH=true
+ elif [[ $HS_STATUS == 'up' && -e $STATUS_PATH ]]; then
+ cf_status_path_age=$(echo "$(date +%s) - $(date -r ${STATUS_PATH} +%s)" | bc 2>> error.log)
+ if [[ $cf_status_path_age -gt $REFRESH_TIME ]]; then
+ TIME_TO_REFRESH=true
+ else
+ TIME_TO_REFRESH=false
+ fi
+ else
+ TIME_TO_REFRESH=false
+ fi
+
+ [[ "$TIME_TO_REFRESH" == "true" ]]
+}
+
+# ============================================================
+# TEST 8: HOTSPOT Initially Down (Doesn't Trigger Refresh)
+# ============================================================
+@test "[TEST 8] Guard logic - when hotspot is DOWN, TIME_TO_REFRESH stays false to keep resources." {
+ HS_STATUS='down'
+ HS_STATUS_PREV='up'
+
+ # Ensure STATUS_PATH file is removed
+ rm -f "$STATUS_PATH"
+
+
+ # Manually run guard logic (behavior simulation when hotspot = UP)
+ if [[ $HS_STATUS == 'up' && ! -e $STATUS_PATH ]]; then
+ TIME_TO_REFRESH=true
+ elif [[ $HS_STATUS == 'up' && -e $STATUS_PATH ]]; then
+ cf_status_path_age=$(echo "$(date +%s) - $(date -r ${STATUS_PATH} +%s)" | bc 2>> error.log)
+ if [[ $cf_status_path_age -gt $REFRESH_TIME ]]; then
+ TIME_TO_REFRESH=true
+ else
+ TIME_TO_REFRESH=false
+ fi
+ else
+ TIME_TO_REFRESH=false
+ fi
+
+ [[ "$TIME_TO_REFRESH" == "false" ]]
+}
+
+# ============================================================
+# TEST 9: Previous State Always Saves Current State First
+# ============================================================
+@test "[TEST 9] State Tracking - HS_STATUS_PREV captures previous state correctly." {
+ # Initial State
+ HS_STATUS='down'
+ HS_STATUS_PREV='down'
+
+ # Call function and save current to 'down'
+ run check_hotspot_status
+ [[ "$HS_STATUS_PREV" == "down" ]]
+
+ # Manually set STATUS to UP
+ HS_STATUS='up'
+
+ # Second call saving 'up' to PREV
+ HS_STATUS_PREV='up'
+ run check_hotspot_status
+
+ [[ "$HS_STATUS_PREV" == "up" ]]
+}
+
+# ============================================================
+# TEST 10: Drift Detection - State UP with Stale File
+# ============================================================
+@test "[TEST 10] Drift Detection - state is UP with stale file inidates refresh needed." {
+ HS_STATUS='up'
+ HS_STATUS_PREV='up'
+
+ # Create status file, but 2+ hours old backdating with -t seconds
+ touch "$STATUS_PATH"
+ touch -t 202301010000 "$STATUS_PATH"
+
+ # Manually run guard logic (behavior simulation when hotspot = UP)
+ if [[ $HS_STATUS == 'up' && ! -e $STATUS_PATH ]]; then
+ TIME_TO_REFRESH=true
+ elif [[ $HS_STATUS == 'up' && -e $STATUS_PATH ]]; then
+ cf_status_path_age=$(echo "$(date +%s) - $(date -r ${STATUS_PATH} +%s)" | bc 2>> error.log)
+ if [[ $cf_status_path_age -gt $REFRESH_TIME ]]; then
+ TIME_TO_REFRESH=true
+ else
+ TIME_TO_REFRESH=false
+ fi
+ else
+ TIME_TO_REFRESH=false
+ fi
+
+ [[ "$TIME_TO_REFRESH" == "true" ]]
+}
+
+# ============================================================
+# TEST 11: Recovery - Fresh File After Refresh
+# ============================================================
+@test "[TEST 11] Recovery - fresh STATUS_PATH file after refresh." {
+ HS_STATUS='up'
+ HS_STATUS_PREV='up'
+
+ # Create fresh file after refresh
+ touch "$STATUS_PATH"
+
+ # Manually run guard logic (behavior simulation when hotspot = UP)
+ if [[ $HS_STATUS == 'up' && ! -e $STATUS_PATH ]]; then
+ TIME_TO_REFRESH=true
+ elif [[ $HS_STATUS == 'up' && -e $STATUS_PATH ]]; then
+ cf_status_path_age=$(echo "$(date +%s) - $(date -r ${STATUS_PATH} +%s)" | bc 2>> error.log)
+ if [[ $cf_status_path_age -gt $REFRESH_TIME ]]; then
+ TIME_TO_REFRESH=true
+ else
+ TIME_TO_REFRESH=false
+ fi
+ else
+ TIME_TO_REFRESH=false
+ fi
+
+ [[ "$TIME_TO_REFRESH" == "false" ]]
+}
+
+# ============================================================
+# TEST 12: Repetition Back-to-Back Calls
+# ============================================================
+@test "[TEST 12] Consistency - B2B calls should have no state change." {
+ HS_STATUS='down'
+ HS_STATUS_PREV='down'
+ TIME_TO_REFRESH='false'
+
+ # First Call
+ run check_hotspot_status
+ local first_stat="$HS_STATUS"
+ local first_prev="$HS_STATUS_PREV"
+ local first_TTR="$TIME_TO_REFRESH"
+
+ # Second Call
+ run check_hotspot_status
+ local second_stat="$HS_STATUS"
+ local second_prev="$HS_STATUS_PREV"
+ local second_TTR="$TIME_TO_REFRESH"
+
+
+ [[ "$first_stat" == "$second_stat" ]]
+ [[ "$first_prev" == "$second_prev" ]]
+ [[ "$first_TTR" == "$second_TTR" ]]
+}
+
+# ============================================================
+# TEST 13: Edge Case - File Age at Exact Threshold
+# ============================================================
+@test "[TEST 13] Edge Case - file age at exact REFRESH_TIME boundary doesn't trigger refresh" {
+ HS_STATUS='up'
+ HS_STATUS_PREV='up'
+
+ # Create file exactly REFRESH_TIME seconds old
+ touch "$STATUS_PATH"
+ touch -d "-${REFRESH_TIME} seconds" "$STATUS_PATH" 2>/dev/null || touch -t 202301010000 "$STATUS_PATH"
+
+ # Manually run guard logic (behavior simulation when hotspot = UP)
+ if [[ $HS_STATUS == 'up' && ! -e $STATUS_PATH ]]; then
+ TIME_TO_REFRESH=true
+ elif [[ $HS_STATUS == 'up' && -e $STATUS_PATH ]]; then
+ cf_status_path_age=$(echo "$(date +%s) - $(date -r ${STATUS_PATH} +%s)" | bc 2>> error.log)
+
+ # FIX: only refresh if strictly greater than REFRESH_TIME
+ if [[ $cf_status_path_age -gt $REFRESH_TIME ]]; then
+ TIME_TO_REFRESH=true
+ else
+ TIME_TO_REFRESH=false
+ fi
+ else
+ TIME_TO_REFRESH=false
+ fi
+
+ [[ "$TIME_TO_REFRESH" == "false" || "$TIME_TO_REFRESH" == "true" ]]
+}
+
+# ============================================================
+# TEST 14: Multiple State Transitions
+# ============================================================
+@test "[TEST 14] Multiple State Transitions - rapidly change UP-DOWN-UP cycle tracking state correctly." {
+ HS_STATUS='down'
+ HS_STATUS_PREV='down'
+
+ HS_STATUS='up'
+ HS_STATUS_PREV='down'
+
+ HS_STATUS='down'
+ HS_STATUS_PREV='up'
+
+ # FInal State should save final transition of the chain
+ [[ "$HS_STATUS" == "down" ]]
+ [[ "$HS_STATUS_PREV" == "up" ]]
+}
+
+
+
+
+
+
+
+
+
+
diff --git a/Backend/test/clear_internet_sessions_test.sh b/Backend/test/clear_internet_sessions_test.sh
new file mode 100755
index 0000000..ee7d3c4
--- /dev/null
+++ b/Backend/test/clear_internet_sessions_test.sh
@@ -0,0 +1,154 @@
+#!/usr/bin/env bash
+set -Eeuo pipefail
+
+# Resolve directories
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+FIXTURE_DIR="$SCRIPT_DIR/fixtures"
+MOCK_DIR="$SCRIPT_DIR/mocks"
+TMP_DIR="$SCRIPT_DIR/tmp"
+
+# Source the function under test (will be sourced in tests to allow mocking)
+# We don't source it here yet because we need to setup variables first
+
+#################
+# Test Helpers
+#################
+
+setup_env() {
+ rm -rf "$MOCK_DIR" "$TMP_DIR"
+ mkdir -p "$MOCK_DIR" "$TMP_DIR"
+
+ # 1. Setup Mock Database
+ export DATABASE_PATH="$TMP_DIR/test_db.sqlite"
+
+ # Initialize DB with schema
+ if [ ! -f "$ROOT_DIR/../Database/CyberCafe_Database_Schema.sql" ]; then
+ echo "Error: Schema file not found at $ROOT_DIR/../Database/CyberCafe_Database_Schema.sql"
+ exit 1
+ fi
+ sqlite3 "$DATABASE_PATH" < "$ROOT_DIR/../Database/CyberCafe_Database_Schema.sql"
+
+ # Seed DB
+ if [ ! -f "$FIXTURE_DIR/seed_sessions.sql" ]; then
+ echo "Error: Seed file not found at $FIXTURE_DIR/seed_sessions.sql"
+ exit 1
+ fi
+ sqlite3 "$DATABASE_PATH" < "$FIXTURE_DIR/seed_sessions.sql"
+
+ # 2. Setup Mock iptables
+ cat > "$MOCK_DIR/iptables" <<'EOT'
+#!/usr/bin/env bash
+echo "iptables $*" >> "${MOCK_DIR}/iptables_calls.log"
+exit 0
+EOT
+ chmod +x "$MOCK_DIR/iptables"
+
+ # 3. Setup Error Log
+ touch "error.log"
+
+ # 4. Export mocks to PATH
+ export PATH="$MOCK_DIR:$PATH"
+ export MOCK_DIR
+}
+
+teardown_env() {
+ rm -rf "$MOCK_DIR" "$TMP_DIR" "error.log" 2>/dev/null || true
+}
+
+assert_db_count() {
+ local table="$1"
+ local expected="$2"
+ local count
+ count=$(sqlite3 "$DATABASE_PATH" "SELECT COUNT(*) FROM $table;")
+ if [[ "$count" -ne "$expected" ]]; then
+ echo "FAIL: Expected $expected rows in $table, found $count"
+ return 1
+ fi
+}
+
+assert_user_data_usage_exists() {
+ local user_id="$1"
+ local count
+ count=$(sqlite3 "$DATABASE_PATH" "SELECT COUNT(*) FROM user_data_usage WHERE user_id=$user_id;")
+ if [[ "$count" -eq 0 ]]; then
+ echo "FAIL: No user_data_usage found for user_id $user_id"
+ return 1
+ fi
+}
+
+#################
+# Tests
+#################
+
+test_clear_multiple_sessions() {
+ echo "TEST: clear_internet_sessions with multiple sessions"
+ setup_env
+
+ # Verify initial state
+ assert_db_count "internet_sessions" 2
+
+ # Run SUT
+ # We need to source the file inside the test or ensure variables are set before sourcing
+ # The script uses HS_INTERFACE
+ export HS_INTERFACE="wlan0"
+
+ # Verify we can source the file without executing code (it only defines functions)
+ source "$ROOT_DIR/Cybercafe_internetSessionFunctions.sh"
+
+ clear_internet_sessions
+
+ # Verify Final State
+ # 1. internet_sessions should be empty
+ assert_db_count "internet_sessions" 0
+
+ # 2. user_data_usage should have entries for the cleared sessions
+ assert_user_data_usage_exists 101 # user from seed
+ assert_user_data_usage_exists 102 # user from seed
+
+ # 3. iptables should have been called
+ if [[ ! -f "$MOCK_DIR/iptables_calls.log" ]]; then
+ echo "FAIL: iptables was not called"
+ teardown_env
+ return 1
+ fi
+
+ local ipt_calls
+ ipt_calls=$(wc -l < "$MOCK_DIR/iptables_calls.log")
+ if [[ "$ipt_calls" -lt 1 ]]; then
+ echo "FAIL: Expected iptables calls, got $ipt_calls"
+ teardown_env
+ return 1
+ fi
+
+ echo "PASS"
+ teardown_env
+}
+
+test_clear_idempotency() {
+ echo "TEST: clear_internet_sessions idempotency (empty DB)"
+ setup_env
+
+ # Clear DB first manually to simulate empty state
+ sqlite3 "$DATABASE_PATH" "DELETE FROM internet_sessions;"
+
+ export HS_INTERFACE="wlan0"
+ source "$ROOT_DIR/Cybercafe_internetSessionFunctions.sh"
+
+ # Run SUT - should not fail
+ if ! clear_internet_sessions; then
+ echo "FAIL: clear_internet_sessions returned non-zero on empty DB"
+ teardown_env
+ return 1
+ fi
+
+ # Verify still empty
+ assert_db_count "internet_sessions" 0
+
+ echo "PASS"
+ teardown_env
+}
+
+# Run tests
+test_clear_multiple_sessions
+test_clear_idempotency
diff --git a/Backend/test/cybercafe.conf b/Backend/test/cybercafe.conf
new file mode 100644
index 0000000..3844598
--- /dev/null
+++ b/Backend/test/cybercafe.conf
@@ -0,0 +1,3 @@
+HS_INTERFACE="wlan0"
+STATUS_PATH="/tmp/hotspot_status"
+REFRESH_TIME=60
diff --git a/Backend/test/delete_user_iptables_rules_test.sh b/Backend/test/delete_user_iptables_rules_test.sh
new file mode 100644
index 0000000..0e864b1
--- /dev/null
+++ b/Backend/test/delete_user_iptables_rules_test.sh
@@ -0,0 +1,193 @@
+#!/usr/bin/env bash
+set -Eeuo pipefail
+# Resolve directories
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+MOCK_DIR="$SCRIPT_DIR/mocks"
+#################
+# Test Helpers
+#################
+setup_mocks() {
+ rm -rf "$MOCK_DIR"
+ mkdir -p "$MOCK_DIR"
+
+ # Create a mock iptables that logs all calls
+ cat > "$MOCK_DIR/iptables" <<'EOT'
+#!/usr/bin/env bash
+# Log the call for verification
+echo "$*" >> "${MOCK_DIR}/iptables_calls.log"
+# Simulate behavior based on arguments
+case "$*" in
+ *"-D"*)
+ # Delete operations - succeed by default
+ # To simulate "rule not found", check for specific IPs
+ if [[ "$*" == *"192.168.1.999"* ]]; then
+ echo "iptables: No chain/target/match by that name." >&2
+ exit 1
+ fi
+ exit 0
+ ;;
+ *)
+ exit 0
+ ;;
+esac
+EOT
+ chmod +x "$MOCK_DIR/iptables"
+
+ # Ensure log file exists
+ touch "$MOCK_DIR/iptables_calls.log"
+
+ # Add mocks to PATH
+ export PATH="$MOCK_DIR:$PATH"
+ export MOCK_DIR
+}
+teardown_mocks() {
+ rm -rf "$MOCK_DIR" error.log 2>/dev/null || true
+}
+assert_iptables_called_with() {
+ local expected="$1"
+ if ! grep -qF -- "$expected" "$MOCK_DIR/iptables_calls.log"; then
+ echo "FAIL: Expected iptables call not found: $expected"
+ echo "Actual calls:"
+ cat "$MOCK_DIR/iptables_calls.log"
+ return 1
+ fi
+}
+assert_call_count() {
+ local expected="$1"
+ local actual
+ actual=$(wc -l < "$MOCK_DIR/iptables_calls.log" | tr -d ' ')
+ if [[ "$actual" -ne "$expected" ]]; then
+ echo "FAIL: Expected $expected iptables calls, got $actual"
+ cat "$MOCK_DIR/iptables_calls.log"
+ return 1
+ fi
+}
+#################
+# Test Setup
+#################
+# Source the function under test
+export HS_INTERFACE="wlan1"
+export USER_IP="" # Initialize global that function uses
+source "$ROOT_DIR/Cybercafe_internetSessionFunctions.sh"
+#################
+# Test Cases
+#################
+test_delete_rules_with_valid_ip() {
+ echo "TEST: delete_user_iptable_rules with valid IP"
+ setup_mocks
+
+ local test_ip="192.168.1.100"
+
+ # Call the function
+ delete_user_iptable_rules "$test_ip"
+ local exit_code=$?
+
+ # Verify exit code
+ if [[ $exit_code -ne 0 ]]; then
+ echo "FAIL: Expected exit code 0, got $exit_code"
+ teardown_mocks
+ return 1
+ fi
+
+ # Verify iptables was called correctly
+ assert_iptables_called_with "-t mangle -D iptmon_rx -o wlan1 -d $test_ip"
+ assert_iptables_called_with "-t mangle -D iptmon_tx -i wlan1 -s $test_ip"
+ assert_iptables_called_with "-t nat -D PREROUTING -p all -s $test_ip -i wlan1 -j RETURN"
+
+ # Verify 5 iptables calls were made
+ assert_call_count 5
+
+ teardown_mocks
+ echo "PASS: delete_user_iptable_rules with valid IP"
+}
+test_delete_rules_idempotent() {
+ echo "TEST: delete_user_iptable_rules is idempotent (rules don't exist)"
+ setup_mocks
+
+ local test_ip="192.168.1.101"
+
+ # Call twice - second call should still succeed (no-op)
+ delete_user_iptable_rules "$test_ip"
+ : > "$MOCK_DIR/iptables_calls.log" # Clear log
+
+ delete_user_iptable_rules "$test_ip"
+ local exit_code=$?
+
+ if [[ $exit_code -ne 0 ]]; then
+ echo "FAIL: Second deletion should succeed (idempotent)"
+ teardown_mocks
+ return 1
+ fi
+
+ teardown_mocks
+ echo "PASS: delete_user_iptable_rules is idempotent"
+}
+test_delete_rules_with_empty_ip() {
+ echo "TEST: delete_user_iptable_rules with empty IP"
+ setup_mocks
+
+ # Call with empty IP - should still not crash
+ delete_user_iptable_rules ""
+ local exit_code=$?
+
+ # Function should complete (errors logged)
+ if [[ $exit_code -ne 0 ]]; then
+ echo "FAIL: Should handle empty IP gracefully"
+ teardown_mocks
+ return 1
+ fi
+
+ teardown_mocks
+ echo "PASS: delete_user_iptable_rules handles empty IP"
+}
+test_delete_rules_logs_errors() {
+ echo "TEST: delete_user_iptable_rules logs errors to error.log"
+ setup_mocks
+
+ # Use IP that mock will reject
+ local test_ip="192.168.1.999"
+ rm -f error.log
+
+ delete_user_iptable_rules "$test_ip"
+
+ # Check that error.log was written to
+ if [[ ! -f error.log ]]; then
+ # Note: This may fail if iptables mock doesn't write stderr properly
+ echo "WARN: error.log not created (expected if mock stderr not captured)"
+ fi
+
+ teardown_mocks
+ echo "PASS: delete_user_iptable_rules error logging test completed"
+}
+#################
+# Run All Tests
+#################
+main() {
+ local passed=0
+ local failed=0
+
+ echo "========================================"
+ echo "delete_user_iptable_rules Test Suite"
+ echo "========================================"
+ echo
+
+ for test_func in test_delete_rules_with_valid_ip \
+ test_delete_rules_idempotent \
+ test_delete_rules_with_empty_ip \
+ test_delete_rules_logs_errors; do
+ if $test_func; then
+ ((++passed))
+ else
+ ((++failed))
+ fi
+ echo
+ done
+
+ echo "========================================"
+ echo "Results: $passed passed, $failed failed"
+ echo "========================================"
+
+ [[ $failed -eq 0 ]] && exit 0 || exit 1
+}
+main "$@"
\ No newline at end of file
diff --git a/Backend/test/fixtures/seed_sessions.sql b/Backend/test/fixtures/seed_sessions.sql
new file mode 100644
index 0000000..77b65ac
--- /dev/null
+++ b/Backend/test/fixtures/seed_sessions.sql
@@ -0,0 +1,20 @@
+-- Fixture: Seed Internet Sessions
+-- Based on CyberCafe_Database_Schema.sql
+
+INSERT INTO users (user_id, username, password, user_level, lane_id, status) VALUES
+(101, 'testuser1', 'pass', 1, 1, 'ACTIVE'),
+(102, 'testuser2', 'pass', 1, 1, 'ACTIVE');
+
+
+-- Active Session 1
+INSERT INTO internet_sessions (table_index, user_id, session_id, ip, session_tx, session_rx, session_access, datetime_created, datetime_sinceLastRequest, pending_deletion) VALUES
+(0, 101, 'sess1', '192.168.1.101', 500, 1000, 1, '2023-01-01 10:00:00', '2023-01-01 10:05:00', 0);
+
+-- Active Session 2 (Pending Deletion)
+INSERT INTO internet_sessions (table_index, user_id, session_id, ip, session_tx, session_rx, session_access, datetime_created, datetime_sinceLastRequest, pending_deletion) VALUES
+(1, 102, 'sess2', '192.168.1.102', 200, 400, 1, '2023-01-01 11:00:00', '2023-01-01 11:05:00', 1);
+
+-- Initial User Data Usage (Required for remove_session logic)
+INSERT INTO user_data_usage (user_id, session_number, session_entry_index, entry_datetime, interval_bytes_tx, interval_bytes_rx) VALUES
+(101, 1, 0, '2023-01-01 10:00:00', 0, 0),
+(102, 1, 0, '2023-01-01 11:00:00', 0, 0);
diff --git a/Backend/test/helpers/assert.sh b/Backend/test/helpers/assert.sh
new file mode 100644
index 0000000..4d3f091
--- /dev/null
+++ b/Backend/test/helpers/assert.sh
@@ -0,0 +1,79 @@
+#!/usr/bin/env bash
+
+# Simple assertion helper functions for bash test scripts
+# Each assertion prints a clear message and exits non-zero on failure
+# Intended to be used with: set -Eeuo pipefail
+# Usage:
+# - Source this file in test scripts: source ./assert.sh
+
+# Print a failure message and exit
+_assert_fail() {
+ echo "ASSERTION FAILED: $*" >&2
+ exit 1
+}
+
+# Assert that two strings are equal
+# Usage: assert_equal "expected" "$actual"
+assert_equal() {
+ local expected="$1"
+ local actual="$2"
+ local msg="${3:-Expected '$expected', but got '$actual'}"
+
+ [[ "$expected" == "$actual" ]] || _assert_fail "$msg"
+}
+
+# Assert two strings are NOT equal
+# Usage: assert_not_equal "not_expected" "$actual"
+assert_not_equal() {
+ local not_expected="$1"
+ local actual="$2"
+ local msg="${3:-Did not expect '$not_expected', but got it}"
+
+ [[ "$not_expected" != "$actual" ]] || _assert_fail "$msg"
+}
+
+# Assert a string contains a substring
+# Usage: assert_contains "$string" "substring"
+assert_contains() {
+ local string="$1"
+ local substring="$2"
+ local msg="${3:-Expected '$string' to contain '$substring'}"
+
+ [[ "$string" == *"$substring"* ]] || _assert_fail "$msg"
+}
+
+# Assert a command/status succeeded (exit code 0
+# Usage: assert_success command "$status"
+assert_success() {
+ local status="$1"
+ local msg="${2:-Expected success (exit code 0), but got $status}"
+
+ [[ "$status" -eq 0 ]] || _assert_fail "$msg"
+}
+
+# Assert a command/status failed (non-zero exit code)
+# Usage: assert_failure "$status"
+assert_failure() {
+ local status="$1"
+ local msg="${2:-Expected failure (non-zero exit code), but got $status}"
+
+ [[ "$status" -ne 0 ]] || _assert_fail "$msg"
+}
+
+# Assert that a file exists
+# Usage: assert_file_exists "filepath"
+assert_file_exists() {
+ local filepath="$1"
+ local msg="${2:-Expected file '$filepath' to exist, but it does not}"
+
+ [[ -f "$filepath" ]] || _assert_fail "$msg"
+}
+
+# Assert that a directory exists
+# Usage: assert_dir_exists "dirpath"
+assert_dir_exists() {
+ local dirpath="$1"
+ local msg="${2:-Expected directory '$dirpath' to exist, but it does not}"
+
+ [[ -d "$dirpath" ]] || _assert_fail "$msg"
+}
diff --git a/Backend/test/helpers/env.sh b/Backend/test/helpers/env.sh
new file mode 100644
index 0000000..ad2c712
--- /dev/null
+++ b/Backend/test/helpers/env.sh
@@ -0,0 +1,63 @@
+#!/usr/bin/env bash
+
+# Test environment bootstrap for CyberCafe Backend tests
+# Goals:
+# - isolate tests in a temp directory
+# - support PATH-based mocking (fakebin)
+# - set safe defaults (DRY_RUN=true)
+# - avoid relying on current working directory
+# - cleanup automatically unless KEEP_TEST_TMP=1
+
+# Usage:
+# - Source this file in test scripts: source ./env.sh
+
+set -Eeuo pipefail
+
+# Resolve Backend/ directory from this file's location
+# env.sh lives in Backend/test/helpers/
+BACKEND_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+TEST_DIR="$BACKEND_DIR/test"
+
+# Provide repo root too (one level up from Backend/)
+REPO_ROOT_DIR="$(cd "$BACKEND_DIR/.." && pwd)"
+
+export CYBERCAFE_BACKEND_DIR="$BACKEND_DIR"
+export CYBERCAFE_TEST_DIR="$TEST_DIR"
+export CYBERCAFE_REPO_ROOT_DIR="$REPO_ROOT_DIR"
+
+# Create a per-test temp directory (one sandbox per test process)
+TEST_TMPDIR="${TEST_TMPDIR:-$(mktemp -d "${TMPDIR:-/tmp}/cybercafe_test.XXXXXX")}"
+export TEST_TMPDIR
+
+# Where our mock binaries live (prepared to PATH)
+FAKEBIN="$TEST_TMPDIR/fakebin"
+mkdir -p "$FAKEBIN"
+export FAKEBIN
+export PATH="$FAKEBIN:$PATH"
+
+# Common scratch locations (tests can use these by default)
+TEST_LOGDIR="$TEST_TMPDIR/logs"
+mkdir -p "$TEST_LOGDIR"
+export TEST_LOGDIR
+
+# Safe defaults for scripts under test (override in a test if needed)
+export DRY_RUN="${DRY_RUN:-true}"
+export HS_INTERFACE="${HS_INTERFACE:-wlan0}"
+
+# Default status/state paths (override per-test if scripts require different variables)
+export STATUS_PATH="${STATUS_PATH:-$TEST_TMPDIR/status}"
+: > "$STATUS_PATH" || true # create empty status file
+
+# Optional: capture all mock command invocations in one place if mocks choose to use it
+export MOCK_CALL_LOG="${MOCK_CALL_LOG:-$TEST_TMPDIR/mock_calls.log}"
+: > "$MOCK_CALL_LOG" || true # create empty mock call log
+
+# Cleanup unless user explicitly asks to keep temp directorys for debugging
+cleanup_test_env() {
+ if [[ "${KEEP_TEST_TMP:-0}" == "1" ]]; then
+ echo "NOTE: KEEP_TEST_TMP=1 set; preserving TEST_TMPDIR: $TEST_TMPDIR" >&2
+ return 0
+ fi
+ rm -rf "$TEST_TMPDIR" 2>/dev/null || true
+}
+trap cleanup_test_env EXIT
\ No newline at end of file
diff --git a/Backend/test/integration/preflight_t95.sh b/Backend/test/integration/preflight_t95.sh
new file mode 100644
index 0000000..7118619
--- /dev/null
+++ b/Backend/test/integration/preflight_t95.sh
@@ -0,0 +1,109 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# T95 Preflight Validater
+# Purpose: Verify the T95 is in a known ready state before running system level tests.
+# Exit Codes:
+# 0 - Ready
+# 2 - Not Ready
+
+log() {
+ printf "%s\n" "$*";
+}
+warn() {
+ printf "WARNING: %s\n" "$*" >&2;
+}
+die() {
+ printf "ERROR: %s\n" "$*" >&2; exit 2;
+}
+
+require_root() {
+ # On Andriod/Termux, EUID may not be set consistently, id -u is unreliable
+ local uid
+ uid="$(id -u)"
+ if [[ "$uid" -ne 0 ]]; then
+ echo "ERROR: This script must be run as root."
+ echo "Run the following:"
+ echo ""
+ echo " su"
+ echo " /data/data/com.termux/files/usr/bin/bash Backend/test/integration/preflight_t95.sh"
+ echo ""
+ die "Must run as root"
+ fi
+}
+
+have_cmd() {
+ command -v "$1" >/dev/null 2>&1;
+}
+
+require_cmd_or_bin() {
+ local cmd="$1" bin="$2"
+ if have_cmd "$cmd"; then
+ return 0
+ fi
+ [[ -x "$bin" ]] || die "Missing required command/binary: $cmd ($bin not found)"
+}
+
+check_iface() {
+ local iface="$1"
+ ip link show "$iface" >/dev/null 2>&1 || die "Expected network interface not found: $iface"
+ # Warn if the interface is not up, but don't fail since it may be expected to be down
+ if ! ip link show "$iface" | grep -q "state UP"; then
+ warn "Network interface $iface exists butis not up"
+ fi
+}
+
+check_iptables_access() {
+ # Confirms iptables works and tables are readable (permissions + binary sanity)
+ iptables -S >/dev/null 2>&1 || die "iptables filter table not readable (permission or binary issue)"
+ iptables -t nat -S >/dev/null 2>&1 || die "iptables nat table not readable (permission or binary issue)"
+}
+
+check_tc_access() {
+ # Confirms tc works and qdisc can be read (permissions + binary sanity)
+ tc qdisc show dev eth0 >/dev/null 2>&1 || die "tc cannot read qdisc for eth0"
+}
+
+check_connectivity() {
+ # Outbound internet is useful for installs/updates; tests might not require it.
+ # We'll warn if missing rather than fail hard
+ if command -v ping >/dev/null 2>&1; then
+ if ! ping -c 1 -W 2 8.8.8.8 >/dev/null 2>&1; then
+ warn "Outbound connectivity ping to 8.8.8.8 failed"
+ fi
+ else
+ warn "ping not available; skipping outbound connectivity check"
+ fi
+}
+
+warn_if_no_hotspot_iface() {
+ # We do NOT fail here because baseline may not have hotspot enabled yet.
+ # We just warn that hotspot dependent tests are gated.
+ if ip link show wlan0 >/dev/null 2>&1 || ip link show ap0 >/dev/null 2>&1; then
+ return 0
+ fi
+ warn "No obvious hotspot/Wi-Fi interface found (wlan0/ap0). Hotspot dependent tests are gated."
+}
+
+
+main() {
+ log "=== T95 Preflight Checks ==="
+ require_root
+ # Required commands for CyberCafe integration readiness
+ require_cmd_or_bin ip /system/bin/ip
+ require_cmd_or_bin ss /system/bin/ss
+ require_cmd_or_bin iptables /system/bin/iptables
+ require_cmd_or_bin tc /system/bin/tc
+ require_cmd_or_bin sqlite3 /system/bin/sqlite3
+
+ check_iface "eth0"
+ check_iptables_access
+ check_tc_access
+ check_connectivity
+ warn_if_no_hotspot_iface
+
+ log "=== Preflight PASSED: T95 appears integration ready ==="
+ exit 0
+}
+
+main "$@"
diff --git a/Backend/test/integration/restore_t95_baseline.sh b/Backend/test/integration/restore_t95_baseline.sh
new file mode 100644
index 0000000..d479ab2
--- /dev/null
+++ b/Backend/test/integration/restore_t95_baseline.sh
@@ -0,0 +1,151 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# restore_t95_baseline.sh
+#
+# Safely restore the T95 to a baseline-ish state after CyberCafe testing.
+# Only removes CyberCafe-owned artifacts.
+# Does NOT flush or modify Android-managed chains (bw_*, fw_*, tetherctrl_*, oem_*).
+
+log() { printf "%s\n" "$*"; }
+warn() { printf "WARNING: %s\n" "$*" >&2; }
+die() { printf "ERROR: %s\n" "$*" >&2; exit 2; }
+
+require_root() {
+ [[ "$(id -u)" -eq 0 ]] || die "Must run as root (use: su)"
+}
+
+# Ensure Android system binaries are reachable when running under su
+export PATH="/system/bin:/system/xbin:$PATH"
+
+delete_rule_exact() {
+ local table="$1"
+ local rule="$2"
+ local del="${rule/-A /-D }"
+ iptables -t "$table" $del >/dev/null 2>&1 || true
+}
+
+delete_jump_rules_to_chain() {
+ local table="$1"
+ local chain="$2"
+
+ while IFS= read -r r; do
+ [[ -z "$r" ]] && continue
+ delete_rule_exact "$table" "$r"
+ log "Deleted jump to ${chain}: (table=$table)"
+ done < <(
+ iptables -t "$table" -S 2>/dev/null \
+ | grep -E -- "-j ${chain}(\s|$)" || true
+ )
+}
+
+flush_delete_chain() {
+ local table="$1"
+ local chain="$2"
+
+ iptables -t "$table" -F "$chain" >/dev/null 2>&1 || return 0
+ iptables -t "$table" -X "$chain" >/dev/null 2>&1 || true
+ log "Removed chain: $chain (table=$table)"
+}
+
+delete_prefixed_chains() {
+ local table="$1"
+ local prefix="$2"
+
+ while IFS= read -r ch; do
+ [[ -z "$ch" ]] && continue
+ delete_jump_rules_to_chain "$table" "$ch"
+ flush_delete_chain "$table" "$ch"
+ done < <(
+ iptables -t "$table" -S 2>/dev/null \
+ | awk '$1=="-N"{print $2}' \
+ | grep -E "^${prefix}" || true
+ )
+}
+
+delete_cybercafe_direct_rules() {
+ local hs_if="${HS_INTERFACE:-}"
+ local local_ip="${LOCAL_IP:-}"
+
+ if [[ -z "$hs_if" ]]; then
+ warn "HS_INTERFACE not set; skipping DNAT/DROP rule cleanup"
+ return 0
+ fi
+
+ # Remove nat PREROUTING DNAT rules to :80 on HS_INTERFACE
+ while IFS= read -r r; do
+ [[ -z "$r" ]] && continue
+ delete_rule_exact nat "$r"
+ log "Deleted nat PREROUTING rule on ${hs_if}"
+ done < <(
+ iptables -t nat -S 2>/dev/null \
+ | grep -E '^-A PREROUTING ' \
+ | grep -E -- "-i ${hs_if} " \
+ | grep -E ':80(\s|$)' || true
+ )
+
+ # Remove filter FORWARD DROP rules involving HS_INTERFACE
+ while IFS= read -r r; do
+ [[ -z "$r" ]] && continue
+ delete_rule_exact filter "$r"
+ log "Deleted filter FORWARD DROP rule on ${hs_if}"
+ done < <(
+ iptables -t filter -S 2>/dev/null \
+ | grep -E '^-A FORWARD ' \
+ | grep -E -- "(-i ${hs_if} |-o ${hs_if} )" \
+ | grep -E ' -j DROP(\s|$)' || true
+ )
+}
+
+restore_tc_best_effort() {
+ local hs_if="${HS_INTERFACE:-}"
+ [[ -z "$hs_if" ]] && return 0
+
+ if ip link show "$hs_if" >/dev/null 2>&1; then
+ tc qdisc del dev "$hs_if" root >/dev/null 2>&1 || true
+ log "Deleted tc root qdisc on ${hs_if} (best-effort)"
+ fi
+}
+
+main() {
+ require_root
+
+ log "=== Restoring T95 baseline-ish state ==="
+ log "HS_INTERFACE=${HS_INTERFACE:-} LOCAL_IP=${LOCAL_IP:-}"
+
+ # Stop captive portal (if running)
+ pkill lighttpd >/dev/null 2>&1 || true
+ log "Stopped lighttpd (best-effort)"
+
+ # Remove status file if known
+ if [[ -n "${STATUS_PATH:-}" && -e "${STATUS_PATH}" ]]; then
+ rm -f "${STATUS_PATH}" || true
+ log "Removed STATUS_PATH"
+ fi
+
+ # Remove mangle accounting chains
+ delete_jump_rules_to_chain mangle iptmon_tx
+ delete_jump_rules_to_chain mangle iptmon_rx
+ flush_delete_chain mangle iptmon_tx
+ flush_delete_chain mangle iptmon_rx
+
+ # Remove mirror + user chains
+ delete_prefixed_chains filter "CYBERCAFE-MIRROR-"
+ delete_prefixed_chains nat "CYBERCAFE-MIRROR-"
+ delete_prefixed_chains mangle "CYBERCAFE-MIRROR-"
+
+ delete_prefixed_chains filter "cybercafe-user-"
+ delete_prefixed_chains nat "cybercafe-user-"
+ delete_prefixed_chains mangle "cybercafe-user-"
+
+ # Remove direct rules (DNAT + DROP)
+ delete_cybercafe_direct_rules
+
+ # Remove traffic shaping
+ restore_tc_best_effort
+
+ log "=== Restore complete ==="
+ log "Run preflight again to confirm known-ready state."
+}
+
+main "$@"
diff --git a/Backend/test/lifecycle_e2e_test.sh b/Backend/test/lifecycle_e2e_test.sh
new file mode 100755
index 0000000..3cb5c0f
--- /dev/null
+++ b/Backend/test/lifecycle_e2e_test.sh
@@ -0,0 +1,77 @@
+#!/usr/bin/env bash
+set -e
+
+echo "======================================="
+echo "CyberCafe Lifecycle E2E Test"
+echo "======================================="
+
+LOG_DIR="/tmp/cybercafe_logs"
+mkdir -p "$LOG_DIR"
+
+# ---------------------------------------
+# STEP 1 — Start Infrastructure
+# ---------------------------------------
+echo "[STEP 1] Starting infrastructure..."
+
+bash Backend/cybercafe.sh run > "$LOG_DIR/run.log" 2>&1 &
+CYBER_PID=$!
+
+sleep 3
+
+RUN_LOG=$(cat "$LOG_DIR/run.log")
+echo "$RUN_LOG"
+
+if echo "$RUN_LOG" | grep -qi "started"; then
+ echo "[OK] Infrastructure start command executed"
+else
+ echo "[ERROR] Infrastructure did not start correctly"
+ exit 1
+fi
+
+# ---------------------------------------
+# STEP 2 — Check Status
+# ---------------------------------------
+echo "[STEP 2] Checking system status..."
+
+STATUS_OUTPUT=$(bash Backend/cybercafe.sh status 2>&1 || true)
+echo "$STATUS_OUTPUT"
+
+if echo "$STATUS_OUTPUT" | grep -q "Status"; then
+ echo "[OK] Status command executed"
+else
+ echo "[WARNING] Status output unclear"
+fi
+
+# ---------------------------------------
+# STEP 3 — System Interaction
+# ---------------------------------------
+echo "[STEP 3] Validating system interaction..."
+
+LIST_OUTPUT=$(bash Backend/cybercafe.sh list users 2>&1 || true)
+echo "$LIST_OUTPUT"
+
+if echo "$LIST_OUTPUT" | grep -qi "no such table"; then
+ echo "[WARNING] Database schema not initialized (expected in this environment)"
+elif echo "$LIST_OUTPUT" | grep -qi "Error"; then
+ echo "[WARNING] User operations returned an error"
+else
+ echo "[OK] System interaction successful"
+fi
+
+# ---------------------------------------
+# STEP 4 — Shutdown
+# ---------------------------------------
+echo "[STEP 4] Shutting down infrastructure..."
+
+bash Backend/cybercafe.sh shutdown > "$LOG_DIR/shutdown.log" 2>&1 || true
+
+kill $CYBER_PID 2>/dev/null || true
+
+echo "[OK] Shutdown completed"
+
+# ---------------------------------------
+# FINAL
+# ---------------------------------------
+echo "======================================="
+echo "Lifecycle test completed"
+echo "======================================="
\ No newline at end of file
diff --git a/Backend/test/run.sh b/Backend/test/run.sh
new file mode 100755
index 0000000..099bc24
--- /dev/null
+++ b/Backend/test/run.sh
@@ -0,0 +1,138 @@
+#!/usr/bin/env bash
+set -Eeuo pipefail
+# -E: The ERR trap is inherited by shell functions.
+# -e: Exit immediately if a command exits with a non-zero status.
+# -u: Treat unset variables as an error when substituting.
+# -o pipefail: the return value of a pipeline is the status of
+
+# Resovle repo root (Backend/) regardless of where this script is called from
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+TEST_DIR="$ROOT_DIR/test"
+
+usage() {
+ cat << 'EOF'
+Usage:
+ test/run.sh Run all tests
+ test/run.sh path/to/test.sh Run a specific test script
+ test/run.sh --filter text Run tests whose path contains 'text'
+EOF
+}
+
+FILTER=""
+SINGLE_FILE=""
+
+# Minimal argument parsing
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ -h|--help) usage; exit 0;;
+ --filter) FILTER="${2:-}"; shift 2;; # next argument is the filter string
+ *) SINGLE_FILE="$1"; shift;; # treat any other arg as a file path
+ esac
+done
+
+# Collect tests into an array safely (handles spaces in paths)
+declare -a TESTS=()
+
+# If a file is specified, run only that file
+if [[ -n "$SINGLE_FILE" ]]; then
+ # Allow file path relative to repo root or absolute
+ if [[ -f "$SINGLE_FILE" ]]; then
+ TESTS+=("$SINGLE_FILE")
+ elif [[ -f "$ROOT_DIR/$SINGLE_FILE" ]]; then
+ TESTS+=("$ROOT_DIR/$SINGLE_FILE")
+ else
+ echo "ERROR: Test file not found: $SINGLE_FILE" >&2
+ exit 2
+ fi
+else
+ if [[ ! -d "$TEST_DIR" ]]; then
+ echo "ERROR: Test directory not found: $TEST_DIR" >&2
+ exit 2
+ fi
+ # Find tests named *_test.sh under Backend/test/
+ # mapfile reads lines into an array without splitting on spaces
+ TESTS=()
+ TMP_FILE="$(mktemp)"
+
+ {
+ find "$TEST_DIR" -type f -name "*_test.sh"
+ find "$TEST_DIR" -type f -name "*.bats"
+ } | sort > "$TMP_FILE"
+
+ while IFS= read -r file; do
+ TESTS+=("$file")
+ done < "$TMP_FILE"
+
+ rm -f "$TMP_FILE"
+fi
+
+# Optional filter (still safe: operates line-by-line)
+if [[ -n "$FILTER" ]]; then
+ declare -a FILTERED=()
+ for t in "${TESTS[@]}"; do
+ if [[ "$t" == *"$FILTER"* ]]; then
+ FILTERED+=("$t")
+ fi
+ done
+ TESTS=("${FILTERED[@]}")
+fi
+
+if (( ${#TESTS[@]} == 0 )); then
+ echo "No tests found."
+ exit 0
+fi
+
+# run a single test based on file type
+run_one_test() {
+ local t="$1"
+ case "$t" in
+ *.bats)
+ if command -v bats >/dev/null 2>&1; then
+ bats "$t"
+ else
+ echo "ERROR: Found .bats test but bats is not installed:" >&2
+ echo " ${t#"$ROOT_DIR"/}" >&2
+ echo "Install bats or convert this test to *_test.sh" >&2
+ return 2
+ fi
+ ;;
+ *)
+ bash "$t"
+ ;;
+ esac
+}
+
+pass=0
+fail=0
+failures=()
+
+echo "Running tests..."
+echo "================"
+
+# Run each test script in its own Bash process
+# Pass/fail is determined by the script's exit code (0=pass, non-0=fail)
+for t in "${TESTS[@]}"; do
+ echo "Running test: $t"
+ if run_one_test "$t"; then
+ echo "PASS: ${t#"$ROOT_DIR"/}"
+ pass=$((pass + 1))
+ else
+ rc=$?
+ echo "FAIL: ${t#"$ROOT_DIR"/} (exit=$rc)"
+ fail=$((fail + 1))
+ failures+=("${t#"$ROOT_DIR"/}")
+ fi
+ echo "----------------"
+done
+
+echo
+echo "Summary: $pass passed, $fail failed."
+
+# If anything failed, exit non-zero so CI fails
+if (( fail > 0 )); then
+ echo "Failed tests:"
+ for f in "${failures[@]}"; do
+ echo " - $f"
+ done
+ exit 1
+fi
\ No newline at end of file
diff --git a/Backend/test/smoke_test.sh b/Backend/test/smoke_test.sh
new file mode 100644
index 0000000..c728930
--- /dev/null
+++ b/Backend/test/smoke_test.sh
@@ -0,0 +1,94 @@
+#!/usr/bin/env bash
+
+# Purpose:
+# - Fast “sanity” test that Cybercafe_setupFunctions.sh can be sourced and key functions run
+# - Runs shutdown_infrastructure in DRY_RUN mode using PATH mocks (FAKEBIN)
+# - Verifies expected output + proves mocked system commands were invoked
+#
+# This test MUST NOT touch real system state (no real iptables/tc changes).
+# It is safe to run on a dev machine and in CI.
+
+set -Eeuo pipefail
+
+# Example to demonstrate usage of helpers in a smoke test:
+# - env.sh gives us a per-test temp directory + mockable PATH (FAKEBIN) + safe defaults
+# - assert.sh gives us simple assertion functions with clear failure messages (like unit tests)
+
+# shellcheck source=test/helpers/assert.sh
+source "$(dirname "$0")/helpers/env.sh"
+
+# shellcheck source=test/helpers/assert.sh
+source "$(dirname "$0")/helpers/assert.sh"
+
+# Source functions first (some scripts reset PATH)
+source "$CYBERCAFE_BACKEND_DIR/Cybercafe_setupFunctions.sh"
+
+# Re-apply our FAKEBIN at the fron of PATH in case sourcing reset it
+export PATH="$FAKEBIN:$PATH"
+
+# Create lightweight mocks so the test never touches real system state
+# The mock commands log what was called and what succeed
+cat > "$FAKEBIN/iptables" <<'EOF'
+#!/usr/bin/env bash
+echo "iptables $*" >> "$MOCK_CALL_LOG"
+exit 0
+EOF
+chmod +x "$FAKEBIN/iptables"
+
+cat > "$FAKEBIN/tc" <<'EOF'
+#!/usr/bin/env bash
+echo "tc $*" >> "$MOCK_CALL_LOG"
+exit 0
+EOF
+chmod +x "$FAKEBIN/tc"
+
+cat > "$FAKEBIN/ip" <<'EOF'
+#!/usr/bin/env bash
+echo "ip $*" >> "$MOCK_CALL_LOG"
+# Pretend interface exists so step 6 doesn't log "Device does not exist"
+exit 0
+EOF
+chmod +x "$FAKEBIN/ip"
+
+cat > "$FAKEBIN/pkill" <<'EOF'
+#!/usr/bin/env bash
+echo "pkill $*" >> "$MOCK_CALL_LOG"
+exit 0
+EOF
+chmod +x "$FAKEBIN/pkill"
+
+cat > "$FAKEBIN/ifconfig" <<'EOF'
+#!/usr/bin/env bash
+echo "ifconfig $*" >> "$MOCK_CALL_LOG"
+# Output that matches the grep/awk pipeline in shutdown_infrastructure
+echo "inet addr:192.168.1.50 Bcast:192.168.1.255 Mask:255.255.255.0"
+exit 0
+EOF
+chmod +x "$FAKEBIN/ifconfig"
+
+# Safe defaults for this test
+export DRY_RUN=true
+export HS_INTERFACE=wlan0
+export STATUS_PATH="$TEST_TMPDIR/status"
+: > "$STATUS_PATH"
+
+
+test_shutdown() {
+ output="$(shutdown_infrastructure 2>&1 || true)"
+
+ # Contract: banner prints
+ assert_contains "$output" "Beginning shutdown_infrastructure (dry-run=true)"
+
+ # Contract: in DRY_RUN, commands are printed (not executed)
+ assert_contains "$output" "pkill lighttpd"
+
+ # Safety: prove we used mocks for commands that are executed even in DRY_RUN
+ # (iptables -L/-S and ifconfig and ip are called outside run_cmd)
+ calls="$(cat "$MOCK_CALL_LOG")"
+ assert_contains "$calls" "iptables -t mangle -L"
+ assert_contains "$calls" "iptables -t nat -S"
+ assert_contains "$calls" "ifconfig wlan0"
+ assert_contains "$calls" "ip link show wlan0"
+}
+
+test_shutdown
\ No newline at end of file
diff --git a/Backend/test/start_captive_webserver_test.bats b/Backend/test/start_captive_webserver_test.bats
new file mode 100755
index 0000000..72ae17a
--- /dev/null
+++ b/Backend/test/start_captive_webserver_test.bats
@@ -0,0 +1,417 @@
+#!/usr/bin/env bats
+
+# ---------------------------------------------------------------------------
+# Organization: Grey-box
+# Project: Cybercafe
+# File: start_captive_webserver_test.bats
+# Description: Automated unit testing for start_captive_webserver() defined
+# in Cybercafe_setupFunctions.sh.
+#
+# This file covers:
+# - Category 1: Variable guard precondition checks (T1-T6)
+# - Category 2: Path validation checks (T7-T11)
+# - Category 3: Happy-path / successful start (T12-T16)
+# - Category 4: Idempotency (T17-T20)
+# - Category 5: Error log format and timestamp (T21-T23)
+# - Category 6: Edge and boundary cases (T24-T25)
+#
+# Usage: bats test/start_captive_webserver_test.bats
+# ---------------------------------------------------------------------------
+
+# Automated unit testing for start_captive_webserver() defined in Cybercafe_setupFunctions().sh.
+# This BATs testing file covers precondition guards, idempotency, process lifecycle, error logging, and returning exit codes.
+
+# setup() — runs automatically before EVERY test
+#
+# Creates fresh isolated temp directories for each test so:
+# - No test pollutes another test's state
+# - error.log writes are captured per-test inside TEST_DIR
+# - The lighttpd stub is a fake executable we fully control
+# - The lighttpd conf is a dummy file that simply exists
+#
+# After sourcing the real implementation, we cd into TEST_DIR so that
+# the function's relative "error.log" writes land in our isolated directory
+# rather than the project root.
+
+setup() {
+ # Create an isolated temp directory for every test.
+ # export ensures these variables survive into each @test subshell.
+ export TEST_DIR="$(mktemp -d)"
+ export ERROR_LOG="${TEST_DIR}/error.log"
+
+ # Create fake lighttpd executable.
+ # Uses a while loop so the process stays alive and pgrep can find it
+ # by full path for idempotency tests.
+ export LIGHTTPD_PATH="${TEST_DIR}/lighttpd"
+ cat > "${LIGHTTPD_PATH}" <<'EOF'
+#!/bin/bash
+while true; do sleep 1; done
+EOF
+ chmod +x "${LIGHTTPD_PATH}"
+
+ # Fake lighttpd config file — function only checks it exists with -f
+ export LIGHTTPD_CONF="${TEST_DIR}/lighttpd.conf"
+ touch "${LIGHTTPD_CONF}"
+
+ # Override pgrep so Tests 18 and 19 pass on macOS, since pgrep lighttpd doesn't match the bash stub
+ pgrep() {
+ if [[ "$1" == "lighttpd" ]]; then
+ command pgrep -f "${LIGHTTPD_PATH}"
+ else
+ command pgrep "$@"
+ fi
+ }
+
+ # Source the real implementation so its functions are available to tests.
+ # BATS_TEST_DIRNAME is the test/ directory, implementation is one level up.
+ # shellcheck source=../Cybercafe_setupFunctions.sh
+ source "${BATS_TEST_DIRNAME}/../Cybercafe_setupFunctions.sh"
+
+ # cd into TEST_DIR so the function's relative "error.log" writes land here.
+ cd "${TEST_DIR}" || exit
+
+ # Placeholder for tracking background PIDs if needed in future tests
+ SPAWNED_PIDS=()
+}
+
+teardown() {
+ # Kill any lighttpd stubs left running
+ pkill -f "${TEST_DIR}/lighttpd" 2>/dev/null || true
+
+ # Remove temp directory
+ rm -rf "${TEST_DIR}"
+}
+
+# ---------------------------------------------------------------------------
+# Helper: make a stub that exits with a given code
+# ---------------------------------------------------------------------------
+make_lighttpd_stub() {
+ local exit_code="${1:-0}"
+ cat > "${LIGHTTPD_PATH}" <".
+@test "[TEST 16] Lighttpd is invoked with the correct config file path" {
+ local invocation_log="${TEST_DIR}/invocation.log"
+
+ cat > "${LIGHTTPD_PATH}" <> "${invocation_log}"
+exit 0
+EOF
+ chmod +x "${LIGHTTPD_PATH}"
+
+ start_captive_webserver
+
+ # macOS background processes might take a split second to launch the stub, so give it a tiny delay
+ sleep 1
+
+ # Check the log was actually created before grepping it
+ [ -f "${invocation_log}" ] || { echo "invocation log not created" >&3; false; }
+ grep -q "\-f ${LIGHTTPD_CONF}" "${invocation_log}"
+}
+
+# ---------------------------------------------------------------------------
+# Category 4 – Idempotency tests (TC-17 – TC-20)
+# ---------------------------------------------------------------------------
+
+# Calling the function a second time while the server is running should
+# still return 0 — the function treats this as a non-error condition.
+@test "[TEST 17] Returns 0 on second call when server is already running" {
+ start_captive_webserver
+ sleep 0.2
+ run start_captive_webserver
+ [ "$status" -eq 0 ]
+}
+
+# When the function detects lighttpd is already running, it must log
+# an "already running" message so operators can confirm the idempotency
+# path fired rather than a fresh start occurring.
+@test "[TEST 18] Logs 'already running' message on second call" {
+ # Stub loops forever so pgrep -f can find it by full path
+ cat > "${LIGHTTPD_PATH}" <<'EOF'
+#!/bin/bash
+while true; do sleep 1; done
+EOF
+ chmod +x "${LIGHTTPD_PATH}"
+
+ # Pre-launch the stub so it's already "running" before the function checks
+ "${LIGHTTPD_PATH}" &
+ STUB_PID=$!
+ sleep 0.5
+
+ # Sanity check — confirm pgrep can see it before calling the function
+ pgrep -f "${LIGHTTPD_PATH}" > /dev/null || { echo "stub not found by pgrep" >&3; false; }
+
+ # Now call the function — it should detect the running process and skip launch
+ start_captive_webserver
+
+ # Confirm the idempotency log message was written
+ grep -q "already running" "${ERROR_LOG}"
+
+ # Clean up
+ kill "$STUB_PID" 2>/dev/null || true
+ wait "$STUB_PID" 2>/dev/null || true
+}
+
+# If lighttpd is already running, calling the function must not launch
+# a second instance. We count processes matching the stub path and
+# assert there is exactly 1.
+@test "[TEST 19] Does not spawn a second lighttpd process on second call" {
+ cat > "${LIGHTTPD_PATH}" <<'EOF'
+#!/bin/bash
+while true; do sleep 1; done
+EOF
+ chmod +x "${LIGHTTPD_PATH}"
+
+ "${LIGHTTPD_PATH}" &
+ STUB_PID=$!
+ sleep 0.5
+
+ start_captive_webserver # should detect already running, not spawn another
+
+ # Count processes matching our stub's full path.
+ # Using the full path avoids accidentally matching unrelated system processes.
+ # wc -l counts matching PIDs; tr removes any trailing whitespace.
+ count=$(pgrep -f "${LIGHTTPD_PATH}" | wc -l | tr -d '[:space:]')
+ [ "$count" -eq 1 ]
+
+ kill "$STUB_PID" 2>/dev/null || true
+ wait "$STUB_PID" 2>/dev/null || true
+
+}
+
+# Calling the function three times in a row must not produce any errors.
+# The final call's exit code is what `run` captures, and it must be 0.
+@test "[TEST 20] Function is callable multiple times without any errors" {
+ start_captive_webserver
+ sleep 0.2
+ run start_captive_webserver
+ run start_captive_webserver
+ # All calls should succeed
+ [ "$status" -eq 0 ]
+}
+
+# ---------------------------------------------------------------------------
+# Category 5 – Error logging format / timestamp tests (TC-21 – TC-23)
+# ---------------------------------------------------------------------------
+
+# Every error.log entry starts with `date -Is` output, which produces
+# an ISO-8601 format: YYYY-MM-DDTHH:MM:SS+HH:MM.
+# We trigger an error (unset LIGHTTPD_PATH) to force a log write,
+# then regex-match the expected timestamp format.
+@test "[TEST 21] Error log entries include an ISO-8601 timestamp" {
+ unset LIGHTTPD_PATH
+ start_captive_webserver || true
+ # date -Is format: YYYY-MM-DDTHH:MM:SS+HH:MM
+ grep -qE "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}" "${ERROR_LOG}"
+}
+
+# Log entries must name the file they came from ("Cybercafe_setupFunction")
+# so that in a merged log file from multiple scripts, the source is clear.
+@test "[TEST 22] Error log entries reference the correct script filename" {
+ unset LIGHTTPD_PATH
+ start_captive_webserver || true
+ grep -q "Cybercafe_setupFunction" "${ERROR_LOG}"
+}
+
+# If error.log does not exist yet when the function runs, the shell's
+# >> redirection operator must create it automatically.
+# We delete the file first to verify creation from scratch.
+@test "[TEST 23] Error log is created if it does not already exist" {
+ rm -f "${ERROR_LOG}"
+ unset LIGHTTPD_PATH
+ start_captive_webserver || true
+ [ -f "${ERROR_LOG}" ]
+
+}
+
+# ---------------------------------------------------------------------------
+# Category 6 – Edge / boundary cases (TC-24 – TC-25)
+# ---------------------------------------------------------------------------
+
+# An empty string for LIGHTTPD_CONF is a different failure mode from
+# an unset variable — the variable exists but is blank. The -z guard
+# must catch this and return 1 just as it would for an unset variable.
+@test "[TEST 24] Returns 1 when LIGHTTPD_CONF is set to empty string" {
+ LIGHTTPD_CONF=""
+ run start_captive_webserver
+ [ "$status" -eq 1 ]
+}
+
+# File paths with spaces in them must be handled correctly.
+# The function uses quoted variable expansions ("${LIGHTTPD_PATH}")
+# which is required to prevent word-splitting on spaces.
+# This test places both the stub and conf inside a directory named
+# "path with spaces" to confirm quoting works end-to-end.
+@test "[TEST 25] Paths containing spaces are handled correctly" {
+ SPACE_DIR="${TEST_DIR}/path with spaces"
+ mkdir -p "${SPACE_DIR}"
+
+ LIGHTTPD_PATH="${SPACE_DIR}/lighttpd"
+ cat > "${LIGHTTPD_PATH}" <<'EOF'
+#!/bin/bash
+exit 0
+EOF
+ chmod +x "${LIGHTTPD_PATH}"
+
+ LIGHTTPD_CONF="${SPACE_DIR}/lighttpd.conf"
+ touch "${LIGHTTPD_CONF}"
+
+ run start_captive_webserver
+ [ "$status" -eq 0 ]
+
+}
diff --git a/Backend/test/test_clear_internet_sessions.bats b/Backend/test/test_clear_internet_sessions.bats
new file mode 100644
index 0000000..8f31090
--- /dev/null
+++ b/Backend/test/test_clear_internet_sessions.bats
@@ -0,0 +1,381 @@
+#!/usr/bin/env bats
+
+# BATS test file for clear_internet_sessions
+# Run with: test/run.sh test/test_clear_internet_sessions.bats
+# or: bats test/test_clear_internet_sessions.bats
+#
+# Strategy:
+# - Real SQLite database (temp file per test)
+# - Mocked iptables (records calls, always succeeds)
+# - Source functions under test after environment is ready
+
+###############################################################################
+# Setup / Teardown
+###############################################################################
+
+setup() {
+ TEST_DIR="$(cd "$(dirname "${BATS_TEST_FILENAME}")" && pwd)"
+ BACKEND_DIR="$(cd "$TEST_DIR/.." && pwd)"
+ REPO_ROOT="$(cd "$BACKEND_DIR/.." && pwd)"
+
+ MOCKBIN="$TEST_DIR/mocks"
+ TMPDIR_TEST="$TEST_DIR/tmp"
+ SCHEMA_FILE="$REPO_ROOT/Database/CyberCafe_Database_Schema.sql"
+
+ rm -rf "$MOCKBIN" "$TMPDIR_TEST"
+ mkdir -p "$MOCKBIN" "$TMPDIR_TEST"
+
+ cat > "$MOCKBIN/iptables" <<'EOT'
+#!/usr/bin/env bash
+echo "$*" >> "${MOCKBIN}/iptables.log"
+exit 0
+EOT
+ chmod +x "$MOCKBIN/iptables"
+ : > "$MOCKBIN/iptables.log"
+
+ export DATABASE_PATH="$TMPDIR_TEST/test.db"
+ sqlite3 "$DATABASE_PATH" < "$SCHEMA_FILE"
+
+ export PATH="$MOCKBIN:$PATH"
+ export MOCKBIN
+ export HS_INTERFACE="wlan1"
+
+ source "$BACKEND_DIR/Cybercafe_internetSessionFunctions.sh"
+}
+
+teardown() {
+ rm -rf "$MOCKBIN" "$TMPDIR_TEST" error.log 2>/dev/null || true
+}
+
+###############################################################################
+# Helpers
+###############################################################################
+
+# Seed a single internet_session row
+# Usage: seed_session
+seed_session() {
+ sqlite3 "$DATABASE_PATH" \
+ "INSERT INTO internet_sessions VALUES($1,$2,'$3','$4',$5,$6,$7,'$8','$9',${10});"
+}
+
+# Seed a user (minimal fields needed)
+seed_user() {
+ sqlite3 "$DATABASE_PATH" \
+ "INSERT OR IGNORE INTO users (user_id,username,password,user_level,lane_id,status) VALUES($1,'user$1','pass',1,1,'ACTIVE');"
+}
+
+# Seed an initial user_data_usage row (PHP normally creates the first entry)
+seed_usage() {
+ sqlite3 "$DATABASE_PATH" \
+ "INSERT INTO user_data_usage (user_id,session_number,session_entry_index,entry_datetime,interval_bytes_tx,interval_bytes_rx) VALUES($1,$2,0,'$3',0,0);"
+}
+
+# Return row count for a table
+db_count() {
+ sqlite3 "$DATABASE_PATH" "SELECT COUNT(*) FROM $1;"
+}
+
+# Return row count for user_data_usage filtered by user_id
+usage_count_for_user() {
+ sqlite3 "$DATABASE_PATH" "SELECT COUNT(*) FROM user_data_usage WHERE user_id=$1;"
+}
+
+# Count lines in iptables mock log
+iptables_call_count() {
+ if [[ -f "$MOCKBIN/iptables.log" ]]; then
+ wc -l < "$MOCKBIN/iptables.log" | tr -d ' '
+ else
+ echo 0
+ fi
+}
+
+# Seed the "standard" two-session fixture used by several tests
+seed_standard_fixture() {
+ seed_user 101
+ seed_user 102
+ seed_session 0 101 sess1 "192.168.1.101" 500 1000 1 "2023-01-01 10:00:00" "2023-01-01 10:05:00" 0
+ seed_session 1 102 sess2 "192.168.1.102" 200 400 1 "2023-01-01 11:00:00" "2023-01-01 11:05:00" 1
+ seed_usage 101 1 "2023-01-01 10:00:00"
+ seed_usage 102 1 "2023-01-01 11:00:00"
+}
+
+###############################################################################
+# 1. Basic Deletion Tests
+###############################################################################
+
+@test "clear_internet_sessions: deletes all rows from internet_sessions" {
+ seed_standard_fixture
+ [ "$(db_count internet_sessions)" -eq 2 ]
+
+ clear_internet_sessions
+
+ [ "$(db_count internet_sessions)" -eq 0 ]
+}
+
+@test "clear_internet_sessions: each session row is individually deleted" {
+ seed_standard_fixture
+
+ clear_internet_sessions
+
+ # Verify specific rows are gone
+ local row0 row1
+ row0=$(sqlite3 "$DATABASE_PATH" "SELECT COUNT(*) FROM internet_sessions WHERE table_index=0;")
+ row1=$(sqlite3 "$DATABASE_PATH" "SELECT COUNT(*) FROM internet_sessions WHERE table_index=1;")
+ [ "$row0" -eq 0 ]
+ [ "$row1" -eq 0 ]
+}
+
+@test "clear_internet_sessions: handles single session" {
+ seed_user 200
+ seed_session 0 200 sessA "10.0.0.1" 100 200 1 "2023-06-01 08:00:00" "2023-06-01 08:05:00" 0
+ seed_usage 200 1 "2023-06-01 08:00:00"
+
+ clear_internet_sessions
+
+ [ "$(db_count internet_sessions)" -eq 0 ]
+}
+
+###############################################################################
+# 2. Archival / user_data_usage Tests
+###############################################################################
+
+@test "clear_internet_sessions: archives usage data to user_data_usage for each session" {
+ seed_standard_fixture
+
+ clear_internet_sessions
+
+ # Each user should have at least 2 rows in user_data_usage
+ # (1 seed + 1 archive entry from remove_session)
+ [ "$(usage_count_for_user 101)" -ge 2 ]
+ [ "$(usage_count_for_user 102)" -ge 2 ]
+}
+
+@test "clear_internet_sessions: archived interval_bytes_tx equals session_tx minus prior sum" {
+ seed_user 300
+ seed_session 0 300 sessX "10.0.0.3" 800 1600 1 "2023-02-01 12:00:00" "2023-02-01 12:05:00" 0
+ seed_usage 300 1 "2023-02-01 12:00:00"
+
+ clear_internet_sessions
+
+ # The archive entry should have interval_bytes_tx = 800 - 0 = 800
+ local archived_tx
+ archived_tx=$(sqlite3 "$DATABASE_PATH" \
+ "SELECT interval_bytes_tx FROM user_data_usage WHERE user_id=300 AND session_entry_index=1;")
+ [ "$archived_tx" -eq 800 ]
+}
+
+@test "clear_internet_sessions: archived interval_bytes_rx equals session_rx minus prior sum" {
+ seed_user 301
+ seed_session 0 301 sessY "10.0.0.4" 500 2000 1 "2023-02-01 13:00:00" "2023-02-01 13:05:00" 0
+ seed_usage 301 1 "2023-02-01 13:00:00"
+
+ clear_internet_sessions
+
+ local archived_rx
+ archived_rx=$(sqlite3 "$DATABASE_PATH" \
+ "SELECT interval_bytes_rx FROM user_data_usage WHERE user_id=301 AND session_entry_index=1;")
+ [ "$archived_rx" -eq 2000 ]
+}
+
+@test "clear_internet_sessions: does not create ghost entries when no prior usage exists" {
+ seed_user 400
+ # Session exists but NO user_data_usage seed → remove_session skips archival
+ seed_session 0 400 sessNoUsage "10.0.0.5" 100 200 1 "2023-03-01 09:00:00" "2023-03-01 09:05:00" 0
+
+ clear_internet_sessions
+
+ # Session should still be deleted
+ [ "$(db_count internet_sessions)" -eq 0 ]
+ # No usage rows should have been created (the RESPONSE != '' guard in remove_session)
+ [ "$(usage_count_for_user 400)" -eq 0 ]
+}
+
+###############################################################################
+# 3. Idempotency Tests
+###############################################################################
+
+@test "clear_internet_sessions: succeeds on empty database (no sessions)" {
+ # No sessions seeded at all
+ run clear_internet_sessions
+ [ "$status" -eq 0 ]
+ [ "$(db_count internet_sessions)" -eq 0 ]
+}
+
+@test "clear_internet_sessions: running twice is idempotent" {
+ seed_standard_fixture
+
+ clear_internet_sessions
+ [ "$(db_count internet_sessions)" -eq 0 ]
+
+ # Second run should still succeed
+ run clear_internet_sessions
+ [ "$status" -eq 0 ]
+ [ "$(db_count internet_sessions)" -eq 0 ]
+}
+
+@test "clear_internet_sessions: second run creates no new user_data_usage rows" {
+ seed_standard_fixture
+
+ clear_internet_sessions
+ local count_after_first
+ count_after_first=$(db_count user_data_usage)
+
+ # Second call on empty table: MAX(table_index) returns NULL,
+ # causing arithmetic error. Use 'run' to capture this.
+ run clear_internet_sessions
+ local count_after_second
+ count_after_second=$(db_count user_data_usage)
+
+ [ "$count_after_first" -eq "$count_after_second" ]
+}
+
+###############################################################################
+# 4. Iptables Cleanup Tests
+###############################################################################
+
+@test "clear_internet_sessions: calls iptables to delete rules for each session's IP" {
+ seed_standard_fixture
+
+ clear_internet_sessions
+
+ # 5 iptables calls per session × 2 sessions = 10 calls
+ [ "$(iptables_call_count)" -eq 10 ]
+}
+
+@test "clear_internet_sessions: iptables receives correct IPs" {
+ seed_standard_fixture
+
+ clear_internet_sessions
+
+ grep -qF "192.168.1.101" "$MOCKBIN/iptables.log"
+ grep -qF "192.168.1.102" "$MOCKBIN/iptables.log"
+}
+
+@test "clear_internet_sessions: iptables deletes mangle, nat, and filter rules" {
+ seed_standard_fixture
+
+ clear_internet_sessions
+
+ grep -q "mangle" "$MOCKBIN/iptables.log"
+ grep -q "nat" "$MOCKBIN/iptables.log"
+ grep -q "filter" "$MOCKBIN/iptables.log"
+}
+
+@test "clear_internet_sessions: no iptables calls when database is empty" {
+ # No sessions → no iptables work needed
+ run clear_internet_sessions
+
+ [ "$(iptables_call_count)" -eq 0 ]
+}
+
+###############################################################################
+# 5. Sparse / Gap Index Tests
+###############################################################################
+
+@test "clear_internet_sessions: handles non-contiguous table_index values (gaps)" {
+ seed_user 500
+ seed_user 501
+ # Indices 0 and 5 with a gap in between
+ seed_session 0 500 sessGap1 "10.0.0.10" 100 200 1 "2023-04-01 10:00:00" "2023-04-01 10:05:00" 0
+ seed_session 5 501 sessGap2 "10.0.0.11" 300 400 1 "2023-04-01 11:00:00" "2023-04-01 11:05:00" 0
+ seed_usage 500 1 "2023-04-01 10:00:00"
+ seed_usage 501 1 "2023-04-01 11:00:00"
+
+ # NOTE: remove_session returns 1 for gap indices (session doesn't exist),
+ # which propagates in strict mode. Use 'run' to capture the overall result.
+ run clear_internet_sessions
+
+ # Both real sessions should still be deleted despite gap errors
+ [ "$(db_count internet_sessions)" -eq 0 ]
+}
+
+@test "clear_internet_sessions: iterates up to MAX(table_index) even with gaps" {
+ seed_user 600
+ # Only one session but at a high index
+ seed_session 10 600 sessHigh "10.0.0.20" 50 100 1 "2023-05-01 10:00:00" "2023-05-01 10:05:00" 0
+ seed_usage 600 1 "2023-05-01 10:00:00"
+
+ # Same gap behavior caveat as above
+ run clear_internet_sessions
+
+ [ "$(db_count internet_sessions)" -eq 0 ]
+ # Should have called iptables 5 times (only 1 real session found)
+ [ "$(iptables_call_count)" -eq 5 ]
+}
+
+###############################################################################
+# 6. Pending Deletion Flag
+###############################################################################
+
+@test "clear_internet_sessions: clears sessions with pending_deletion=1" {
+ seed_user 700
+ seed_session 0 700 sessPend "10.0.0.30" 0 0 1 "2023-06-01 10:00:00" "2023-06-01 10:05:00" 1
+ seed_usage 700 1 "2023-06-01 10:00:00"
+
+ clear_internet_sessions
+
+ [ "$(db_count internet_sessions)" -eq 0 ]
+}
+
+@test "clear_internet_sessions: clears mix of pending and non-pending sessions" {
+ seed_user 800
+ seed_user 801
+ seed_session 0 800 sessNonPend "10.0.0.40" 100 200 1 "2023-07-01 10:00:00" "2023-07-01 10:05:00" 0
+ seed_session 1 801 sessPend2 "10.0.0.41" 300 400 1 "2023-07-01 11:00:00" "2023-07-01 11:05:00" 1
+ seed_usage 800 1 "2023-07-01 10:00:00"
+ seed_usage 801 1 "2023-07-01 11:00:00"
+
+ clear_internet_sessions
+
+ [ "$(db_count internet_sessions)" -eq 0 ]
+}
+
+###############################################################################
+# 7. Failure Paths
+###############################################################################
+
+@test "clear_internet_sessions: handles missing database without crashing" {
+ export DATABASE_PATH="$TMPDIR_TEST/nonexistent.db"
+
+ run clear_internet_sessions
+
+ # The function's heavy error redirection (> /dev/null 2>> error.log)
+ # swallows the sqlite3 error. It may return 0 or non-zero depending
+ # on how the arithmetic evaluates. The key contract point is that
+ # the function does not hang or crash unexpectedly.
+ # We just verify it completed (status was captured).
+ [[ "$status" -eq 0 || "$status" -ne 0 ]]
+}
+
+@test "clear_internet_sessions: produces no stdout on successful run" {
+ seed_standard_fixture
+
+ run clear_internet_sessions
+
+ [ "$status" -eq 0 ]
+ [ -z "$output" ]
+}
+
+###############################################################################
+# 8. Data Integrity
+###############################################################################
+
+@test "clear_internet_sessions: does not modify users table" {
+ seed_standard_fixture
+ local users_before
+ users_before=$(db_count users)
+
+ clear_internet_sessions
+
+ [ "$(db_count users)" -eq "$users_before" ]
+}
+
+@test "clear_internet_sessions: does not modify data_lanes table" {
+ seed_standard_fixture
+ local lanes_before
+ lanes_before=$(db_count data_lanes)
+
+ clear_internet_sessions
+
+ [ "$(db_count data_lanes)" -eq "$lanes_before" ]
+}
diff --git a/Backend/test/test_delete_user_iptables_rules.bats b/Backend/test/test_delete_user_iptables_rules.bats
new file mode 100644
index 0000000..7e11ac1
--- /dev/null
+++ b/Backend/test/test_delete_user_iptables_rules.bats
@@ -0,0 +1,255 @@
+#!/usr/bin/env bats
+
+# BATS test file for delete_user_iptable_rules
+# Run with: ./test/run.sh test/test_delete_user_iptables_rules.bats
+
+bats_require_minimum_version 1.5.0
+
+setup() {
+ TEST_DIR="$(cd "$(dirname "${BATS_TEST_FILENAME}")" && pwd)"
+ BACKEND_DIR="$(cd "$TEST_DIR/.." && pwd)"
+ MOCKBIN="$TEST_DIR/mocks"
+
+ rm -rf "$MOCKBIN"
+ mkdir -p "$MOCKBIN"
+
+ cat > "$MOCKBIN/iptables" <<'EOT'
+#!/usr/bin/env bash
+echo "$*" >> "$MOCKBIN/iptables.log"
+if [[ "$*" == *"10.0.0.999"* ]]; then
+ echo "iptables: Rule does not exist." >&2
+ exit 1
+fi
+exit 0
+EOT
+ chmod +x "$MOCKBIN/iptables"
+
+ : > "$MOCKBIN/iptables.log"
+
+ export PATH="$MOCKBIN:$PATH"
+ export MOCKBIN
+ export HS_INTERFACE="wlan1"
+ export USER_IP=""
+
+ source "$BACKEND_DIR/Cybercafe_internetSessionFunctions.sh"
+}
+
+teardown() {
+ rm -rf "$MOCKBIN" error.log 2>/dev/null || true
+}
+
+#################
+# Helper Functions
+#################
+
+# Count number of iptables calls
+count_iptables_calls() {
+ wc -l < "$MOCKBIN/iptables.log" | tr -d ' '
+}
+
+# Check if a specific iptables command was called
+iptables_was_called_with() {
+ grep -qF -- "$1" "$MOCKBIN/iptables.log"
+}
+
+#################
+# Basic Functionality Tests
+#################
+
+@test "delete_user_iptable_rules: deletes all 5 rules for valid IP" {
+ run delete_user_iptable_rules "192.168.1.50"
+
+ [ "$status" -eq 0 ]
+ [ "$(count_iptables_calls)" -eq 5 ]
+}
+
+@test "delete_user_iptable_rules: calls correct mangle iptmon_rx rule" {
+ delete_user_iptable_rules "192.168.1.50"
+
+ iptables_was_called_with "-t mangle -D iptmon_rx -o wlan1 -d 192.168.1.50"
+}
+
+@test "delete_user_iptable_rules: calls correct mangle iptmon_tx rule" {
+ delete_user_iptable_rules "192.168.1.50"
+
+ iptables_was_called_with "-t mangle -D iptmon_tx -i wlan1 -s 192.168.1.50"
+}
+
+@test "delete_user_iptable_rules: calls correct nat PREROUTING rule" {
+ delete_user_iptable_rules "192.168.1.50"
+
+ iptables_was_called_with "-t nat -D PREROUTING -p all -s 192.168.1.50 -i wlan1 -j RETURN"
+}
+
+#################
+# Idempotency Tests
+#################
+
+@test "delete_user_iptable_rules: is idempotent (second call succeeds)" {
+ # First call
+ run delete_user_iptable_rules "192.168.1.51"
+ [ "$status" -eq 0 ]
+
+ # Second call (simulating rules already deleted)
+ run delete_user_iptable_rules "192.168.1.51"
+ [ "$status" -eq 0 ]
+}
+
+@test "delete_user_iptable_rules: calling when no rules exist is safe (no-op)" {
+ # Simulate scenario where rules don't exist
+ run delete_user_iptable_rules "192.168.1.52"
+
+ # Should complete successfully even if rules didn't exist
+ [ "$status" -eq 0 ]
+}
+
+#################
+# Error Handling Tests
+#################
+
+@test "delete_user_iptable_rules: handles iptables failure gracefully" {
+ # Use IP that mock will reject
+ run delete_user_iptable_rules "10.0.0.999"
+
+ # Note: The function does not suppress iptables exit codes, only stderr.
+ # When iptables fails, that error will propagate. This is expected behavior
+ # since the error is logged to error.log for debugging.
+ # We just verify the function completes (doesn't hang) and calls were made.
+ [ "$(wc -l < "$MOCKBIN/iptables.log" | tr -d ' ')" -eq 5 ]
+}
+
+@test "delete_user_iptable_rules: handles empty IP argument" {
+ run delete_user_iptable_rules ""
+
+ # Should not crash
+ [ "$status" -eq 0 ]
+}
+
+#################
+# Interface Configuration Tests
+#################
+
+@test "delete_user_iptable_rules: works with different HS_INTERFACE" {
+ # shellcheck disable=SC2030
+ export HS_INTERFACE="eth0"
+
+ delete_user_iptable_rules "172.16.0.5"
+
+ iptables_was_called_with "-o eth0"
+ iptables_was_called_with "-i eth0"
+}
+
+@test "delete_user_iptable_rules: uses HS_INTERFACE from environment" {
+ # shellcheck disable=SC2031
+ export HS_INTERFACE="wlan0"
+
+ delete_user_iptable_rules "10.20.30.40"
+
+ # Verify wlan0 is used in the commands
+ iptables_was_called_with "wlan0"
+}
+
+#################
+# IP Address Format Tests
+#################
+
+@test "delete_user_iptable_rules: handles IPv4 with common private ranges" {
+ run delete_user_iptable_rules "192.168.1.0"
+
+ [ "$status" -eq 0 ]
+ iptables_was_called_with "192.168.1.0"
+}
+
+@test "delete_user_iptable_rules: handles 10.x.x.x private range" {
+ run delete_user_iptable_rules "10.0.0.1"
+
+ [ "$status" -eq 0 ]
+ iptables_was_called_with "10.0.0.1"
+}
+
+@test "delete_user_iptable_rules: handles 172.16.x.x private range" {
+ run delete_user_iptable_rules "172.16.0.100"
+
+ [ "$status" -eq 0 ]
+ iptables_was_called_with "172.16.0.100"
+}
+
+#################
+# Global Variable Tests
+#################
+
+@test "delete_user_iptable_rules: USER_IP global not required if argument passed" {
+ # Ensure USER_IP is empty
+ # shellcheck disable=SC2030
+ export USER_IP=""
+
+ run delete_user_iptable_rules "10.10.10.10"
+
+ # Function should work with argument only
+ [ "$status" -eq 0 ]
+ [ "$(count_iptables_calls)" -eq 5 ]
+}
+
+@test "delete_user_iptable_rules: uses argument \$1 for all 5 rules (ignores global USER_IP)" {
+ # shellcheck disable=SC2031
+ export USER_IP="SHOULD_NOT_USE_THIS"
+
+ delete_user_iptable_rules "192.168.100.200"
+
+ # All 5 rules should use the passed argument, not the global USER_IP
+ iptables_was_called_with "-d 192.168.100.200"
+ iptables_was_called_with "-s 192.168.100.200"
+
+ # Verify the global USER_IP was NOT used in any command
+ run ! grep -q "SHOULD_NOT_USE_THIS" "$MOCKBIN/iptables.log"
+}
+
+#################
+# Start/Stop Behavior Tests
+#################
+
+@test "delete_user_iptable_rules: starting when stopped works correctly" {
+ # This tests that the function can be called on a fresh state
+ run delete_user_iptable_rules "192.168.1.1"
+
+ [ "$status" -eq 0 ]
+ [ "$(count_iptables_calls)" -eq 5 ]
+}
+
+@test "delete_user_iptable_rules: multiple different IPs can be deleted sequentially" {
+ delete_user_iptable_rules "192.168.1.10"
+ count_iptables_calls > /dev/null # first call
+
+ delete_user_iptable_rules "192.168.1.20"
+ local second_count
+ second_count=$(count_iptables_calls)
+
+ # Should have 10 total calls (5 per IP)
+ [ "$second_count" -eq 10 ]
+}
+
+#################
+# Logging/Status Validation Tests
+#################
+
+@test "delete_user_iptable_rules: produces no stdout on success" {
+ run delete_user_iptable_rules "192.168.1.60"
+
+ [ "$status" -eq 0 ]
+ # Output should be empty (errors go to error.log)
+ [ -z "$output" ]
+}
+
+@test "delete_user_iptable_rules: completes all 5 iptables calls in order" {
+ delete_user_iptable_rules "192.168.1.70"
+
+ # Verify all expected calls are in the log
+ local call_count
+ call_count=$(count_iptables_calls)
+ [ "$call_count" -eq 5 ]
+
+ # Check that mangle, nat, and filter tables are all addressed
+ grep -q "mangle" "$MOCKBIN/iptables.log"
+ grep -q "nat" "$MOCKBIN/iptables.log"
+ grep -q "filter" "$MOCKBIN/iptables.log"
+}
diff --git a/Backend/test/test_setup_infrastructure.bats b/Backend/test/test_setup_infrastructure.bats
new file mode 100644
index 0000000..ddf8f41
--- /dev/null
+++ b/Backend/test/test_setup_infrastructure.bats
@@ -0,0 +1,147 @@
+#!/usr/bin/env bats
+
+# ---------------------------------------------------------
+# SETUP
+# ---------------------------------------------------------
+setup() {
+
+ date() {
+ command date "+%Y-%m-%dT%H:%M:%S"
+ }
+ export -f date
+
+ export HS_INTERFACE="wlan0"
+ export LIGHTTPD_CONF="./dummy.conf"
+ touch "$LIGHTTPD_CONF"
+
+ export ERROR_LOG="./error.log"
+ > "$ERROR_LOG"
+
+ export IPTABLES_LOG="./iptables_calls.log"
+ > "$IPTABLES_LOG"
+
+ ifconfig() {
+ echo "inet addr:192.168.43.1 Bcast:192.168.43.255 Mask:255.255.255.0"
+ }
+
+ iptables() {
+ echo "iptables $*" >> "$IPTABLES_LOG"
+ return 0
+ }
+
+ export -f ifconfig
+ export -f iptables
+
+ source "${BATS_TEST_DIRNAME}/../Cybercafe_setupFunctions.sh"
+
+ start_captive_webserver() {
+ echo "webserver_started" >> webserver_invoked.log
+ return 0
+ }
+
+ export -f start_captive_webserver
+ rm -f webserver_invoked.log
+}
+
+# ---------------------------------------------------------
+# TEARDOWN
+# ---------------------------------------------------------
+teardown() {
+ rm -f iptables_calls.log webserver_invoked.log error.log dummy.conf
+}
+
+# =========================================================
+# ORIGINAL CORE TESTS
+# =========================================================
+
+@test "LOCAL_IP extraction works correctly" {
+ setup_infrastructure
+ [ "$LOCAL_IP" = "192.168.43.1" ]
+}
+
+@test "Attempts to create iptmon_tx chain" {
+ setup_infrastructure
+ run grep -- "iptmon_tx" "$IPTABLES_LOG"
+ [ "$status" -eq 0 ]
+}
+
+@test "Attempts to create iptmon_rx chain" {
+ setup_infrastructure
+ run grep -- "iptmon_rx" "$IPTABLES_LOG"
+ [ "$status" -eq 0 ]
+}
+
+@test "Inserts NAT PREROUTING redirect rule" {
+ setup_infrastructure
+ run grep -- "PREROUTING" "$IPTABLES_LOG"
+ [ "$status" -eq 0 ]
+}
+
+@test "Inserts FORWARD rule reference appears if executed" {
+ setup_infrastructure
+ run grep -- "FORWARD" "$IPTABLES_LOG"
+ [ "$status" -eq 0 ]
+}
+
+@test "Function is idempotent (runs twice safely)" {
+ run setup_infrastructure
+ [ "$status" -eq 0 ]
+ run setup_infrastructure
+ [ "$status" -eq 0 ]
+}
+
+@test "Captive webserver is invoked" {
+ setup_infrastructure
+ run grep -- "webserver_started" webserver_invoked.log
+ [ "$status" -eq 0 ]
+}
+
+# =========================================================
+# ADDITIONAL SAFE TESTS
+# =========================================================
+
+@test "LOCAL_IP variable is not empty" {
+ setup_infrastructure
+ [ -n "$LOCAL_IP" ]
+}
+
+@test "iptables mangle table is referenced" {
+ setup_infrastructure
+ run grep -- "-t mangle" "$IPTABLES_LOG"
+ [ "$status" -eq 0 ]
+}
+
+@test "iptables nat table is referenced" {
+ setup_infrastructure
+ run grep -- "-t nat" "$IPTABLES_LOG"
+ [ "$status" -eq 0 ]
+}
+
+@test "DNAT rule contains LOCAL_IP" {
+ setup_infrastructure
+ run grep -- "$LOCAL_IP:80" "$IPTABLES_LOG"
+ [ "$status" -eq 0 ]
+}
+
+@test "iptmon_tx appears in log before completion" {
+ setup_infrastructure
+ tx_line=$(grep -n "iptmon_tx" "$IPTABLES_LOG" | head -n1 | cut -d: -f1)
+ [ -n "$tx_line" ]
+}
+
+@test "iptmon_rx appears in log" {
+ setup_infrastructure
+ run grep -- "iptmon_rx" "$IPTABLES_LOG"
+ [ "$status" -eq 0 ]
+}
+
+@test "At least one iptables command executed" {
+ setup_infrastructure
+ count=$(wc -l < "$IPTABLES_LOG")
+ [ "$count" -ge 1 ]
+}
+
+@test "Webserver invocation log file exists" {
+ setup_infrastructure
+ [ -f webserver_invoked.log ]
+}
\ No newline at end of file
diff --git a/Backend/test/test_shutdown_infrastructure.bats b/Backend/test/test_shutdown_infrastructure.bats
new file mode 100755
index 0000000..c49292c
--- /dev/null
+++ b/Backend/test/test_shutdown_infrastructure.bats
@@ -0,0 +1,69 @@
+#!/usr/bin/env bats
+
+setup() {
+ # Use DRY_RUN so no real system state is touched
+ export DRY_RUN=true
+
+ # Minimal required environment
+ export HS_INTERFACE="wlan0"
+ export STATUS_PATH="./tmp/test_shutdown_status"
+
+ mkdir -p ./tmp
+ touch "$STATUS_PATH"
+
+ # Source the code under test
+ source "$BATS_TEST_DIRNAME/../Cybercafe_setupFunctions.sh"
+}
+
+teardown() {
+ rm -rf ./tmp || true
+}
+
+@test "shutdown_infrastructure runs cleanly in dry-run mode" {
+ run shutdown_infrastructure
+
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"Beginning shutdown_infrastructure"* ]]
+ [[ "$output" == *"shutdown_infrastructure completed"* ]]
+}
+
+@test "shutdown_infrastructure is idempotent (can run twice)" {
+ run shutdown_infrastructure
+ [ "$status" -eq 0 ]
+
+ run shutdown_infrastructure
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"shutdown_infrastructure completed"* ]]
+}
+
+@test "shutdown_infrastructure does not fail when resources are missing" {
+ rm -f "$STATUS_PATH"
+
+ run shutdown_infrastructure
+
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"shutdown_infrastructure completed"* ]]
+}
+
+@test "shutdown_infrastructure succeeds even if STATUS_PATH directory is missing" {
+ rm -rf ./tmp
+
+ run shutdown_infrastructure
+
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"shutdown_infrastructure completed"* ]]
+}
+
+@test "shutdown_infrastructure prints dry-run indication" {
+ run shutdown_infrastructure
+
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"dry-run"* ]]
+}
+
+@test "shutdown_infrastructure produces some output (not silent)" {
+ run shutdown_infrastructure
+
+ [ "$status" -eq 0 ]
+ [ -n "$output" ]
+}
diff --git a/Backend/test/utils/logging.sh b/Backend/test/utils/logging.sh
new file mode 100644
index 0000000..15c8e28
--- /dev/null
+++ b/Backend/test/utils/logging.sh
@@ -0,0 +1,11 @@
+log_info() {
+ echo "[INFO] $*"
+}
+
+log_error() {
+ echo "[ERROR] $*" >&2
+}
+
+log_warn() {
+ echo "[WARN] $*"
+}
diff --git a/Backend/test/utils/net_helpers.sh b/Backend/test/utils/net_helpers.sh
new file mode 100755
index 0000000..b5a9ebb
--- /dev/null
+++ b/Backend/test/utils/net_helpers.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+create_chain() {
+ iptables -t mangle -N "$2"
+}
+
+add_rule() {
+ echo "iptables $*"
+}
+
+delete_rule() {
+ echo "iptables $*"
+}
diff --git a/Backend/test_start_captive_webserver.sh b/Backend/test_start_captive_webserver.sh
new file mode 100644
index 0000000..75dff34
--- /dev/null
+++ b/Backend/test_start_captive_webserver.sh
@@ -0,0 +1,65 @@
+#!/usr/bin/env bash
+#Organization: Grey-box
+#Project: Cybercafe
+#File: test_start_captive_webserver.sh
+#Description: Used to test start_captive_webserver function contained in Cybercafe_setupFunctions.sh
+
+echo "--- Cybercafe Webserver Test Script ---"
+
+#Load necessary config
+if [ -f "./cybercafe.conf" ]; then
+ echo "[+] Loading configuration file..."
+ . ./cybercafe.conf
+else
+ echo "[!] Error: Could not find cybercafe.conf file!"
+ exit 1
+fi
+
+#Load necessary implementation files
+if [ -f "./Cybercafe_setupFunctions.sh" ]; then
+ echo "[+] Loading Cybercafe setup functions..."
+ . ./Cybercafe_setupFunctions.sh
+else
+ echo "[!] Error: Could not find Cybercafe_setupFunctions.sh file!"
+ exit 1
+fi
+
+#Print Paths
+echo "[i] LIGHTTPD Path: $LIGHTTPD_PATH"
+echo "[i] LIGHTTPD_CONF Path: $LIGHTTPD_CONF"
+
+#Make sure paths exist
+if [ ! -x "$LIGHTTPD_PATH" ]; then
+ echo "[!] Error: lighttpd executable not found at $LIGHTTPD_PATH"
+ exit 1
+fi
+
+if [ ! -f "$LIGHTTPD_CONF" ]; then
+ echo "[!] Error: lighttpd configuration file not found at $LIGHTTPD_CONF"
+ exit 1
+fi
+
+echo "[+] Starting captive webserver (1st time)..."
+start_captive_webserver
+RC1=$?
+
+echo "[+] Starting captive webserver (2nd time, should be idempotent)..."
+start_captive_webserver
+RC2=$?
+
+echo ""
+echo "--- Test Results ---"
+echo "First start_captive_webserver call return code: $RC1"
+echo "Second start_captive_webserver call return code: $RC2"
+echo ""
+
+#Check if lighttpd process is running
+if pgrep lighttpd > /dev/null 2>> error.log; then
+ echo "[+] Captive webserver is running."
+else
+ echo "[!] Error: Captive webserver is not running."
+ exit 1
+fi
+
+echo "Check error.log for any logged errors during the test."
+echo "--- End of Test ---"
\ No newline at end of file
diff --git a/Backend/testhotspot.sh b/Backend/testhotspot.sh
new file mode 100644
index 0000000..f2027af
--- /dev/null
+++ b/Backend/testhotspot.sh
@@ -0,0 +1,179 @@
+#!/usr/bin/env bash
+# Test script for check_hotspot_status function
+# Tests the merged John + Chris implementation
+
+###INCLUDES###
+. ./cybercafe.conf
+. ./Cybercafe_setupFunctions.sh
+
+echo "=========================================="
+echo "Testing check_hotspot_status Function"
+echo "=========================================="
+echo ""
+
+# ============================================================
+# TEST 1: Initial State (Hotspot Down)
+# ============================================================
+echo "[TEST 1] Initial state - hotspot down"
+echo "Expected: HS_STATUS='down', HS_STATUS_PREV='down', TIME_TO_REFRESH='false'"
+check_hotspot_status
+echo "Actual: HS_STATUS='$HS_STATUS', HS_STATUS_PREV='$HS_STATUS_PREV', TIME_TO_REFRESH='$TIME_TO_REFRESH'"
+if [[ $HS_STATUS == 'down' ]] && [[ $HS_STATUS_PREV == 'down' ]] && [[ $TIME_TO_REFRESH == 'false' ]]; then
+ echo "✓ PASS"
+else
+ echo "✗ FAIL"
+fi
+echo ""
+
+# ============================================================
+# TEST 2: State Persistence (No Change)
+# ============================================================
+echo "[TEST 2] Call function again - state should stay same"
+echo "Expected: HS_STATUS='down', HS_STATUS_PREV='down', TIME_TO_REFRESH='false'"
+check_hotspot_status
+echo "Actual: HS_STATUS='$HS_STATUS', HS_STATUS_PREV='$HS_STATUS_PREV', TIME_TO_REFRESH='$TIME_TO_REFRESH'"
+if [[ $HS_STATUS == 'down' ]] && [[ $HS_STATUS_PREV == 'down' ]] && [[ $TIME_TO_REFRESH == 'false' ]]; then
+ echo "✓ PASS"
+else
+ echo "✗ FAIL"
+fi
+echo ""
+
+# ============================================================
+# TEST 3: Simulate Hotspot Coming Up (Transition Detection)
+# ============================================================
+echo "[TEST 3] Simulate hotspot UP - should detect transition"
+echo "Note: This requires hotspot to actually be enabled on your system"
+echo "To test: Enable WiFi hotspot, then press Enter"
+read -r -p "Ready? Press Enter to continue..."
+echo ""
+echo "Expected: HS_STATUS='up', HS_STATUS_PREV='down' (transition detected)"
+check_hotspot_status
+echo "Actual: HS_STATUS='$HS_STATUS', HS_STATUS_PREV='$HS_STATUS_PREV', TIME_TO_REFRESH='$TIME_TO_REFRESH'"
+if [[ $HS_STATUS == 'up' ]] && [[ $HS_STATUS_PREV == 'down' ]]; then
+ echo "✓ PASS - Transition detected correctly"
+else
+ echo "✗ FAIL - Transition not detected (hotspot may not be enabled)"
+fi
+echo ""
+
+# ============================================================
+# TEST 4: Fresh Setup File (Guard Logic - Fresh Config)
+# ============================================================
+echo "[TEST 4] Fresh STATUS_PATH file - guard should not trigger refresh"
+# MOCK: Manually set HS_STATUS to 'up' to test guard logic
+HS_STATUS='up'
+HS_STATUS_PREV='down'
+mkdir -p "$(dirname $STATUS_PATH)" 2>/dev/null
+touch $STATUS_PATH
+echo "MOCK: Set HS_STATUS='up' and created fresh $STATUS_PATH file"
+echo ""
+echo "Expected: TIME_TO_REFRESH='false' (config is fresh)"
+# Manually call just the guard logic part (since we can't mock the probe)
+# Recreate the guard check
+if [[ $HS_STATUS == 'up' && ! -e $STATUS_PATH ]]; then
+ TIME_TO_REFRESH=true
+elif [[ $HS_STATUS == 'up' && -e $STATUS_PATH ]]; then
+ cf_status_path_age=$(echo "$(date +%s) - $(date -r ${STATUS_PATH} +%s)" | bc 2>> error.log)
+ if [[ $cf_status_path_age -gt $REFRESH_TIME ]]; then
+ TIME_TO_REFRESH=true
+ else
+ TIME_TO_REFRESH=false
+ fi
+else
+ TIME_TO_REFRESH=false
+fi
+echo "Actual: TIME_TO_REFRESH='$TIME_TO_REFRESH'"
+if [[ $TIME_TO_REFRESH == 'false' ]]; then
+ echo "✓ PASS - Fresh config detected"
+else
+ echo "✗ FAIL"
+fi
+echo ""
+
+# ============================================================
+# TEST 5: Stale Setup File (Guard Logic - Stale Config)
+# ============================================================
+echo "[TEST 5] Stale STATUS_PATH file - guard should trigger refresh"
+# MOCK: Set hotspot UP and backdate file
+HS_STATUS='up'
+HS_STATUS_PREV='up'
+echo "Setting file timestamp to 2+ hours old (REFRESH_TIME=$REFRESH_TIME seconds)"
+# Backdate the file to 2 hours ago
+touch -t 202301010000 $STATUS_PATH
+echo "MOCK: Set HS_STATUS='up' and backdated $STATUS_PATH file"
+echo ""
+echo "Expected: TIME_TO_REFRESH='true' (config is stale)"
+# Manually run guard logic
+if [[ $HS_STATUS == 'up' && ! -e $STATUS_PATH ]]; then
+ TIME_TO_REFRESH=true
+elif [[ $HS_STATUS == 'up' && -e $STATUS_PATH ]]; then
+ cf_status_path_age=$(echo "$(date +%s) - $(date -r ${STATUS_PATH} +%s)" | bc 2>> error.log)
+ if [[ $cf_status_path_age -gt $REFRESH_TIME ]]; then
+ TIME_TO_REFRESH=true
+ else
+ TIME_TO_REFRESH=false
+ fi
+else
+ TIME_TO_REFRESH=false
+fi
+echo "Actual: TIME_TO_REFRESH='$TIME_TO_REFRESH'"
+
+# Calculate actual age for debugging
+file_age=$(echo "$(date +%s) - $(date -r ${STATUS_PATH} +%s)" | bc 2>/dev/null)
+echo "DEBUG: File age = $file_age seconds, REFRESH_TIME = $REFRESH_TIME seconds"
+
+if [[ $TIME_TO_REFRESH == 'true' ]]; then
+ echo "✓ PASS - Stale config detected"
+else
+ echo "✗ FAIL"
+fi
+echo ""
+
+# ============================================================
+# TEST 6: Missing Setup File (Guard Logic - First Setup)
+# ============================================================
+echo "[TEST 6] Missing STATUS_PATH file - guard should trigger refresh"
+# MOCK: Set hotspot UP and delete file
+HS_STATUS='up'
+HS_STATUS_PREV='down'
+rm -f $STATUS_PATH
+echo "MOCK: Set HS_STATUS='up' and deleted $STATUS_PATH file"
+echo ""
+echo "Expected: TIME_TO_REFRESH='true' (no setup record exists)"
+# Manually run guard logic
+if [[ $HS_STATUS == 'up' && ! -e $STATUS_PATH ]]; then
+ TIME_TO_REFRESH=true
+elif [[ $HS_STATUS == 'up' && -e $STATUS_PATH ]]; then
+ cf_status_path_age=$(echo "$(date +%s) - $(date -r ${STATUS_PATH} +%s)" | bc 2>> error.log)
+ if [[ $cf_status_path_age -gt $REFRESH_TIME ]]; then
+ TIME_TO_REFRESH=true
+ else
+ TIME_TO_REFRESH=false
+ fi
+else
+ TIME_TO_REFRESH=false
+fi
+echo "Actual: TIME_TO_REFRESH='$TIME_TO_REFRESH'"
+if [[ $TIME_TO_REFRESH == 'true' ]]; then
+ echo "✓ PASS - Missing file detected (first setup needed)"
+else
+ echo "✗ FAIL"
+fi
+echo ""
+
+# ============================================================
+# CLEANUP
+# ============================================================
+echo "=========================================="
+echo "Test Complete - Cleaning Up"
+echo "=========================================="
+rm -f $STATUS_PATH
+echo "Removed test STATUS_PATH file"
+echo ""
+echo "Summary:"
+echo "- Tests 1-2: John's state tracking (✓ should pass)"
+echo "- Test 3: Transition detection (fails on Windows - expected)"
+echo "- Tests 4-6: Chris's guard logic (✓ should pass with mocks)"
+echo ""
+echo "If all tests pass except Test 3: Function is working correctly!"
\ No newline at end of file
diff --git a/Backend/utils/logging.sh b/Backend/utils/logging.sh
new file mode 100755
index 0000000..c81dfe9
--- /dev/null
+++ b/Backend/utils/logging.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env sh
+# Backend/utils/logging.sh
+# Minimal structured logging for CyberCafe
+
+LOG_FILE="/var/log/cybercafe.log"
+# fallback to project-local file if /var/log is not writable
+if [ ! -w "$(dirname "$LOG_FILE")" ]; then
+ LOG_FILE="$(pwd)/Backend/cybercafe.log"
+fi
+
+_timestamp() {
+ date "+%Y-%m-%d %H:%M:%S"
+}
+
+# _log LEVEL FUNCTION MESSAGE
+_log() {
+ LEVEL="$1"
+ FUNCTION_NAME="$2"
+ MESSAGE="$3"
+ printf "[%s] [%s] %s: %s\n" "$(_timestamp)" "$FUNCTION_NAME" "$LEVEL" "$MESSAGE" | tee -a "$LOG_FILE"
+}
+
+log_info() { _log "INFO" "$1" "$2"; }
+log_warn() { _log "WARN" "$1" "$2"; }
+log_error() { _log "ERROR" "$1" "$2"; }
diff --git a/Backend/utils/net_helpers.sh b/Backend/utils/net_helpers.sh
new file mode 100755
index 0000000..2737c2b
--- /dev/null
+++ b/Backend/utils/net_helpers.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env sh
+# Backend/utils/net_helpers.sh
+# Minimal network helper wrappers for CyberCafe
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+. "${SCRIPT_DIR}/logging.sh" 2>/dev/null || true
+
+# create_chain
+create_chain() {
+ FN="$1"
+ CHAIN_SUFFIX="$2"
+ FULL="CYBERCAFE_${CHAIN_SUFFIX}"
+ # only create if does not exist
+ iptables -L "$FULL" -n >/dev/null 2>&1 || {
+ iptables -N "$FULL" >/dev/null 2>&1 && log_info "$FN" "Created chain $FULL" || log_warn "$FN" "Failed to create chain $FULL (insufficient privileges?)"
+ }
+}
+
+# add_rule ""
+# ARGS should be the arguments after 'iptables', e.g. "-t nat -I PREROUTING 1 -p tcp ..."
+add_rule() {
+ FN="$1"
+ ARGS="$2"
+ if [ "${DRY_RUN:-}" = "true" ]; then
+ log_info "$FN" "[DRY-RUN] iptables $ARGS"
+ return 0
+ fi
+ eval iptables $ARGS >/dev/null 2>&1
+ if [ $? -eq 0 ]; then
+ log_info "$FN" "Inserted iptables rule: $ARGS"
+ return 0
+ else
+ log_error "$FN" "Failed to insert iptables rule: $ARGS"
+ return 1
+ fi
+}
diff --git a/Database/CyberCafe_Database.db b/Database/CyberCafe_Database.db
new file mode 100644
index 0000000..52d88ec
Binary files /dev/null and b/Database/CyberCafe_Database.db differ
diff --git a/Database/CyberCafe_Database_Schema.sql b/Database/CyberCafe_Database_Schema.sql
new file mode 100644
index 0000000..ef06efb
--- /dev/null
+++ b/Database/CyberCafe_Database_Schema.sql
@@ -0,0 +1,56 @@
+-- Internet Sessions --
+CREATE TABLE internet_sessions(
+table_index INTEGER NOT NULL,
+user_id INTEGER NOT NULL,
+session_id TEXT NOT NULL,
+ip TEXT NOT NULL,
+session_tx INTEGER NOT NULL,
+session_rx INTEGER NOT NULL,
+session_access INTEGER NOT NULL,
+datetime_created TEXT NOT NULL,
+datetime_sinceLastRequest TEXT NOT NULL,
+pending_deletion INTEGER NOT NULL,
+PRIMARY KEY (user_id),
+FOREIGN KEY (user_id) REFERENCES users(user_id)
+);
+
+-- Users --
+CREATE TABLE users(
+user_id INTEGER NOT NULL,
+name TEXT,
+email TEXT,
+phone TEXT,
+username TEXT NOT NULL,
+password TEXT NOT NULL,
+user_level INTEGER NOT NULL,
+lane_id INTEGER NOT NULL,
+status TEXT NOT NULL,
+PRIMARY KEY (user_id)
+);
+
+-- User Data Usage --
+CREATE TABLE user_data_usage(
+user_id INTEGER NOT NULL,
+session_number INTEGER NOT NULL,
+session_entry_index INTEGER NOT NULL,
+entry_datetime TEXT NOT NULL,
+interval_bytes_tx INTEGER NOT NULL,
+interval_bytes_rx INTEGER NOT NULL,
+FOREIGN KEY (user_id) REFERENCES users(user_id));
+
+-- Data Lanes --
+CREATE TABLE data_lanes(
+lane_id INTEGER NOT NULL,
+lane_name TEXT,
+bytelimit_daily BIGINT,
+bytelimit_weekly BIGINT,
+bytelimit_monthly BIGINT,
+PRIMARY KEY (lane_id)
+);
+
+-- insert debug entries --
+INSERT INTO users VALUES('0','','','','admin','8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918','0','1','ACTIVE');
+INSERT INTO users VALUES('1','','','','user','04f8996da763b7a969b1028ee3007569eaf3a635486ddab211d512c85b9df8fb','1','2','ACTIVE');
+INSERT INTO data_lanes VALUES(0,'no-limit','100000000000000000','100000000000000000','100000000000000000');
+INSERT INTO data_lanes VALUES(1,'testlane1','1000000','1000000','1000000');
+INSERT INTO data_lanes VALUES(2,'testlane2','5000000','5000000','5000000');
\ No newline at end of file
diff --git a/Database/CyberCafe_DropAllTables.sql b/Database/CyberCafe_DropAllTables.sql
new file mode 100644
index 0000000..50300ff
--- /dev/null
+++ b/Database/CyberCafe_DropAllTables.sql
@@ -0,0 +1,4 @@
+DROP TABLE internet_sessions;
+DROP TABLE users;
+DROP TABLE user_data_usage;
+DROP TABLE data_lanes;
\ No newline at end of file
diff --git a/Database/CyberCafe_Example.db b/Database/CyberCafe_Example.db
deleted file mode 100644
index 2152ca9..0000000
Binary files a/Database/CyberCafe_Example.db and /dev/null differ
diff --git a/Database/CyberCafe_Schema.sql b/Database/CyberCafe_Schema.sql
deleted file mode 100644
index c0704e4..0000000
--- a/Database/CyberCafe_Schema.sql
+++ /dev/null
@@ -1,89 +0,0 @@
--- Primary "General user" DB. Has no concept of privilege.
-CREATE TABLE user_data (
- user_id TEXT NOT NULL,
- name TEXT NOT NULL,
- email TEXT,
- phone TEXT,
- PRIMARY KEY(user_id)
-);
-
--- Will hold the few number of speed "tiers" that are
--- configured.
-CREATE TABLE speed_queues (
- sq_id TEXT NOT NULL,
- queue_name TEXT NOT NULL,
- queue_description TEXT,
- upload_speed REAL NOT NULL,
- download_speed REAL NOT NULL,
- PRIMARY KEY (sq_id)
-);
-
--- Payment isn't necessarily money. An entry here would be
--- accompanied by a number of bytes.
-CREATE TABLE payment_history (
- ph_id TEXT NOT NULL,
- user_id TEXT NOT NULL,
- pay_datetime TEXT NOT NULL,
- sq_id INTEGER NOT NULL,
- total_bytes INTEGER NOT NULL,
- PRIMARY KEY (ph_id),
- FOREIGN KEY (user_id) REFERENCES user_data(user_id),
- FOREIGN KEY (sq_id) REFERENCES speed_queues(sq_id)
-);
-
--- After each browsing session, the tallied bytes utilized
--- need to be saved (and/or subtracted) from the particular
--- user's account balance.
-CREATE TABLE balance_table (
- user_id TEXT NOT NULL,
- sq_id INTEGER NOT NULL,
- last_update TEXT,
- bytes_remaining INTEGER NOT NULL,
- PRIMARY KEY (user_id, sq_id),
- FOREIGN KEY (user_id) REFERENCES user_data(user_id),
- FOREIGN KEY (sq_id) REFERENCES speed_queues(sq_id)
-);
-
--- When a session is created by a user going past the captive
--- portal, it should be logged here. An expired session was
--- imagined to be left here, indicated 'expired' by 'isess_length'
-CREATE TABLE internet_sessions (
- isess_id INTEGER NOT NULL,
- user_id TEXT NOT NULL,
- sq_id INTEGER NOT NULL,
- isess_datetime TEXT NOT NULL,
- isess_length INTEGER NULL,
- rx_bytes INTEGER NULL,
- tx_bytes INTEGER NULL,
- PRIMARY KEY (isess_id),
- FOREIGN KEY (user_id) REFERENCES user_data(user_id),
- FOREIGN KEY (sq_id) REFERENCES speed_queue(sq_id)
-);
-
--- This is simply to keep state for the PHP portal pages
-CREATE TABLE website_sessions (
- wsess_id TEXT NOT NULL,
- user_id TEXT NOT NULL,
- wsess_datetime TEXT NOT NULL,
- expr_datetime TEXT NOT NULL
-);
-
--- Links "General users" to a particular status. I imagined one
--- entry per user.
-CREATE TABLE user_status (
- user_id TEXT NOT NULL,
- user_status TEXT CHECK(user_status IN ('ACTIVE', 'BANNED', 'BULK', 'EXPIRED', 'PAUSED')),
- PRIMARY KEY (user_id),
- FOREIGN KEY (user_id) REFERENCES user_data(user_id)
-);
-
--- When a user's status changes, put the old status here.
-CREATE TABLE user_status_history (
- user_id TEXT NOT NULL,
- change_datetime TEXT NOT NULL,
- new_user_status TEXT CHECK(new_user_status IN ('ACTIVE', 'BANNED', 'BULK', 'EXPIRED', 'PAUSED')),
- previous_user_status TEXT CHECK(previous_user_status IN ('ACTIVE', 'BANNED', 'BULK', 'EXPIRED', 'PAUSED')),
- reason TEXT,
- PRIMARY KEY (user_id, change_datetime),
- FOREIGN KEY (user_id) REFERENCES user_data(user_id)
-);
diff --git a/Database/RMIT_CyberCafe_Schema.sql b/Database/RMIT_CyberCafe_Schema.sql
deleted file mode 100644
index 5cf8c36..0000000
--- a/Database/RMIT_CyberCafe_Schema.sql
+++ /dev/null
@@ -1,47 +0,0 @@
--- Mapping of session code to download limit and website blocking group.
--- Used to initialise a session
--- Session code is entered by the user in the captive portal.
-DROP TABLE IF EXISTS session_types;
-CREATE TABLE session_types(
- session_code TEXT NOT NULL,
- group_id TEXT NOT NULL,
- bytes_limit INTEGER NOT NULL,
- PRIMARY KEY (session_code),
- FOREIGN KEY (group_id) REFERENCES website_blocking_groups(group_id)
-);
-
--- Mapping session_id to a mac address (device) and a blocking group.
-DROP TABLE IF EXISTS session_details;
-CREATE TABLE session_details(
- session_id INTEGER PRIMARY KEY AUTOINCREMENT,
- session_start TEXT NOT NULL,
- session_end TEXT,
- group_id TEXT NOT NULL,
- mac_address TEXT NOT NULL,
- bytes_remaining INTEGER NOT NULL,
- FOREIGN KEY (group_id) REFERENCES website_blocking_groups(group_id)
-);
-
--- Website blocking groups
-DROP TABLE IF EXISTS website_blocking_groups;
-CREATE TABLE website_blocking_groups (
- group_id TEXT NOT NULL,
- group_name TEXT,
- PRIMARY KEY (group_id)
-);
-
--- Maps website blocking groups to URL
-DROP TABLE IF EXISTS website_blocking_groups_url;
-CREATE TABLE website_blocking_groups_url(
- website_url TEXT NOT NULL,
- group_id TEXT NOT NULL,
- FOREIGN KEY (group_id) REFERENCES website_blocking_groups(group_id),
- PRIMARY KEY ( website_url, group_id)
-);
-
--- Admin users
-DROP TABLE IF EXISTS admin;
-CREATE TABLE admin(
- username TEXT NOT NULL,
- password TEXT NOT NULL
-)
\ No newline at end of file
diff --git a/Database/createDatabaseEntries.sh b/Database/createDatabaseEntries.sh
new file mode 100644
index 0000000..d37b5f5
--- /dev/null
+++ b/Database/createDatabaseEntries.sh
@@ -0,0 +1,58 @@
+#FOR TESTING ONLY#
+DATABASE_PATH=./CyberCafe_Database.db
+
+
+TABLE_INDEX=$((0))
+USER_ID=$((1))
+SESSION_ID="a838fjdlkc908sdjfk3jnk2wjnef"
+USER_IP="192.168.1.131"
+SESSION_TX=$((1029423))
+SESSION_RX=$((34423))
+SESSION_ACCESS=$((1))
+DATETIME=$(date '+%Y-%m-%d %H:%M:%S')
+DATETIME_LASTREQUEST=$(date '+%Y-%m-%d %H:%M:%S')
+sqlite3 $DATABASE_PATH "INSERT INTO internet_sessions VALUES (${TABLE_INDEX},${USER_ID},'${SESSION_ID}','${USER_IP}',${SESSION_TX},${SESSION_RX},${SESSION_ACCESS},'${DATETIME}','${DATETIME_LASTREQUEST}',0)"
+echo $?
+echo "sqlite3 $DATABASE_PATH \"INSERT INTO internet_sessions VALUES (${TABLE_INDEX},${USER_ID},'${SESSION_ID}','${USER_IP}',${SESSION_TX},${SESSION_RX},${SESSION_ACCESS},'${DATETIME}','${DATETIME_LASTREQUEST}',0)\""
+exit
+
+I=$((0))
+while [[ $I -lt 8 ]]; do
+USER_ID=$((1))
+DATETIME=$(date '+%Y-%m-%d %H:%M:%S')
+INTERVAL_TX=$((1+$RANDOM%10000))
+INTERVAL_RX=$((1+$RANDOM%10000))
+SESSION_NUM=$((5))
+sqlite3 $DATABASE_PATH "INSERT INTO user_data_usage (user_id,session_entry_index,session_number,entry_datetime,interval_bytes_tx,interval_bytes_rx) VALUES (${USER_ID},${I},${SESSION_NUM},'${DATETIME}',${INTERVAL_TX},${INTERVAL_RX})"
+echo $?
+echo "sqlite3 $DATABASE_PATH \"INSERT INTO user_data_usage (user_id,entry_index,entry_datetime,interval_bytes_tx,interval_bytes_rx) VALUES (${USER_ID},${I},'${DATETIME}',${INTERVAL_TX},${INTERVAL_RX})"
+I=$(($I+1))
+done
+
+I=$((0))
+while [[ $I -lt 14 ]]; do
+USER_ID=$((1))
+DATETIME=$(date '+%Y-%m-%d %H:%M:%S')
+INTERVAL_TX=$((1+$RANDOM%10000))
+INTERVAL_RX=$((1+$RANDOM%10000))
+SESSION_NUM=$((6))
+sqlite3 $DATABASE_PATH "INSERT INTO user_data_usage (user_id,session_entry_index,session_number,entry_datetime,interval_bytes_tx,interval_bytes_rx) VALUES (${USER_ID},${I},${SESSION_NUM},'${DATETIME}',${INTERVAL_TX},${INTERVAL_RX})"
+echo $?
+echo "sqlite3 $DATABASE_PATH \"INSERT INTO user_data_usage (user_id,entry_index,entry_datetime,interval_bytes_tx,interval_bytes_rx) VALUES (${USER_ID},${I},'${DATETIME}',${INTERVAL_TX},${INTERVAL_RX})"
+I=$(($I+1))
+done
+
+I=$((0))
+while [[ $I -lt 23 ]]; do
+USER_ID=$((2))
+DATETIME=$(date '+%Y-%m-%d %H:%M:%S')
+INTERVAL_TX=$((1+$RANDOM%10000))
+INTERVAL_RX=$((1+$RANDOM%10000))
+SESSION_NUM=$((3))
+sqlite3 $DATABASE_PATH "INSERT INTO user_data_usage (user_id,session_entry_index,session_number,entry_datetime,interval_bytes_tx,interval_bytes_rx) VALUES (${USER_ID},${I},${SESSION_NUM},'${DATETIME}',${INTERVAL_TX},${INTERVAL_RX})"
+echo $?
+echo "sqlite3 $DATABASE_PATH \"INSERT INTO user_data_usage (user_id,entry_index,entry_datetime,interval_bytes_tx,interval_bytes_rx) VALUES (${USER_ID},${I},'${DATETIME}',${INTERVAL_TX},${INTERVAL_RX})"
+I=$(($I+1))
+done
+
+exit
diff --git a/README.md b/README.md
index 7cbca71..9116e67 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,76 @@
# Project Cybercafe
-Source code related to trying to achieve the Project CyberCafe dream.
-As of typing this, active work is only happening in `./Backend/` and `./PHP_Portal/`
+Source code related to trying to achieve the Project CyberCafe.
-See [Notion](https://www.notion.so/grey-box/Project-CyberCafe-ec1b3f4482714efc9e07261dc174f63b).
+## System Overview
+Project Cybercafe is an infrastructure project aimed at providing controlled internet access via a captive portal and a hotspot interface. The system acts as a router that intercepts unauthenticated web traffic, redirects users to a captive portal for login, and uses `iptables` and data tracking mechanisms to manage internet sessions based on data balances and speed queues.
+
+The architecture consists of three main components:
+1. **Backend**: A set of Bash scripts managing the core daemon, `iptables` rules (NAT, Mangle, Filter), and `lighttpd` web server configuration.
+2. **PHP Portal**: The captive portal frontend built with PHP that authenticates users, checks data balances, and inserts internet session approvals.
+3. **Database**: SQLite databases storing user information, data lane balances, and active internet sessions.
+
+## Setup Instructions
+
+### Prerequisites
+- A Linux-based environment (e.g., Raspberry Pi, standard Linux server, or Android with Termux as seen in the codebase).
+- Required packages:
+ - `bash` (for running the scripts)
+ - `sqlite3` (for database operations)
+ - `lighttpd` (for the captive portal web server)
+ - `iptables` (for routing and traffic shaping)
+ - `tc` / `iproute2` (for network interface and traffic control)
+ - `php` with standard extensions (for the portal)
+
+### Installation
+1. Clone the repository to your target machine.
+2. Make sure all scripts in `./Backend` are executable (`chmod +x ./Backend/*.sh`).
+3. Configure the environment variables in `./Backend/cybercafe.conf` appropriately for your system. Pay special attention to:
+ - `HS_INTERFACE` (The network interface your hotspot runs on, e.g., `wlan0`)
+ - `DATABASE_PATH` (Absolute path to your SQLite DB)
+ - `LIGHTTPD_PATH` and `LIGHTTPD_CONF`
+4. The database (`CyberCafe.db` or configured otherwise) should be initialized with valid `users` and `balance_table` entries. The `website_sessions` and `internet_sessions` tables will be dynamically updated by the PHP portal.
+
+## Usage Guide
+
+The primary interface for managing the backend is the `cybercafe.sh` script located in the `Backend` directory.
+
+### Starting the System
+To start the daemon and setup the captive portal:
+```bash
+cd ./Backend
+./cybercafe.sh run
+```
+This starts the Cybercafe daemon, which periodically checks the hotspot status, applies default `iptables` drop rules, redirects unauthenticated users to the captive server via DNAT on port 80, and starts the `lighttpd` web server.
+
+### Checking Status
+To view if the daemon is running and check the interface IP:
+```bash
+cd ./Backend
+./cybercafe.sh status
+```
+
+### Viewing System Information
+You can list active internet sessions, user data, data lanes, and current iptables rules:
+```bash
+./cybercafe.sh list sessions
+./cybercafe.sh list users
+./cybercafe.sh list lanes
+./cybercafe.sh list rules
+```
+
+### Shutting Down
+To safely shut down the daemon and clean up the `iptables` routing rules:
+```bash
+./cybercafe.sh shutdown
+```
+In an emergency, use `./cybercafe.sh kill` to force stop all scripts and flush the infrastructure (this abandons active sessions and cleans up all related rules).
+
+### Troubleshooting and Errors
+Errors caught by the daemon are logged to `./Backend/error.log`. You can view the latest entries easily with:
+```bash
+./cybercafe.sh errorlog
+```
+
+### Interactive Mode
+If you run `./cybercafe.sh` without arguments, it opens an interactive prompt where you can run the commands (`run`, `status`, `list `, `shutdown`, `help`) repeatedly without prefixing commands.
diff --git a/Website_ASU2024/about/page.php b/Website_ASU2024/about/page.php
new file mode 100644
index 0000000..013c351
--- /dev/null
+++ b/Website_ASU2024/about/page.php
@@ -0,0 +1,95 @@
+Project Cybercafe is software that runs on an Android device, enabling smart hotspot functionality for the sharing of internet connectivity
+ with other Wi-Fi devices. Recognizing costs and other restrictions that may exist in our targeted regions, several controls are in place to allow
+ a high level of control over how a user’s hotspot device is used for internet access.
+ https://www.grey-box.ca/project-cybercafe/
+ ';
+
+function aboutPageAdmin()
+{
+ echo '
+
+
+
+ Cybercafe Demo
+
+
+
+
+
+
+ '.$GLOBALS['adminNavHTML'].'
+ About
+ '.$GLOBALS['aboutPageContent'].'
+
+
+ ';
+}
+function aboutPageUser()
+{
+ echo '
+
+
+
+ Cybercafe Demo
+
+
+
+
+
+
+ '.$GLOBALS['userNavHTML'].'
+ About
+ '.$GLOBALS['aboutPageContent'].'
+
+
+ ';
+}
+function aboutPageDefault()
+{
+ echo '
+
+
+
+ Cybercafe Demo
+
+
+
+
+
+
+ '.$GLOBALS['defaultNavHTML'].'
+ About
+ '.$GLOBALS['aboutPageContent'].'
+
+
+ ';
+}
+
+$userType = global_verifyUser($_COOKIE);
+if($userType=='admin')
+{
+ aboutPageAdmin();
+}
+elseif($userType=='user')
+{
+ aboutPageUser();
+}
+elseif($userType=='default')
+{
+ aboutPageDefault();
+}
+else
+{}
+?>
\ No newline at end of file
diff --git a/Website_ASU2024/assets/CyberCafe_logo.png b/Website_ASU2024/assets/CyberCafe_logo.png
new file mode 100644
index 0000000..a8fbc36
Binary files /dev/null and b/Website_ASU2024/assets/CyberCafe_logo.png differ
diff --git a/Website_ASU2024/createaccount/page.php b/Website_ASU2024/createaccount/page.php
new file mode 100644
index 0000000..da88cee
--- /dev/null
+++ b/Website_ASU2024/createaccount/page.php
@@ -0,0 +1,135 @@
+query("SELECT 1 FROM users WHERE username='".$username."'");
+ $responseArray = $response->fetchArray();
+ if($responseArray)
+ {
+ return 3;
+ }
+ #create account
+ $response = $db->query("SELECT MAX(user_id) FROM users");
+ $responseArray = $response->fetchArray();
+ $user_id = (int)$responseArray[0]+1;
+ $db->exec("INSERT INTO users (
+ user_id,
+ name,
+ email,
+ phone,
+ username,
+ password,
+ user_level,
+ lane_id,
+ status)
+ VALUES(
+ ".$user_id.",
+ '".$realname."',
+ '".$email."',
+ '".$phone."',
+ '".$username."',
+ '".$password."',
+ 1,
+ 0,
+ 'DISABLED'
+ )");
+ $db->close();
+ return 1;
+}
+
+function displayPage()
+{
+ echo '
+
+
+
+ Cybercafe Demo
+
+
+
+
+
+
+ '.$GLOBALS['defaultNavHTML'].'
+ Create Account
+
+
+
+ ';
+}
+
+$userType = global_verifyUser($_COOKIE);
+if($userType=='admin')
+{
+ header('Location: /home');
+}
+elseif($userType=='user')
+{
+ header('Location: /home');
+}
+elseif($userType=='default')
+{
+ if(isset($_POST['username'])&&
+ isset($_POST['password'])&&
+ isset($_POST['password-retyped'])&&
+ isset($_POST['realname'])&&
+ isset($_POST['phone'])&&
+ isset($_POST['email']))
+ {
+ $returnVal=createAccount($_POST['username'],$_POST['password'],$_POST['password-retyped'],$_POST['realname'],$_POST['phone'],$_POST['email']);
+ if($returnVal==1)
+ {
+ header('Location: ../login');
+ }
+ if($returnVal==2)
+ {
+ displayPage();
+ echo "Passwords don't match.";
+ }
+ if($returnVal==3)
+ {
+ displayPage();
+ echo "Username is taken.";
+ }
+ else
+ {
+ displayPage();
+ echo "Error creating account";
+ }
+ }
+ else
+ {
+ displayPage();
+ }
+}
+else
+{}
+?>
diff --git a/Website_ASU2024/global.php b/Website_ASU2024/global.php
new file mode 100644
index 0000000..a4d77c2
--- /dev/null
+++ b/Website_ASU2024/global.php
@@ -0,0 +1,133 @@
+
+ Home
+ Stats
+ Manage Users
+ Manage Lanes
+ About
+ Logout
+ ';
+$GLOBALS['userNavHTML']='
+ ';
+$GLOBALS['defaultNavHTML']='
+ ';
+$GLOBALS['defaultStyle']='
+ ul {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+ overflow: hidden;
+ background-color: #e7e7e7;
+ }
+ li {
+ float: left;
+ }
+ li a {
+ display: block;
+ color: black;
+ text-align: center;
+ padding: 14px 16px;
+ text-decoration: none;
+ }
+ li a:hover {
+ background-color: #bfbfbf;
+ }
+ th, td
+ {
+ padding-top: 10px;
+ padding-bottom: 10px;
+ padding-left: 10px;
+ padding-right: 10px;
+ border-style: groove;
+ text-align: center;
+ font-size: 70%
+ }
+ td
+ {
+ font-weight:normal;
+ font-size: 60%;
+ }
+ p
+ {
+ font-family:verdana;
+ font-size:70%;
+ font-weight:normal;
+ }
+ ';
+
+function hashPassword($passwordString)
+{
+ return hash('sha256', $passwordString);
+}
+
+function global_createDatabaseObj()
+{
+ return $db = new SQLite3($GLOBALS['database_path']);
+}
+
+function global_removeInternetSession($table_index)
+{
+ $db = global_createDatabaseObj();
+ $db->exec("UPDATE internet_sessions SET pending_deletion=1 WHERE table_index=".$table_index);
+ $db->close();
+}
+
+function global_verifyUser($cookies)
+{
+ if(isset($cookies['session_id']))
+ {
+ $db = global_createDatabaseObj();
+ $response = $db->query("SELECT user_id FROM internet_sessions WHERE session_id='".$cookies['session_id']."'");
+ $responseArray = $response->fetchArray();
+ if($responseArray)
+ {
+ $user_id = (int)$responseArray['user_id'];
+ $response2 = $db->query("SELECT * FROM users WHERE user_id=".$user_id."");
+ $responseArray2 = $response2->fetchArray();
+ $db->close();
+ if($responseArray2['user_level']==0)
+ {
+ return 'admin';
+ }
+ elseif($responseArray2['user_level']==1 && $responseArray2['status']!='BANNED')
+ {
+ return 'user';
+ }
+ else
+ {
+ setcookie('session_id', '', time()-3600, '/');
+ header('Location: /login');
+ return -1;
+ }
+ }
+ #if there is no internet session matching the cookie then return to login
+ else
+ {
+ $db->close();
+ setcookie('session_id', '', time()-3600, '/');
+ header('Location: /login');
+ return -1;
+ }
+ $db->close();
+ }
+ else
+ {
+ return 'default';
+ }
+}
+?>
diff --git a/Website_ASU2024/globalfunctions.php b/Website_ASU2024/globalfunctions.php
new file mode 100644
index 0000000..c87117d
--- /dev/null
+++ b/Website_ASU2024/globalfunctions.php
@@ -0,0 +1,84 @@
+
+ Home
+ Stats
+ Manage Users
+ Manage Lanes
+ About
+ Logout
+ ';
+$GLOBALS['userNavHTML']='
+ ';
+$GLOBALS['defaultNavHTML']='
+ ';
+
+function global_createDatabaseObj()
+{
+ return $db = new SQLite3($GLOBALS['database_path']);
+}
+
+function global_removeInternetSession($table_index)
+{
+ $db = global_createDatabaseObj();
+ $db->exec("UPDATE internet_sessions SET pending_deletion=1 WHERE table_index=".$table_index);
+ $db->close();
+}
+
+function global_verifyUser($cookies)
+{
+ if(isset($cookies['session_id']))
+ {
+ $db = global_createDatabaseObj();
+ $response = $db->query("SELECT user_id FROM internet_sessions WHERE session_id='".$cookies['session_id']."'");
+ $responseArray = $response->fetchArray();
+ if($responseArray)
+ {
+ $user_id = (int)$responseArray['user_id'];
+ $response2 = $db->query("SELECT * FROM users WHERE user_id=".$user_id."");
+ $responseArray2 = $response2->fetchArray();
+ $db->close();
+ if($responseArray2['user_level']==0)
+ {
+ return 'admin';
+ }
+ elseif($responseArray2['user_level']==1 && $responseArray2['status']!='BANNED')
+ {
+ return 'user';
+ }
+ else
+ {
+ setcookie('session_id', '', time()-3600, '/');
+ header('Location: /login');
+ return -1;
+ }
+ }
+ #if there is no internet session matching the cookie then return to login
+ else
+ {
+ $db->close();
+ setcookie('session_id', '', time()-3600, '/');
+ header('Location: /login');
+ return -1;
+ }
+ $db->close();
+ }
+ else
+ {
+ return 'default';
+ }
+}
+?>
diff --git a/Website_ASU2024/home/ManageLanes/page.php b/Website_ASU2024/home/ManageLanes/page.php
new file mode 100644
index 0000000..af85e31
--- /dev/null
+++ b/Website_ASU2024/home/ManageLanes/page.php
@@ -0,0 +1,226 @@
+exec("UPDATE data_lanes SET bytelimit_daily=".$bytelimit." WHERE lane_id=".$lane_id);
+ $db->close();
+}
+
+function updateByteLimitWeekly($lane_id,$bytelimit)
+{
+ $bytelimit=$bytelimit*(10**6);
+ $db = global_createDatabaseObj();
+ $db->exec("UPDATE data_lanes SET bytelimit_weekly=".$bytelimit." WHERE lane_id=".$lane_id);
+ $db->close();
+}
+
+function updateByteLimitMonthly($lane_id,$bytelimit)
+{
+ $bytelimit=$bytelimit*(10**6);
+ $db = global_createDatabaseObj();
+ $db->exec("UPDATE data_lanes SET bytelimit_monthly=".$bytelimit." WHERE lane_id=".$lane_id);
+ $db->close();
+}
+
+function removeLane($lane_id)
+{
+ $db = global_createDatabaseObj();
+ $db->exec("DELETE FROM data_lanes WHERE lane_id=".$lane_id);
+ $db->close();
+}
+
+function newLane()
+{
+ $db = global_createDatabaseObj();
+ $response=$db->query("SELECT MAX(lane_id) FROM data_lanes");
+ $responseArray=$response->fetchArray();
+ if($response)
+ {
+ $nextLaneID=$responseArray[0]+1;
+ }
+ else
+ {
+ $nextLaneID=0;
+ }
+ $db->exec("INSERT INTO data_lanes (lane_id,lane_name,bytelimit_daily,bytelimit_weekly,bytelimit_monthly) VALUES (".$nextLaneID.",'new lane',0,0,0)");
+ $db->close();
+}
+
+function renameLane($lane_id,$newName)
+{
+ $db = global_createDatabaseObj();
+ $db->exec("UPDATE data_lanes SET lane_name='".$newName."' WHERE lane_id=".$lane_id);
+ $db->close();
+}
+
+function displayPage()
+{
+ $db =global_createDatabaseObj();
+ $response = $db->query("SELECT * FROM data_lanes");
+ $table_entries="";
+ $i=0;
+ while($responseArray=$response->fetchArray())
+ {
+ $lane_id=$responseArray['lane_id'];
+ $lane_name=$responseArray['lane_name'];
+ $bytelimit_daily=$responseArray['bytelimit_daily']/(10**6);
+ $bytelimit_weekly=$responseArray['bytelimit_weekly']/(10**6);
+ $bytelimit_monthly=$responseArray['bytelimit_monthly']/(10**6);
+ $response2=$db->query("SELECT username FROM users WHERE lane_id=".$lane_id);
+ $responseArray2=$response2->fetchArray(SQLITE3_NUM);
+ if($responseArray2)
+ {
+ $usersInLane=sizeof($responseArray2);
+ }
+ else
+ {
+ $usersInLane=0;
+ }
+ $table_entries=$table_entries."
+ | ".$lane_id." |
+ |
+ ".$usersInLane." |
+ |
+ |
+ |
+ |
+
";
+ $i=$i+1;
+ }
+ $db->close();
+ echo '
+
+
+ Cybercafe Demo
+
+
+
+
+
+
+ '.$GLOBALS['adminNavHTML'].'
+ Manage Lanes
+
+
+ | Lane ID |
+ Lane Name |
+ Users In Lane |
+ Bytelimit Daily |
+ Bytelimit Weekly |
+ Bytelimit Monthly |
+ Actions |
+
+ '.$table_entries.'
+
+
+
+
+ ';
+}
+
+$userType = global_verifyUser($_COOKIE);
+if($userType=='admin')
+{
+ if(array_key_exists('bytelimit_daily', $_POST))
+ {
+ updateByteLimitDaily($_POST['action_laneID'],$_POST['bytelimit_daily']);
+ }
+ if(array_key_exists('bytelimit_weekly', $_POST))
+ {
+ updateByteLimitWeekly($_POST['action_laneID'],$_POST['bytelimit_weekly']);
+ }
+ if(array_key_exists('bytelimit_monthly', $_POST))
+ {
+ updateByteLimitMonthly($_POST['action_laneID'],$_POST['bytelimit_monthly']);
+ }
+ if(array_key_exists('laneButton_remove', $_POST))
+ {
+ removeLane($_POST['action_laneID']);
+ }
+ if(array_key_exists('laneButton_new', $_POST))
+ {
+ newLane();
+ }
+ if(array_key_exists('text_laneName', $_POST))
+ {
+ renameLane($_POST['action_laneID'],$_POST['text_laneName']);
+ }
+ displayPage();
+}
+elseif($userType=='user')
+{
+ header('Location: /home');
+}
+elseif($userType=='default')
+{
+ header('Location: /login');
+}
+else
+{}
+?>
\ No newline at end of file
diff --git a/Website_ASU2024/home/ManageUsers/page.php b/Website_ASU2024/home/ManageUsers/page.php
new file mode 100644
index 0000000..97ea8c9
--- /dev/null
+++ b/Website_ASU2024/home/ManageUsers/page.php
@@ -0,0 +1,243 @@
+query("SELECT user_level FROM users WHERE user_id='".$user_id."'")->fetchArray()[0]!=0)
+ {
+ $db->exec("UPDATE users SET status='BANNED' WHERE user_id='".$user_id."'");
+ }
+ $db->close();
+}
+function unbanUser($user_id)
+{
+ $db = global_createDatabaseObj();
+ $db->exec("UPDATE users SET status='ACTIVE' WHERE user_id='".$user_id."'");
+ $db->close();
+}
+function disableUser($user_id)
+{
+ $db = global_createDatabaseObj();
+ $db->exec("UPDATE users SET status='DISABLED' WHERE user_id='".$user_id."'");
+ $db->close();
+}
+function enableUser($user_id)
+{
+ $db = global_createDatabaseObj();
+ $db->exec("UPDATE users SET status='ACTIVE' WHERE user_id='".$user_id."'");
+ $db->close();
+}
+
+function updateLaneID($user_id,$lane_id)
+{
+ $db = global_createDatabaseObj();
+ $db->exec("UPDATE users SET lane_id=".$lane_id." WHERE user_id='".$user_id."'");
+ $db->close();
+}
+
+function displayPage()
+{
+ $db = global_createDatabaseObj();
+ $response = $db->query("SELECT * FROM users");
+ $response2 = $db->query("SELECT COUNT(lane_id) FROM data_lanes");
+ $responseArray2=$response2->fetchArray(SQLITE3_NUM);
+ $numberOfDataLanes=(int)$responseArray2[0];
+ $table_entries="";
+ $i=0;
+ while($responseArray=$response->fetchArray())
+ {
+ $user_id=$responseArray['user_id'];
+ $dataLaneFormHTML="";
+ $response2=$db->query("SELECT session_tx,session_rx FROM internet_sessions WHERE user_id='".$user_id."'");
+ if($responseArray2=$response2->fetchArray())
+ {
+ $session_rx=$responseArray2['session_rx'];
+ $session_tx=$responseArray2['session_tx'];
+ }
+ else
+ {
+ $session_rx='0';
+ $session_tx='0';
+ }
+ if($responseArray['user_level']==0)
+ {
+ $isAdmin='Yes';
+ }
+ else
+ {
+ $isAdmin='No';
+ }
+ #Convert to KB
+ $session_rx=round($session_rx/1000);
+ $session_tx=round($session_tx/1000);
+ $table_entries=$table_entries."
+ | ".$responseArray['user_id']." |
+ ".$responseArray['username']." |
+ ".$responseArray['name']." |
+ ".$responseArray['email']." |
+ ".$responseArray['phone']." |
+ ".$responseArray['status']." |
+ ".$session_rx." KB |
+ ".$session_tx." KB |
+ ".$dataLaneFormHTML." |
+ ".$isAdmin." | ";
+ if($responseArray['status']=="BANNED")
+ {
+ $table_entries=$table_entries."
+ |
+ |
+
";
+ }
+ else
+ {
+ if($responseArray['status']=="DISABLED")
+ {
+ if($isAdmin=='Yes')
+ {
+ $table_entries=$table_entries."
+ |
+ |
+ ";
+ }
+ else
+ {
+ $table_entries=$table_entries."
+ |
+ |
+ ";
+ }
+ }
+ else
+ {
+ if($isAdmin=='Yes')
+ {
+ $table_entries=$table_entries."
+ |
+ |
+ ";
+ }
+ else
+ {
+ $table_entries=$table_entries."
+ |
+ |
+ ";
+ }
+ }
+ }
+ }
+ echo '
+
+
+ Cybercafe Demo
+
+
+
+
+
+
+ '.$GLOBALS['adminNavHTML'].'
+ Manage Users
+
+
+
+ | User ID |
+ Username |
+ Name |
+ Email |
+ Phone |
+ Status |
+ Session RX |
+ Session TX |
+ Lane ID |
+ Admin |
+ Actions |
+
+ '.$table_entries.'
+
+
+
+ ';
+ $db->close();
+}
+
+$userType = global_verifyUser($_COOKIE);
+if($userType=='admin')
+{
+ if(array_key_exists('button', $_POST))
+ {
+ if($_POST['button']=='disable')
+ {
+ disableUser($_POST['action_userID']);
+ }
+ elseif($_POST['button']=='ban')
+ {
+ banUser($_POST['action_userID']);
+ }
+ elseif($_POST['button']=='enable')
+ {
+ enableUser($_POST['action_userID']);
+ }
+ elseif($_POST['button']=='unban')
+ {
+ unbanUser($_POST['action_userID']);
+ }
+ }
+ if(array_key_exists('laneID', $_POST))
+ {
+ updateLaneID($_POST['action_userID'],(int)$_POST['laneID']);
+ }
+ displayPage();
+}
+elseif($userType=='user')
+{
+ header('Location: /home');
+}
+elseif($userType=='default')
+{
+ header('Location: /login');
+}
+else
+{}
+?>
\ No newline at end of file
diff --git a/Website_ASU2024/home/Stats/page.php b/Website_ASU2024/home/Stats/page.php
new file mode 100644
index 0000000..6043532
--- /dev/null
+++ b/Website_ASU2024/home/Stats/page.php
@@ -0,0 +1,235 @@
+query("SELECT * FROM users");
+ $HTML='
+
+
+ | User ID |
+ Username |
+ Name |
+ Today\'s RX |
+ Today\'s TX |
+ Today\'s Total |
+ Lane ID |
+ Admin |
+
';
+ $i=0;
+ while($responseArray=$response->fetchArray())
+ {
+ $user_id=$responseArray['user_id'];
+ $response2 = $db->query("SELECT SUM(interval_bytes_rx) FROM user_data_usage WHERE entry_datetime>=date(datetime(),'localtime','-1 days') AND user_id=".$user_id);
+ $response3 = $db->query("SELECT SUM(interval_bytes_tx) FROM user_data_usage WHERE entry_datetime>=date(datetime(),'localtime','-1 days') AND user_id=".$user_id);
+ if($response2)
+ {
+ $userRXToday=$response2->fetchArray(SQLITE3_NUM)[0];
+ }
+ if($response3)
+ {
+ $userTXToday=$response3->fetchArray(SQLITE3_NUM)[0];
+ }
+ if($responseArray['user_level']==0)
+ {
+ $isAdmin='Yes';
+ }
+ else
+ {
+ $isAdmin='No';
+ }
+ #Convert to KB
+ $userTXToday=round($userTXToday/1000);
+ $userRXToday=round($userRXToday/1000);
+ if(($userRXToday!=0 && $userRXToday!='')||($userTXToday!=0 && $userTXToday!=''))
+ {
+ $userTotalToday=(int)$userRXToday+(int)$userTXToday;
+ $HTML=$HTML."
+ | ".$responseArray['user_id']." |
+ ".$responseArray['username']." |
+ ".$responseArray['name']." |
+ ".$userRXToday." KB |
+ ".$userTXToday." KB |
+ ".$userTotalToday." KB |
+ ".$responseArray['lane_id']." |
+ ".$isAdmin." | ";
+ }
+ }
+ $HTML=$HTML."
";
+ $db->close();
+ return $HTML;
+}
+
+function weeklyStats()
+{
+ $db = global_createDatabaseObj();
+ #Daily Stats
+ $response = $db->query("SELECT * FROM users");
+ $HTML='
+
+
+ | User ID |
+ Username |
+ Name |
+ Week\'s RX |
+ Week\'s TX |
+ Week\'s Total |
+ Lane ID |
+ Admin |
+
';
+ $i=0;
+ while($responseArray=$response->fetchArray())
+ {
+ $user_id=$responseArray['user_id'];
+ $response2 = $db->query("SELECT SUM(interval_bytes_rx) FROM user_data_usage WHERE entry_datetime>=date(datetime(),'localtime','-7 days') AND user_id=".$user_id);
+ $response3 = $db->query("SELECT SUM(interval_bytes_tx) FROM user_data_usage WHERE entry_datetime>=date(datetime(),'localtime','-7 days') AND user_id=".$user_id);
+ if($response2)
+ {
+ $userRXWeek=$response2->fetchArray(SQLITE3_NUM)[0];
+ }
+ if($response3)
+ {
+ $userTXWeek=$response3->fetchArray(SQLITE3_NUM)[0];
+ }
+ if($responseArray['user_level']==0)
+ {
+ $isAdmin='Yes';
+ }
+ else
+ {
+ $isAdmin='No';
+ }
+ #Convert to KB
+ $userTXWeek=round($userTXWeek/1000);
+ $userRXWeek=round($userRXWeek/1000);
+ if(($userRXWeek!=0 && $userRXWeek!='')||($userTXWeek!=0 && $userTXWeek!=''))
+ {
+ $userTotalToday=(int)$userRXWeek+(int)$userTXWeek;
+ $HTML=$HTML."
+ | ".$responseArray['user_id']." |
+ ".$responseArray['username']." |
+ ".$responseArray['name']." |
+ ".$userRXWeek." KB |
+ ".$userTXWeek." KB |
+ ".$userTotalToday." KB |
+ ".$responseArray['lane_id']." |
+ ".$isAdmin." | ";
+ }
+ }
+ $HTML=$HTML."
";
+ $db->close();
+ return $HTML;
+}
+
+function monthlyStats()
+{
+ $db = global_createDatabaseObj();
+ #Daily Stats
+ $response = $db->query("SELECT * FROM users");
+ $HTML='
+
+
+ | User ID |
+ Username |
+ Name |
+ Month\'s RX |
+ Month\'s TX |
+ Month\'s Total |
+ Lane ID |
+ Admin |
+
';
+ $i=0;
+ while($responseArray=$response->fetchArray())
+ {
+ $user_id=$responseArray['user_id'];
+ $response2 = $db->query("SELECT SUM(interval_bytes_rx) FROM user_data_usage WHERE entry_datetime>=date(datetime(),'localtime','-30 days') AND user_id=".$user_id);
+ $response3 = $db->query("SELECT SUM(interval_bytes_tx) FROM user_data_usage WHERE entry_datetime>=date(datetime(),'localtime','-30 days') AND user_id=".$user_id);
+ if($response2)
+ {
+ $userRXMonth=$response2->fetchArray(SQLITE3_NUM)[0];
+ }
+ if($response3)
+ {
+ $userTXMonth=$response3->fetchArray(SQLITE3_NUM)[0];
+ }
+ if($responseArray['user_level']==0)
+ {
+ $isAdmin='Yes';
+ }
+ else
+ {
+ $isAdmin='No';
+ }
+ #Convert to KB
+ $userTXMonth=round($userTXMonth/1000);
+ $userRXMonth=round($userRXMonth/1000);
+ if(($userRXMonth!=0 && $userRXMonth!='')||($userTXMonth!=0 && $userTXMonth!=''))
+ {
+ $userTotalToday=(int)$userRXMonth+(int)$userTXMonth;
+ $HTML=$HTML."
+ | ".$responseArray['user_id']." |
+ ".$responseArray['username']." |
+ ".$responseArray['name']." |
+ ".$userRXMonth." KB |
+ ".$userTXMonth." KB |
+ ".$userTotalToday." KB |
+ ".$responseArray['lane_id']." |
+ ".$isAdmin." | ";
+ }
+ }
+ $HTML=$HTML."
";
+ $db->close();
+ return $HTML;
+}
+
+function displayPage()
+{
+ echo '
+
+
+ Cybercafe Demo
+
+
+
+
+
+
+ '.$GLOBALS['adminNavHTML'].'
+ Statistics
+
+ Daily
+ '.dailyStats().'
+ Weekly
+ '.weeklyStats().'
+ Monthly
+ '.monthlyStats().'
+
+
+ ';
+}
+
+$userType = global_verifyUser($_COOKIE);
+if($userType=='admin')
+{
+ displayPage();
+}
+elseif($userType=='user')
+{
+ header('Location: /home');
+}
+elseif($userType=='default')
+{
+ header('Location: /login');
+}
+else
+{}
+?>
\ No newline at end of file
diff --git a/Website_ASU2024/home/page.php b/Website_ASU2024/home/page.php
new file mode 100644
index 0000000..2016322
--- /dev/null
+++ b/Website_ASU2024/home/page.php
@@ -0,0 +1,302 @@
+query("SELECT * FROM internet_sessions WHERE session_id='".$_COOKIE['session_id']."'");
+ $responseArray = $response->fetchArray();
+ $user_id=$responseArray['user_id'];
+
+ $response2 = $db->query("SELECT MAX(session_number) FROM user_data_usage WHERE user_id=".$user_id);
+ $responseArray2 = $response2->fetchArray();
+ $sessionNumber=(int)$responseArray2[0];
+
+ #Get data usage information
+ #Session TX
+ $sessionTX=$responseArray['session_tx'];
+ #Session RX
+ $sessionRX=$responseArray['session_rx'];
+ #Today
+ $response2 = $db->query("SELECT SUM(interval_bytes_tx),SUM(interval_bytes_rx) FROM user_data_usage WHERE user_id=".$user_id." AND entry_datetime>=datetime(datetime(),'localtime','-1 days')");
+ $responseArray2 = $response2->fetchArray(SQLITE3_NUM);
+ $todayTotalTX = (int)$responseArray2[0];
+ $todayTotalRX = (int)$responseArray2[1];
+ $todayTotal = $todayTotalTX+$todayTotalRX;
+ #Week
+ $response2 = $db->query("SELECT SUM(interval_bytes_tx),SUM(interval_bytes_rx) FROM user_data_usage WHERE user_id=".$user_id." AND entry_datetime>=datetime(datetime(),'localtime','-7 days')");
+ $responseArray2 = $response2->fetchArray(SQLITE3_NUM);
+ $weekTotalTX = (int)$responseArray2[0];
+ $weekTotalRX = (int)$responseArray2[1];
+ $weekTotal = $weekTotalTX+$weekTotalRX;
+ #Month
+ $response2 = $db->query("SELECT SUM(interval_bytes_tx),SUM(interval_bytes_rx) FROM user_data_usage WHERE user_id=".$user_id." AND entry_datetime>=datetime(datetime(),'localtime','-30 days')");
+ $responseArray2 = $response2->fetchArray(SQLITE3_NUM);
+ $monthTotalTX = (int)$responseArray2[0];
+ $monthTotalRX = (int)$responseArray2[1];
+ $monthTotal = $monthTotalTX+$monthTotalRX;
+
+ #Status
+ $response2 = $db->query("SELECT status,lane_id FROM users WHERE user_id=".$user_id);
+ $responseArray2 = $response2->fetchArray();
+ $status=$responseArray2['status'];
+ $lane_id=$responseArray2['lane_id'];
+ if($status=='ACTIVE')
+ {
+ $status='ADMIN';
+ $laneDailyLimit='ADMIN';
+ $laneWeeklyLimit='ADMIN';
+ $laneMonthlyLimit='ADMIN';
+ }
+ else
+ {
+ $laneDailyLimit=0;
+ $laneWeeklyLimit=0;
+ $laneMonthlyLimit=0;
+ }
+ echo '
+
+
+ Cybercafe Demo
+
+
+
+
+
+
+ '.$GLOBALS['adminNavHTML'].'
+ Admin Page
+
+
+
+ | Session # |
+ '.$sessionNumber.' |
+
+
+ | Data Lane # |
+ '.$lane_id.' |
+
+
+ | Session Status |
+ '.$status.' |
+
+
+ | Session TX |
+ '.$sessionTX.' KB |
+
+
+ | Session RX |
+ '.$sessionRX.' KB |
+
+
+ | Today\'s Byte Usage |
+ '.$todayTotal.' KB |
+
+
+ | Lane Daily Limit |
+ '.$laneDailyLimit.' KB |
+
+
+ | Weeks\'s Byte Usage |
+ '.$weekTotal.' KB |
+
+
+ | Lane Weekly Limit |
+ '.$laneWeeklyLimit.' KB |
+
+
+ | Month\'s Byte Usage |
+ '.$monthTotal.' KB |
+
+
+ | Lane Monthly Limit |
+ '.$laneMonthlyLimit.' KB |
+
+
+
+ ';
+}
+function userHomePage()
+{
+ $db = global_createDatabaseObj();
+ $response = $db->query("SELECT * FROM internet_sessions WHERE session_id='".$_COOKIE['session_id']."'");
+ $responseArray = $response->fetchArray();
+ $user_id=$responseArray['user_id'];
+
+ $response2 = $db->query("SELECT MAX(session_number) FROM user_data_usage WHERE user_id=".$user_id);
+ $responseArray2 = $response2->fetchArray();
+ $sessionNumber=(int)$responseArray2[0];
+
+ #Get data usage information
+ #Session TX
+ $sessionTX=$responseArray['session_tx'];
+ #Session RX
+ $sessionRX=$responseArray['session_rx'];
+ #Today
+ $response2 = $db->query("SELECT SUM(interval_bytes_tx),SUM(interval_bytes_rx) FROM user_data_usage WHERE user_id=".$user_id." AND entry_datetime>=datetime(datetime(),'localtime','-1 days')");
+ $responseArray2 = $response2->fetchArray(SQLITE3_NUM);
+ $todayTotalTX = (int)$responseArray2[0];
+ $todayTotalRX = (int)$responseArray2[1];
+ $todayTotal = $todayTotalTX+$todayTotalRX;
+ #Week
+ $response2 = $db->query("SELECT SUM(interval_bytes_tx),SUM(interval_bytes_rx) FROM user_data_usage WHERE user_id=".$user_id." AND entry_datetime>=datetime(datetime(),'localtime','-7 days')");
+ $responseArray2 = $response2->fetchArray(SQLITE3_NUM);
+ $weekTotalTX = (int)$responseArray2[0];
+ $weekTotalRX = (int)$responseArray2[1];
+ $weekTotal = $weekTotalTX+$weekTotalRX;
+ #Month
+ $response2 = $db->query("SELECT SUM(interval_bytes_tx),SUM(interval_bytes_rx) FROM user_data_usage WHERE user_id=".$user_id." AND entry_datetime>=datetime(datetime(),'localtime','-30 days')");
+ $responseArray2 = $response2->fetchArray(SQLITE3_NUM);
+ $monthTotalTX = (int)$responseArray2[0];
+ $monthTotalRX = (int)$responseArray2[1];
+ $monthTotal = $monthTotalTX+$monthTotalRX;
+
+ #Status
+ $response2 = $db->query("SELECT status,lane_id FROM users WHERE user_id=".$user_id);
+ $responseArray2 = $response2->fetchArray();
+ $status=$responseArray2['status'];
+ $lane_id=$responseArray2['lane_id'];
+ if($status=='ACTIVE')
+ {
+ #Calculate if overlimits
+ #LANE_DAILY_LIMIT=$(($(sqlite3 "${DATABASE_PATH}" "SELECT bytelimit_daily FROM data_lanes WHERE lane_id=${LANE_ID}"))) > /dev/null 2>> error.log
+ #LANE_WEEKLY_LIMIT=$(($(sqlite3 "${DATABASE_PATH}" "SELECT bytelimit_weekly FROM data_lanes WHERE lane_id=${LANE_ID}"))) > /dev/null 2>> error.log
+ #LANE_MONTHLY_LIMIT=$(($(sqlite3 "${DATABASE_PATH}" "SELECT bytelimit_monthly FROM data_lanes WHERE lane_id=${LANE_ID}"))) > /dev/null 2>> error.log
+ $response2 = $db->query("SELECT bytelimit_daily,bytelimit_weekly,bytelimit_monthly FROM data_lanes WHERE lane_id=".$lane_id);
+ $responseArray2 = $response2->fetchArray();
+ $laneDailyLimit=(int)$responseArray2[0];
+ $laneWeeklyLimit=(int)$responseArray2[1];
+ $laneMonthlyLimit=(int)$responseArray2[2];
+ if($todayTotal>$laneDailyLimit or $weekTotal>$laneWeeklyLimit or $monthTotal>$laneMonthlyLimit)
+ {
+ $status='Over Limits';
+ }
+ #Convert to KB
+ $laneDailyLimit=round($laneDailyLimit/1000);
+ $laneWeeklyLimit=round($laneWeeklyLimit/1000);
+ $laneMonthlyLimit=round($laneMonthlyLimit/1000);
+ $todayTotal=round($todayTotal/1000);
+ $weekTotal=round($weekTotal/1000);
+ $monthTotal=round($monthTotal/1000);
+ $sessionTX=round($sessionTX/1000);
+ $sessionRX=round($sessionRX/1000);
+ }
+ else
+ {
+ $laneDailyLimit=0;
+ $laneWeeklyLimit=0;
+ $laneMonthlyLimit=0;
+ }
+ echo '
+
+
+ Cybercafe Demo
+
+
+
+
+
+
+ '.$GLOBALS['userNavHTML'].'
+ User Page
+
+
+
+ | Session # |
+ '.$sessionNumber.' |
+
+
+ | Data Lane # |
+ '.$lane_id.' |
+
+
+ | Session Status |
+ '.$status.' |
+
+
+ | Session TX |
+ '.$sessionTX.' KB |
+
+
+ | Session RX |
+ '.$sessionRX.' KB |
+
+
+ | Today\'s Byte Usage |
+ '.$todayTotal.' KB |
+
+
+ | Lane Daily Limit |
+ '.$laneDailyLimit.' KB |
+
+
+ | Weeks\'s Byte Usage |
+ '.$weekTotal.' KB |
+
+
+ | Lane Weekly Limit |
+ '.$laneWeeklyLimit.' KB |
+
+
+ | Month\'s Byte Usage |
+ '.$monthTotal.' KB |
+
+
+ | Lane Monthly Limit |
+ '.$laneMonthlyLimit.' KB |
+
+
+
+ ';
+}
+function defaultHomePage()
+{
+ echo '
+
+
+ Cybercafe Demo
+
+
+
+
+
+
+ '.$GLOBALS['defaultNavHTML'].'
+ Greybox: Project Cybercafe
+
Project Cybercafe is software that runs on an Android device, enabling smart hotspot functionality for the sharing of internet connectivity
+ with other Wi-Fi devices. Recognizing costs and other restrictions that may exist in our targeted regions, several controls are in place to allow
+ a high level of control over how a user’s hotspot device is used for internet access.
+ https://www.grey-box.ca/project-cybercafe/
+
+
+ ';
+}
+
+$userType = global_verifyUser($_COOKIE);
+if($userType=='admin')
+{
+ adminHomePage();
+}
+elseif($userType=='user')
+{
+ userHomePage();
+}
+elseif($userType=='default')
+{
+ defaultHomePage();
+}
+else
+{}
+?>
\ No newline at end of file
diff --git a/Website_ASU2024/login/page.php b/Website_ASU2024/login/page.php
new file mode 100644
index 0000000..433c56e
--- /dev/null
+++ b/Website_ASU2024/login/page.php
@@ -0,0 +1,204 @@
+query("SELECT * FROM users WHERE username='".$username."'");
+ $responseArray = $response->fetchArray();
+ if($responseArray['status']=='BANNED' && $responseArray['user_level']!=0)
+ {
+ return false;
+ }
+ elseif($responseArray['password']==$password)
+ {
+ $user_level = (int)$responseArray['user_level'];
+ $user_id = (int)$responseArray['user_id'];
+ $lane_id = (int)$responseArray['lane_id'];
+ #check to see if there are any sessions already active with this user_id
+ $response = $db->query("SELECT table_index FROM internet_sessions WHERE user_id='".$user_id."'");
+ $responseArray = $response->fetchArray();
+
+ #had to add this code for truthy check due to php's interpretation of 0 as NULL (i.e. the first index of the table is 0)
+ $response2 = $db->query("SELECT 1 FROM internet_sessions WHERE user_id='".$user_id."'");
+ $responseArray2 = $response2->fetchArray();
+ #
+ #Must close database temporarily so process write mutex lock for the database can be given to global function
+ $db->close();
+ if($responseArray2)
+ {
+ global_removeInternetSession($responseArray['table_index']);
+ sleep(2); #it takes at most 2 seconds for the daemon to remove the session
+ }
+ if($user_level=='0')
+ {
+ $sessionAccessBit = 1;
+ }
+ else if($user_level=='1')
+ {
+ $sessionAccessBit = 0;
+ }
+ $db = global_createDatabaseObj();
+ $response = $db->query("SELECT MAX(table_index) FROM internet_sessions");
+ $responseArray = $response->fetchArray();
+
+ #had to add this code for truthy check due to php's interpretation of 0 as NULL (i.e. the first index of the table is 0)
+ $response2 = $db->query("SELECT 1 FROM internet_sessions");
+ $responseArray2 = $response2->fetchArray();
+ #
+ if($responseArray2)
+ {
+ $internetSessionIndex=((int)$responseArray[0])+1;
+ }
+ else
+ {
+ $internetSessionIndex=0;
+ }
+ $datetimeObj = new DateTime();
+ $datetime = $datetimeObj->format('Y-m-d H:i:s');
+ $db->exec("INSERT INTO internet_sessions (
+ table_index,
+ user_id,
+ session_id,
+ ip,
+ session_tx,
+ session_rx,
+ session_access,
+ datetime_created,
+ datetime_sinceLastRequest,
+ pending_deletion)
+ VALUES(
+ ".$internetSessionIndex.",
+ ".$user_id.",
+ '".$session_id."',
+ '".$_SERVER['REMOTE_ADDR']."',
+ 0,0,
+ ".$sessionAccessBit.",
+ '".$datetime."',
+ '".$datetime."',
+ 0)");
+ $response = $db->query("SELECT MAX(session_number) FROM user_data_usage WHERE user_id='".$user_id."'");
+ $responseArray = $response->fetchArray();
+
+ #had to add this code for truthy check due to php's interpretation of 0 as NULL (i.e. the first index of the table is 0)
+ $response2 = $db->query("SELECT 1 FROM user_data_usage WHERE user_id=".$user_id);
+ $responseArray2 = $response2->fetchArray();
+ #
+ if($responseArray2)
+ {
+ $this_session_number=((int)$responseArray[0])+1;
+ $db->exec("INSERT INTO user_data_usage (
+ user_id,
+ session_number,
+ session_entry_index,
+ entry_datetime,
+ interval_bytes_tx,
+ interval_bytes_rx)
+ VALUES(
+ ".$user_id.",
+ ".$this_session_number.",
+ 0,
+ '".$datetime."',
+ 0,
+ 0)");
+ }
+ else
+ {
+ $db->exec("INSERT INTO user_data_usage (
+ user_id,
+ session_number,
+ session_entry_index,
+ entry_datetime,
+ interval_bytes_tx,
+ interval_bytes_rx)
+ VALUES(
+ ".$user_id.",
+ 0,
+ 0,
+ '".$datetime."',
+ 0,
+ 0)");
+ }
+ $db->close();
+ return true;
+ }
+ $db->close();
+ return false;
+}
+function validSessionID($session_id)
+{
+ $db = global_createDatabaseObj();
+ $response = $db->query("SELECT 1 FROM internet_sessions WHERE session_id='".$session_id."'");
+ $result = $response->fetchArray();
+
+ if($result)
+ {
+ $db->close();
+ return true;
+ }
+ else
+ {
+ $db->close();
+ return false;
+ }
+}
+
+function displayPage()
+{
+ echo '
+
+
+ Cybercafe Demo
+
+
+
+
+
+
+ '.$GLOBALS['defaultNavHTML'].'
+ Log-In
+
+
+ ';
+}
+
+if(isset($_COOKIE['session_id'])&&validSessionID($_COOKIE['session_id']))
+{
+ header('Location: ../home');
+ die();
+}
+else
+{
+ if(isset($_POST['username'])&&isset($_POST['password']))
+ {
+ $session_id = session_create_id();
+ if(logIn($_POST['username'],$_POST['password'],$session_id))
+ {
+ setcookie("session_id",$session_id,time()+$GLOBALS['sessionTimeOut'],"/");
+ header('Location: ../home');
+ die();
+ }
+ else
+ {
+ displayPage();
+ echo "Login Failed!";
+ }
+ }
+ else
+ {
+ displayPage();
+ }
+}
+?>
diff --git a/Website_ASU2024/logout/page.php b/Website_ASU2024/logout/page.php
new file mode 100644
index 0000000..de0ace5
--- /dev/null
+++ b/Website_ASU2024/logout/page.php
@@ -0,0 +1,21 @@
+query("SELECT * FROM internet_sessions WHERE session_id='".$_COOKIE['session_id']."'");
+ $responseArray = $response->fetchArray();
+ $db->close();
+ if($response)
+ {
+ global_removeInternetSession($responseArray['table_index']);
+ }
+ setcookie('session_id', '', time()-3600, '/');
+ header('Refresh:0');
+}
+else
+{
+ header('Location: /');
+}
+?>
\ No newline at end of file
diff --git a/Website_ASU2024/page.php b/Website_ASU2024/page.php
new file mode 100644
index 0000000..c49cc21
--- /dev/null
+++ b/Website_ASU2024/page.php
@@ -0,0 +1,3 @@
+
\ No newline at end of file