"/sd/portals/", "internal" => "/root/portals/"); private $BASE_EP_COMMAND = 'module EvilPortal'; // CONSTANTS /** * An implementation of the route method from Module. * This method routes the request to the method that handles it. */ public function route() { $this->createPortalFolders(); // if the portal folders (/root/portals | /sd/portals) do not exist create them. switch ($this->request->action) { case 'status': $this->response = array( "running" => $this->checkEvilPortalRunning(), "startOnBoot" => $this->checkAutoStart(), "sdAvailable" => $this->isSDAvailable() ); break; case 'writeFileContent': $this->response = $this->writeFileContents($this->request->filePath, $this->request->content, $this->request->append); break; case 'deletePortal': case 'deleteDirectory': case 'deleteFile': $this->response = $this->deleteFileOrDirectory($this->request->filePath); break; case 'getDirectoryContent': case 'getFileContent': $this->response = $this->getFileOrDirectoryContents($this->request->filePath); break; case 'download': $this->response = $this->download($this->request->filePath); break; case 'listAvailablePortals': $this->response = $this->getListOfPortals(); break; case 'createNewPortal': $this->response = $this->createNewPortal($this->request->name, $this->request->type, $this->request->storage); break; case 'movePortal': $this->response = $this->movePortal($this->request->name, $this->request->storage); break; case 'activatePortal': $this->response = $this->activatePortal($this->request->name, $this->request->storage); break; case 'deactivatePortal': $this->response = $this->deactivatePortal($this->request->name, $this->request->storage); break; case 'getRules': $this->response = $this->getPortalRules($this->request->name, $this->request->storage); break; case 'saveRules': $this->response = $this->savePortalRules($this->request->name, $this->request->storage, $this->request->rules); break; case 'toggleOnBoot': $this->response = $this->autoStartEvilPortal(); break; case 'toggleCaptivePortal': $this->response = $this->toggleCaptivePortal(); break; case 'removeClientFromList': $this->response = $this->removeFromList($this->request->clientIP, $this->request->listName); break; case 'authorizeClient': $this->authorizeClient($this->request->clientIP); $this->response = array("success" => true); break; } } /** * Create the folders that portals are stored in if they don't exist */ private function createPortalFolders() { if (!is_dir($this->STORAGE_LOCATIONS["internal"])) { mkdir($this->STORAGE_LOCATIONS["internal"]); } if (!is_dir($this->STORAGE_LOCATIONS["sd"]) and $this->isSDAvailable()) { mkdir($this->STORAGE_LOCATIONS["sd"]); } } /** * Decide if a file for a given portal is "deletable" or not. * If it is not then the UI should not display a delete option for the file. * @param $file: The name of the file * @return bool: Is the file deletable or not */ private function isFileDeletable($file) { if (substr($file, -strlen(".ep")) == ".ep") return false; return !in_array($file, array("MyPortal.php", "default.php", "helper.php", "index.php")); } /** * Get the contents of a specified file or directory * * If this method is being called as the result of an HTTP request, make sure that "file" is specified as a * parameter of the request and includes the full path to the file that should have its contents returned. * * @param $file : The file or directory to get contents of * @return array */ private function getFileOrDirectoryContents($file) { if (!file_exists($file)) { $message = "No such file or directory {$file}."; $contents = null; $success = false; } else if (is_file($file)) { $message = "Found file {$file} and retrieved contents"; $contents = array( "name" => basename($file), "path" => $file, "size" => $this->readableFileSize($file), "fileContent" => file_get_contents($file) ); $success = true; } else if (is_dir($file)) { $contents = array(); $message = "Returning directory contents for {$file}"; foreach (preg_grep('/^([^.])/', scandir($file)) as $object) { // skip .ep files because they shouldn't be edited directly. if (substr($object, -strlen(".ep")) == ".ep") continue; $obj = array("name" => $object, "directory" => is_dir("{$file}/{$object}"), "path" => realpath("{$file}/{$object}"), "permissions" => substr(sprintf('%o', fileperms("{$file}/{$object}")), -4), "size" => $this->readableFileSize("{$file}/{$object}"), "deletable" => $this->isFileDeletable($object)); array_push($contents, $obj); } $success = true; } else { $contents = null; $success = false; $message = "Unknown case. This should never happen."; } return array("success" => $success, "message" => $message, "content" => $contents); } /** * Write given content to a given file. * @param $file : The file to write content to * @param $content : The content to write to the file * @param $append : Should the data be appended to the end of the file (true) or over-write the file (false) * @return array */ private function writeFileContents($file, $content, $append) { if ($append) file_put_contents($file, $content, FILE_APPEND); else file_put_contents($file, $content); return array("success" => true, "message" => null); } /** * Delete a given file or directory and check if it has been deleted. * If the file was deleted the success will be true otherwise success is false * @param $filePath * @return array */ private function deleteFileOrDirectory($filePath) { if ($this->isFileDeletable(basename($filePath))) { exec(escapeshellcmd("rm -rf {$filePath}")); $success = (!file_exists($filePath)); $message = (file_exists($filePath)) ? "Error deleting file {$filePath}." : "{$filePath} has been deleted."; } else { $success = false; $message = "{$filePath} can not be deleted!"; } return array("success" => $success, "message" => $message); } /** * Download a file * @param: The path to the file to download * @return array : array */ private function download($filePath) { if (file_exists($filePath)) { return array("success" => true, "message" => null, "download" => $this->downloadFile($filePath)); } else { return array("success" => false, "message" => "File does not exist", "download" => null); } } /** * Get a list of portals found on internal and sd storage. */ private function getListOfPortals() { // an array of all of the portals found $portals = array(); $availableMediums = array("internal"); // if the sd card is available add it to the availableMediums if ($this->isSDAvailable()) { array_push($availableMediums, "sd"); } foreach($availableMediums as $medium) { $storageLocation = $this->STORAGE_LOCATIONS[$medium]; foreach (preg_grep('/^([^.])/', scandir($storageLocation)) as $object) { if (!is_dir($storageLocation . $object)) // skip the object if it is not a directory. continue; $portal = array( "title" => $object, "portalType" => $this->getValueFromJSONFile(array("type"), "{$storageLocation}{$object}/{$object}.ep")["type"], "size" => $this->readableFileSize("{$storageLocation}{$object}"), "location" => "{$storageLocation}{$object}", "storage" => $medium, "active" => (file_exists("/www/{$object}.ep")) ); // push the portal object to the array of portals found array_push($portals, $portal); } } return array("success" => true, "portals" => $portals); } /** * Create a new Portal with a given name of a given type on a given storage medium. * @param $name : The name of the new portal * @param $type : The type of portal to create (targeted or basic) * @param $storage : The storage medium to save the portal to (sd or internal) * @return array */ private function createNewPortal($name, $type, $storage) { // force the name of the portal to be lower cased and replace spaces with underscores $name = strtolower(str_replace(' ', '_', $name)); // $storage should be equal to "sd" or "internal". If its anything else just make it "internal" $storage = ($storage == "sd" or $storage == "internal") ? $storage : "internal"; // the path to store the portal $portalPath = $this->STORAGE_LOCATIONS[$storage]; // verify that no portal with the same name already exists if (file_exists("{$this->STORAGE_LOCATIONS["internal"]}{$name}") or (file_exists("{$this->STORAGE_LOCATIONS["sd"]}{$name}") and $this->isSDAvailable())) { return array("success" => false, "message" => "A portal named {$name} already exists!"); } // if the portal is supposed to be stored on the SD card, make sure that it is indeed available first. if ($storage == "sd" and !$this->isSDAvailable()) { return array("success" => false, "message" => "There is no SD card available!"); } // create the directory for the portal mkdir($portalPath . $name); // handle the portal types. If anything other than "targeted" is specified then it will create a basic portal switch ($type) { case 'targeted': exec("cp /pineapple/modules/EvilPortal/includes/targeted_skeleton/* {$portalPath}{$name}/"); exec("cp /pineapple/modules/EvilPortal/includes/targeted_skeleton/.* {$portalPath}{$name}/"); exec("mv {$portalPath}{$name}/portalinfo.json {$portalPath}{$name}/{$name}.ep"); $this->updateJSONFile(array("name" => $name, "type" => "targeted"), "{$portalPath}{$name}/{$name}.ep"); exec("sed -i 's/\"portal_name_here\"/\"{$name}\"/g' {$portalPath}{$name}/index.php"); break; default: exec("cp /pineapple/modules/EvilPortal/includes/skeleton/* {$portalPath}{$name}/"); exec("cp /pineapple/modules/EvilPortal/includes/skeleton/.* {$portalPath}{$name}/"); exec("mv {$portalPath}{$name}/portalinfo.json {$portalPath}{$name}/{$name}.ep"); $this->updateJSONFile(array("name" => $name, "type" => "basic"), "{$portalPath}{$name}/{$name}.ep"); break; } // make these scripts executable exec("chmod +x {$portalPath}{$name}/.enable"); exec("chmod +x {$portalPath}{$name}/.disable"); return array("success" => true, "message" => "Created {$type} portal {$name}!"); } /** * Move a portal between one storage medium to another. * * If the current medium is "internal" then the portal will be moved to "sd" and visa-versa * * @param $name : The name of the portal to move * @param $storage : The current storage medium * @return array */ private function movePortal($name, $storage) { $storage = ($storage == "internal" || $storage == "sd") ? $storage : "internal"; $newMedium = ($storage == "internal") ? "sd" : "internal"; $newStorage = $this->STORAGE_LOCATIONS[$newMedium]; // active portals should not be moved so check if the portal is currently active if (file_exists("/www/{$name}.ep")) { return array("success" => false, "message" => "You can not move an active portal!"); } else // make sure that an SD card is inserted if it is going to be needed if (($storage == "sd" || $newMedium == "sd") && !$this->isSDAvailable()) { return array("success" => false, "message" => "Please insert a SD card to preform this action."); } // if the portal doesn't exist then return an error if (!file_exists($this->STORAGE_LOCATIONS[$storage] . $name)) { return array("success" => false, "message" => "Could not find portal named {$name} on {$storage} storage"); } // verify that a portal with the same name doesn't already exist in the new location. if (file_exists($newStorage . $name)) { return array("success" => false, "message" => "A portal named {$name} already exists on {$newMedium} storage"); } // all of the above conditions should have passed so lets move the damn portal. exec(escapeshellcmd("mv {$this->STORAGE_LOCATIONS[$storage]}{$name} {$newStorage}{$name}")); // verify that the directory was moved if (file_exists($newStorage . $name)) { return array("success" => true, "message" => "{$name} was moved to {$newMedium} storage!"); } else { return array("success" => false, "message" => "An error occurred moving {$name} to {$newMedium} storage"); } } /** * Set a given portal to "active". * This means to move the portals contents to /www so it can be access via HTTP port 80. * * If any file with the same name as one of the files being copied to /www already exists in /www * then that file will be renamed to {file_name}.ep_backup and restored when the portal is deactivated. * * If any portals are currently active when this method is called they will be deactivated. * * @param $name: The name of the portal to activate * @param $storage: The storage medium the portal is on * @return array */ private function activatePortal($name, $storage) { $dir = $this->STORAGE_LOCATIONS[$storage]; // check if there is a currently activate portal and deactivate it. foreach(scandir("/www") as $file) { if (substr($file, strlen($file) - strlen(".ep")) === ".ep") { // deactivate a portal if needed $portalName = rtrim($file, ".ep"); $realPath = realpath("/www/{$file}"); $storage = ($realPath == "{$this->STORAGE_LOCATIONS['internal']}{$portalName}/{$portalName}.ep") ? "internal": "sd"; $this->deactivatePortal($portalName, $storage); break; } } $success = false; $portalPath = escapeshellarg($dir . $name); if (file_exists($dir . $name)) { exec("ln -s /pineapple/modules/EvilPortal/includes/api /www/captiveportal"); $portal_files = scandir($dir . $name); foreach ($portal_files as $file) { if (file_exists("/www/{$file}")) { rename("/www/{$file}", "/www/{$file}.ep_backup"); } exec("ln -s {$portalPath}/{$file} /www/{$file}"); $success = true; } // holding off on toggle commands until a future release. // exec("echo {$portalPath}/.enable | at now"); $message = "{$name} is now active."; } else { $message = "Couldn't find {$portalPath}."; } return array("message" => $message, "success" => $success); } /** * Deactivate a given portal. * * To do this we remove all files associated with the given portal from /www * This method also renames any files with the extension ".ep_backup" to their original name * * @param $name: The name of the portal to deactivate * @param $storage: The storage medium the portal is on * @return array */ private function deactivatePortal($name, $storage) { $storage = ($storage == "internal" || $storage == "sd") ? $storage : "internal"; $dir = $this->STORAGE_LOCATIONS[$storage]; // if the portal is not active then return an error if (!(file_exists("/www/{$name}.ep"))) { return array("success" => false, "message" => "{$name} is not currently active."); } // if the portal does not exist then return an error if (!file_exists($dir . $name)) { return array("success" => false, "message" => "Unable to find the portal {$name}."); } // remove portal files from /www foreach(scandir($dir. $name) as $file) { unlink("/www/{$file}"); } // rename any files that may have been renamed back to their original name foreach(scandir("/www/") as $file) { if (substr($file, strlen($file) - strlen(".ep_backup")) === ".ep_backup") { $oldName = str_replace(".ep_backup", "", $file); rename("/www/{$file}", "www/{$oldName}"); } } // holding off on toggle commands until a future release. // exec("echo {$dir}{$name}/.disable | at now"); return array("success" => true, "message" => "Deactivated {$name}."); } /** * Attempt to get rules for a targeted portal * @param $name: The name of the portal * @param $storage: The storage medium of the portal * @return array */ private function getPortalRules($name, $storage) { $storage = ($storage == "internal" || $storage == "sd") ? $storage : "internal"; $path = $this->STORAGE_LOCATIONS[$storage]; if (is_file("{$path}{$name}/{$name}.ep")) { $rules = $this->getValueFromJSONFile(array("targeted_rules"), "{$path}{$name}/{$name}.ep")["targeted_rules"]; return array( "message" => "Found portal rules", "data" => $rules, "success" => true ); } else { return array("message" => "Unable to find portal.", "success" => false); } } /** * Save rules to a targeted portal * @param $name : The name of the portal * @param $storage : The storage medium of the portal * @param $rules : The rules to save * @return array */ private function savePortalRules($name, $storage, $rules) { $storage = ($storage == "internal" || $storage == "sd") ? $storage : "internal"; $path = $this->STORAGE_LOCATIONS[$storage]; if (is_file("{$path}{$name}/{$name}.ep")) { $this->updateJSONFile(array("targeted_rules" => json_decode($rules)), "{$path}{$name}/{$name}.ep")["targeted_rules"]; return array( "message" => "Saved portal rules", "success" => true ); } else { return array("message" => "Unable to find portal {$name}.", "success" => false); } } /** * Check if Evil Portal is currently running or not be checking iptables. * @return bool */ private function checkEvilPortalRunning() { return exec("iptables -t nat -L PREROUTING | grep 172.16.42.1") != ''; } /** * Check if EvilPortal is running when the Pineapple starts or not * @return bool */ public function checkAutoStart() { return !(exec("ls /etc/rc.d/ | grep evilportal") == ''); } /** * Grant a client access to the internet and stop blocking them with the captive portal * @param $client: The IP address of the client to authorize */ private function authorizeClient($client) { exec("iptables -t nat -I PREROUTING -s {$client} -j ACCEPT"); // exec("{$this->BASE_EP_COMMAND} add {$client}"); $this->writeFileContents($this->CLIENTS_FILE, "{$client}", true); } /** * Revoke a clients access to the internet and start blocking them with the captive portal * @param $client: The IP address of the client to revoke */ private function revokeClient($client) { // exec("{$this->BASE_EP_COMMAND} remove {$client}"); exec("iptables -t nat -D PREROUTING -s {$client}"); exec("iptables -t nat -D PREROUTING -s {$client} -j ACCEPT"); } /** * Start the captive portal portion of Evil Portal * * All clients in the White List should be automatically authorized when Evil Portal starts. * * @return array */ private function startEvilPortal() { // Delete client tracking file if it exists if (file_exists($this->CLIENTS_FILE)) { unlink($this->CLIENTS_FILE); } // Enable forwarding. It should already be enabled on the pineapple but do it anyways just to be safe exec("echo 1 > /proc/sys/net/ipv4/ip_forward"); exec("ln -s /pineapple/modules/EvilPortal/includes/api /www/captiveportal"); // Insert allowed clients into tracking file $allowedClients = file_get_contents($this->ALLOWED_FILE); file_put_contents($this->CLIENTS_FILE, $allowedClients); // Configure other rules exec("iptables -A INPUT -s 172.16.42.0/24 -j DROP"); exec("iptables -A OUTPUT -s 172.16.42.0/24 -j DROP"); exec("iptables -A INPUT -s 172.16.42.0/24 -p udp --dport 53 -j ACCEPT"); // Allow the pineapple exec("iptables -A INPUT -s 172.16.42.1 -j ACCEPT"); exec("iptables -A OUTPUT -s 172.16.42.1 -j ACCEPT"); //exec("iptables -A INPUT -i br-lan -p tcp --dport 443 -j DROP"); //exec("iptables -t nat -A PREROUTING -i br-lan -j DROP"); exec("iptables -t nat -A PREROUTING -i br-lan -p tcp --dport 80 -j DNAT --to-destination 172.16.42.1:80"); exec("iptables -t nat -A POSTROUTING -j MASQUERADE"); // exec("{$this->BASE_EP_COMMAND} init"); // Add rule for each allowed client $lines = file($this->CLIENTS_FILE); foreach ($lines as $client) { $this->authorizeClient($client); } $success = $this->checkEvilPortalRunning(); $message = ($success) ? "EvilPortal is now up and running!" : "EvilPortal failed to start."; return array("success" => $success, "message" => $message); } /** * Stop the captive portal portion of Evil Portal from running * @return mixed */ private function stopEvilPortal() { if (file_exists($this->CLIENTS_FILE)) { $lines = file($this->CLIENTS_FILE); foreach ($lines as $client) { $this->revokeClient($client); } unlink($this->CLIENTS_FILE); } exec("iptables -t nat -D PREROUTING -i br-lan -p tcp --dport 80 -j DNAT --to-destination 172.16.42.1:80"); exec("iptables -D INPUT -p tcp --dport 53 -j ACCEPT"); exec("iptables -D INPUT -j DROP"); // exec("{$this->BASE_EP_COMMAND} purge"); $success = !$this->checkEvilPortalRunning(); $message = ($success) ? "EvilPortal has stopped running" : "There was an issue stopping EvilPortal"; return array("success" => $success, "messsage" => $message); } /** * If Evil Portal is running then stop it, otherwise start it. */ private function toggleCaptivePortal() { // Make the file executable. In the future the `module` command should do this for us. chmod("/pineapple/modules/EvilPortal/executable/executable", 0755); return $this->checkEvilPortalRunning() ? $this->stopEvilPortal() : $this->startEvilPortal(); } /** * Enable or Disable Evil Portal to run when the pineapple boots. * If Evil Portal is supposed to run when this method is called then it will be disabled * If Evil Portal is not supposed to run when this method is called then it will be enabled on boot. * * This method does not start nor stop current running instances of Evil Portal. * * @return array */ private function autoStartEvilPortal() { // if EvilPortal is not set to start on startup then set that shit if (!$this->checkAutoStart()) { copy("/pineapple/modules/EvilPortal/includes/evilportal.sh", "/etc/init.d/evilportal"); chmod("/etc/init.d/evilportal", 0755); exec("/etc/init.d/evilportal enable"); $enabled = $this->checkAutoStart(); $message = ($enabled) ? "EvilPortal is now enabled on start up" : "Error enabling EvilPotal on startup."; return array( "success" => $enabled, "message" => $message ); } else { // if evil portal is set to run on startup then disable that shit. exec("/etc/init.d/evilportal disable"); $enabled = !$this->checkAutoStart(); $message = ($enabled) ? "EvilPortal is now disabled on startup." : "Error disabling EvilPortal on startup."; return array( "success" => $enabled, "message" => $message ); } } /** * Removes a client from either the whiteList or authorizedList * @param $clientIP: The IP address of the client to be removed * @param $listName: The name of the list to remove the client from * @return array */ private function removeFromList($clientIP, $listName) { $valid = preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/', $clientIP); // if the IP address is invalid then return an error message if (!$valid) { return array("success" => false, "message" => "Invalid IP Address."); } $success = true; switch ($listName) { case "whiteList": $data = file_get_contents($this->ALLOWED_FILE); $data = str_replace("{$clientIP}\n", '', $data); file_put_contents("/root/removeFromList", $data); file_put_contents($this->ALLOWED_FILE, $data); break; case "accessList": $data = file_get_contents($this->CLIENTS_FILE); $data = str_replace("{$clientIP}\n", '', $data); file_put_contents($this->CLIENTS_FILE, $data); $this->revokeClient($clientIP); break; default: $success = false; break; } $message = ($success) ? "Successfully removed {$clientIP} from {$listName}" : "Error removing {$clientIP} from {$listName}"; return array("success" => $success, "message" => $message); } /** * Add a value to a json file * @param $keyValueArray: The data to add to the file * @param $file: The file to write the content to. */ private function updateJSONFile($keyValueArray, $file) { $data = json_decode(file_get_contents($file), true); foreach ($data as $key => $value) { if (isset($keyValueArray[$key])) { $data[$key] = $keyValueArray[$key]; } } file_put_contents($file, json_encode($data)); } /** * Get values from a JSON file * @param $keys: The key or keys you wish to get the value from * @param $file: The file to that contains the JSON data. * @return array */ private function getValueFromJSONFile($keys, $file) { $data = json_decode(file_get_contents($file), true); $values = array(); foreach ($data as $key => $value) { if (in_array($key, $keys)) { $values[$key] = $value; } } return $values; } /** * Get the size of a file and add a unit to the end of it. * @param $file: The file to get size of * @return string: File size plus unit. Exp: 3.14M */ private function readableFileSize($file) { $size = filesize($file); if ($size == null) return "0 Bytes"; if ($size < 1024) { return "{$size} Bytes"; } else if ($size >= 1024 && $size < 1024*1024) { return round($size / 1024, 2) . "K"; } else if ($size >= 1024*1024) { return round($size / (1024*1024), 2) . "M"; } return "{$size} Bytes"; } }