├── EvilPortal ├── data │ └── allowed.txt ├── includes │ ├── skeleton │ │ ├── portalinfo.json │ │ ├── .disable │ │ ├── .enable │ │ ├── MyPortal.php │ │ ├── index.php │ │ └── helper.php │ ├── api │ │ ├── index.php │ │ ├── API.php │ │ └── Portal.php │ ├── targeted_skeleton │ │ ├── .disable │ │ ├── .enable │ │ ├── portalinfo.json │ │ ├── MyPortal.php │ │ ├── default.php │ │ ├── helper.php │ │ └── index.php │ └── evilportal.sh ├── module.info ├── executable │ └── executable ├── js │ └── module.js ├── api │ └── module.php └── module.html ├── .gitignore ├── README.md └── LICENSE /EvilPortal/data/allowed.txt: -------------------------------------------------------------------------------- 1 | 172.16.42.42 2 | -------------------------------------------------------------------------------- /EvilPortal/includes/skeleton/portalinfo.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": null, 3 | "type": "basic" 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .DS_Store 3 | EvilPortal/.DS_Store 4 | EvilPortal/sftp-config.json 5 | /EvilPortal.tar.gz 6 | /sftp-config.json 7 | -------------------------------------------------------------------------------- /EvilPortal/includes/api/index.php: -------------------------------------------------------------------------------- 1 | go(); 8 | -------------------------------------------------------------------------------- /EvilPortal/includes/skeleton/.disable: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Commands in this file are ran when a portal is de-activated. 4 | # You can use any interpreter you want to, the default is bash. 5 | -------------------------------------------------------------------------------- /EvilPortal/includes/targeted_skeleton/.disable: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Commands in this file are ran when a portal is de-activated. 4 | # You can use any interpreter you want to, the default is bash. 5 | -------------------------------------------------------------------------------- /EvilPortal/includes/skeleton/.enable: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Commands in this file are ran when a portal is activated and when Evil Portal startsup on boot. 4 | # You can use any interpreter you want to, the default is bash. 5 | -------------------------------------------------------------------------------- /EvilPortal/module.info: -------------------------------------------------------------------------------- 1 | { 2 | "author": "newbi3", 3 | "description": "An Evil Captive Portal.", 4 | "devices": [ 5 | "nano", 6 | "tetra" 7 | ], 8 | "title": "Evil Portal", 9 | "version": "3.1" 10 | } -------------------------------------------------------------------------------- /EvilPortal/includes/targeted_skeleton/.enable: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Commands in this file are ran when a portal is activated and when Evil Portal startsup on boot. 4 | # You can use any interpreter you want to, the default is bash. 5 | -------------------------------------------------------------------------------- /EvilPortal/includes/targeted_skeleton/portalinfo.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": null, 3 | "type": "targeted", 4 | "targeted_rules": { 5 | "default": "default.php", 6 | "rule_order": ["mac", "ssid", "hostname", "useragent"], 7 | "rules": { 8 | "mac": { 9 | "exact": { 10 | }, 11 | "regex": { 12 | } 13 | }, 14 | "ssid": { 15 | "exact": { 16 | }, 17 | "regex": { 18 | } 19 | }, 20 | "hostname": { 21 | "exact": { 22 | }, 23 | "regex": { 24 | } 25 | }, 26 | "useragent": { 27 | "exact": { 28 | }, 29 | "regex": { 30 | } 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /EvilPortal/includes/evilportal.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh /etc/rc.common 2 | 3 | # This is the auto-start script for EvilPortal 4 | 5 | START=200 6 | 7 | start() { 8 | # Enable ip forward. 9 | echo 1 > /proc/sys/net/ipv4/ip_forward 10 | # Create symlink 11 | ln -s /pineapple/modules/EvilPortal/includes/api /www/captiveportal 12 | # Run iptables commands 13 | iptables -t nat -A PREROUTING -s 172.16.42.0/24 -p tcp --dport 80 -j DNAT --to-destination 172.16.42.1:80 14 | iptables -A INPUT -p tcp --dport 53 -j ACCEPT 15 | } 16 | 17 | stop() { 18 | rm /www/captiveportal 19 | iptables -t nat -D PREROUTING -s 172.16.42.0/24 -p tcp --dport 80 -j DNAT --to-destination 172.16.42.1:80 20 | iptables -D INPUT -p tcp --dport 53 -j ACCEPT 21 | iptables -D INPUT -j DROP 22 | } 23 | 24 | disable() { 25 | rm /etc/rc.d/*evilportal 26 | } 27 | -------------------------------------------------------------------------------- /EvilPortal/includes/skeleton/MyPortal.php: -------------------------------------------------------------------------------- 1 | request = (object)$_POST; 11 | } 12 | 13 | public function route() 14 | { 15 | $portalPath = "/www/MyPortal.php"; 16 | $portalClass = "evilportal\\MyPortal"; 17 | 18 | if (!file_exists($portalPath)) { 19 | $this->error = "MyPortal.php does not exist in {$portalPath}"; 20 | return; 21 | } 22 | 23 | require_once("Portal.php"); 24 | require_once($portalPath); 25 | 26 | if (!class_exists($portalClass)) { 27 | $this->error = "The class {$portalClass} does not exist in {$portalPath}"; 28 | return; 29 | } 30 | 31 | $portal = new $portalClass($this->request); 32 | $portal->handleAuthorization(); 33 | $this->response = $portal->getResponse(); 34 | } 35 | 36 | public function finalize() 37 | { 38 | if ($this->error) { 39 | return json_encode(array("error" => $this->error)); 40 | } elseif ($this->response) { 41 | return json_encode($this->response); 42 | } 43 | } 44 | 45 | public function go() 46 | { 47 | $this->route(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /EvilPortal/includes/targeted_skeleton/default.php: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | Evil Portal 9 | 10 | 11 | 12 | 15 | 16 | 17 | 18 |
19 |

Evil Portal

20 |

This is the default Evil Portal page.

21 |

The SSID you are connected to is

22 |

Your host name is

23 |

Your MAC Address is

24 |

Your internal IP address is

25 | 26 |
27 | 28 | 29 |
30 | 31 |
32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /EvilPortal/includes/skeleton/index.php: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | Evil Portal 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |

Evil Portal

18 |

This is the default Evil Portal page.

19 |

The SSID you are connected to is

20 |

Your host name is

21 |

Your MAC Address is

22 |

Your internal IP address is

23 | 24 |
25 | 26 | 27 |
28 | 29 |
30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /EvilPortal/includes/skeleton/helper.php: -------------------------------------------------------------------------------- 1 | getClientMac($_SERVER['REMOTE_ADDR']), 22 | "ssid" => getClientSSID($_SERVER['REMOTE_ADDR']), 23 | "hostname" => getClientHostName($_SERVER['REMOTE_ADDR']), 24 | "useragent" => $_SERVER['HTTP_USER_AGENT'] 25 | ]; 26 | 27 | // Read the json 28 | $jsonData = json_decode(file_get_contents("{$PORTAL_NAME}.ep"), true); 29 | $routeData = $jsonData['targeted_rules']; 30 | 31 | // This variable represents the page to include 32 | $includePage = null; 33 | 34 | // Check rules to find the page 35 | foreach ($routeData['rule_order'] as $key) { 36 | $includePage = handle_rule($routeData['rules'][$key], $MAPPED_RULES[$key]); 37 | if ($includePage != null) { 38 | include $includePage; 39 | break; 40 | } 41 | } 42 | 43 | // We have to display something. 44 | // If the includePage variable is still null after checking the rules 45 | // then include the default page. 46 | if ($includePage == null) { 47 | include $routeData['default']; 48 | } 49 | 50 | /** 51 | * Checks if a given rule matches a given value 52 | * @param $rules: The rules to check the client data against 53 | * @param $client_data: The data to check if the rules match 54 | * @return string: If a rule matches it returns the page to include, null otherwise 55 | */ 56 | function handle_rule($rules, $client_data) { 57 | $return_value = null; 58 | foreach ($rules as $key => $val) { 59 | switch($key) { 60 | case "exact": // exact matches 61 | if (isset($val[$client_data])) { 62 | $return_value = $val[$client_data]; 63 | break 2; // break out of the loop 64 | } 65 | break 1; 66 | 67 | case "regex": // regex matches 68 | foreach($val as $expression => $destination) { 69 | if (preg_match($expression, $client_data)) { 70 | $return_value = $destination; 71 | break 1; // match was found. Exit this loop 72 | } 73 | 74 | if ($return_value != null) 75 | break 2; // break out of the main loop 76 | } 77 | break 1; 78 | } 79 | } 80 | return $return_value; 81 | } 82 | -------------------------------------------------------------------------------- /EvilPortal/executable/executable: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #Modified by oXis for the Wifi Pineapple (OpenWRT) 4 | 5 | # Written by Sitwon and The Doctor. 6 | # Copyright (C) 2013 Project Byzantium 7 | # This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. 8 | 9 | arp () { cat /proc/net/arp; } # arp function 10 | 11 | IPTABLES=/usr/sbin/iptables 12 | ARP=arp 13 | IP=172.16.42.1 14 | 15 | case "$1" in 16 | 'init') 17 | 18 | # Convert the IP address of the client interface into a netblock. 19 | CLIENTNET=`echo $IP | sed 's/1$/0\/24/'` 20 | 21 | # Exempt traffic which does not originate from the client network. 22 | $IPTABLES -t mangle -I PREROUTING -p all ! -s $CLIENTNET -j RETURN 23 | 24 | # Traffic not coming from an accepted user gets marked 99. 25 | $IPTABLES -t mangle -A fwmark -j MARK --set-mark 99 26 | 27 | # Traffic which has been marked 99 and is headed for 80/TCP or 443/TCP 28 | # should be redirected to the captive portal web server. 29 | $IPTABLES -t nat -A prerouting_rule -m mark --mark 99 -p tcp --dport 80 -j DNAT --to-destination $IP:80 30 | # Need to activate HTTPS on the nginx server of the PineAP, so for now HTTPS traffic is dropped. 31 | #$IPTABLES -t nat -A prerouting_rule -m mark --mark 99 -p tcp --dport 443 -j DNAT --to-destination $IP:443 32 | 33 | # for use with dns spoff 34 | $IPTABLES -t filter -A forwarding_rule -p udp --dport 53 -j ACCEPT 35 | $IPTABLES -t nat -A prerouting_rule -m mark --mark 99 -p udp --dport 53 -j DNAT --to-destination $IP:53 36 | 37 | $IPTABLES -t filter -A input_rule -p tcp --dport 80 -j ACCEPT #Webserver 38 | #$IPTABLES -t filter -A input_rule -p tcp --dport 443 -j ACCEPT #Webserver 39 | $IPTABLES -t filter -A input_rule -p tcp --dport 1471 -j ACCEPT #PineAP admin page 40 | $IPTABLES -t filter -A input_rule -p tcp --dport 22 -j ACCEPT #SSH 41 | 42 | # All other traffic which is marked 99 is just dropped 43 | $IPTABLES -t filter -A forwarding_rule -m mark --mark 99 -j DROP 44 | # Even on INPUT rule 45 | $IPTABLES -t filter -A input_rule -m mark --mark 99 -j DROP 46 | 47 | exit 0 48 | ;; 49 | 'add') 50 | # $2: IP address of client. 51 | CLIENT=$2 52 | 53 | # Isolate the MAC address of the client in question. 54 | CLIENTMAC=`$ARP -n | grep ':' | grep $CLIENT | awk '{print $4}'` 55 | 56 | # Add the MAC address of the client to the whitelist, so it'll be able 57 | # to access the mesh even if its IP address changes. 58 | $IPTABLES -t mangle -I fwmark -m mac --mac-source $CLIENTMAC -j RETURN 59 | $IPTABLES -A INPUT -m mac --mac-source 74:da:38:5a:03:66 -p udp --dport 53 -j ACCEPT 60 | 61 | exit 0 62 | ;; 63 | 'remove') 64 | # $2: IP address of client. 65 | CLIENT=$2 66 | 67 | # Isolate the MAC address of the client in question. 68 | CLIENTMAC=`$ARP -n | grep ':' | grep $CLIENT | awk '{print $4}'` 69 | 70 | # Delete the MAC address of the client from the whitelist. 71 | $IPTABLES -t mangle -D fwmark -m mac --mac-source $CLIENTMAC -j RETURN 72 | 73 | exit 0 74 | ;; 75 | 'purge') 76 | CLIENTNET=`echo $IP | sed 's/1$/0\/24/'` 77 | # Purge the user defined chains 78 | $IPTABLES -t mangle -F fwmark 79 | $IPTABLES -t nat -F prerouting_rule 80 | $IPTABLES -t filter -F input_rule 81 | $IPTABLES -t filter -F forwarding_rule 82 | $IPTABLES -t mangle -D PREROUTING -p all ! -s $CLIENTNET -j RETURN 83 | 84 | $IPTABLES -t nat -D prerouting_rule -m mark --mark 99 -p udp --dport 53 -j DNAT --to-destination $IP:53 85 | 86 | exit 0 87 | ;; 88 | 'list') 89 | # Display the currently running IP tables ruleset. 90 | $IPTABLES --list -t nat -n 91 | $IPTABLES --list -t mangle -n 92 | $IPTABLES --list -t filter -n 93 | 94 | exit 0 95 | ;; 96 | *) 97 | echo "USAGE: $0 {initialize|add |remove |purge|list}" 98 | exit 0 99 | esac -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EvilPortal 2 | Evil Portal is a captive portal module for the [Hak5](https://www.hak5.org) [Wifi Pineapple](https://www.wifipineapple.com/). This is the repository for the [Wifi Pineapple Nano](http://hakshop.myshopify.com/products/wifi-pineapple?variant=81044992) and [Wifi Pineapple Tetra](http://hakshop.myshopify.com/products/wifi-pineapple?variant=11303845317). If you have a Wifi Pineapple MKV you can find the code for that version [here](https://github.com/frozenjava/evilportal). 3 | 4 | ## Overview 5 | 6 | ### Basic Portals 7 | Basic Portals allow you to create a simple captive portal page that is the same for everyone who visits it. This is useful if your needs don't involve different clients seeing different branded pages or pages with unique functionality to them. 8 | 9 | ### Targeted Portals 10 | Targeted Portals allow you to create different portals to target a specific device or groups of devices based upon your pre-defined conditions. This is incredibly useful if you want all android devices to go to one android themed portal and all clients who are connected to "some-coffee-shop-wifi" go to a different portal all together. Targeted portals currently let you create targeting rules based on mac addresses, ssids, hostnames, and http useragents all on a per-client basis. You can either specify exact string matches or regex matches. 11 | 12 | ## Manual Installation 13 | 14 | First clone the repo and checkout the development branch 15 | 16 | ``` 17 | git clone https://github.com/frozenjava/EvilPortalNano.git 18 | git checkout -b development origin/development 19 | git pull 20 | ``` 21 | 22 | Next change directory to EvilPortalNano 23 | 24 | ``` 25 | cd EvilPortalNano 26 | ``` 27 | 28 | Finally, with your Wifi Pineapple connected upload the EvilPortal directory to the Wifi Pineapple to the /pineapple/modules directory. 29 | 30 | ``` 31 | scp -r EvilPortal root@172.16.42.1:/pineapple/modules/ 32 | ``` 33 | 34 | Head on over to the Wifi Pineapples Web Interface and go to the Evil Portal module. You're all done! 35 | 36 | ## Useful Links 37 | [Official Hak5 Forum Thread](https://forums.hak5.org/index.php?/topic/37874-official-evilportal/) 38 | [Official Youtube Playlist](https://www.youtube.com/playlist?list=PLW7RuuSaPPzDgrZINbNkt4ujR7RDTUCMB) 39 | [My website: frozendevelopment.net](http://frozendevelopment.net/) 40 | 41 | ## Tasks for Upcoming Release 42 | 43 | If you want to contribute to the project feel free to tackle one of these tasks! 44 | 45 | ### TODO 46 | * Figure out how to redirect clients going to HTTPS sites 47 | * Add ability to program commands to run when a portal is enabled/disabled 48 | 49 | ## Release History 50 | 51 | ### Version 3.1 52 | * Added ability to write and view logs on a per-portal basis 53 | * Created method writeLog($message) that writes to the portal log file 54 | * Created method notify($message) that sends a notification to the web ui 55 | * Added ability to download files 56 | * Tab button in file editor will now insert four spaces 57 | * Revamped the file editor modal 58 | * Showing file sizes in the portal work bench 59 | * Various quality of life improvements 60 | 61 | ### Version 3.0 62 | * Add doc strings to all methods in module.php and functions in module.js 63 | * Get SSID of connected client by IP address 64 | * Add ability to route clients to different portals based upon some identifier [ssid, mac vendor, ip, etc...] 65 | * Update the work bench so users can choose between targeted and non-targeted portals 66 | * Create easy-to-use interface for creating targeting rules 67 | * Create some consistency throughout the UI 68 | * Add ability to create portals on an SD card and move between SD and Internal storage easily 69 | * Make white listed and authorized clients IP addresses clickable like SSIDs in PineAP 70 | * Write up some helpful information so people can just start using the module 71 | * Consolidate all portal info into a single portal_name.json file 72 | * Disable the button to move a portal while it is activated 73 | * Fixed client redirection after authorization 74 | 75 | ### Version 2.1 76 | * Removed un-needed verbosity 77 | * Made tab key indent in the editor instead of change elements 78 | * Added confirmation dialogue box when deleting a portal 79 | * Created auto-start feature 80 | * Various other quality of life updates 81 | 82 | ### Version 2.0 83 | * Captive Portal is now purely iptables (because F*** NoDogSplash) 84 | 85 | ### Version 1.0 86 | * Install/Remove NoDogSplash 87 | * Start/Stop NoDogSplash 88 | * Enable/Disable NoDogSplash 89 | * Create/Edit/Delete/Active Portals 90 | * Live Preview portals 91 | * All panels collapse for a better mobile experience 92 | -------------------------------------------------------------------------------- /EvilPortal/includes/api/Portal.php: -------------------------------------------------------------------------------- 1 | request = $request; 15 | } 16 | 17 | public function getResponse() 18 | { 19 | if (empty($this->error) && !empty($this->response)) { 20 | return $this->response; 21 | } elseif (empty($this->error) && empty($this->response)) { 22 | return array('error' => 'API returned empty response'); 23 | } else { 24 | return array('error' => $this->error); 25 | } 26 | } 27 | 28 | /** 29 | * Run a command in the background and don't wait for it to finish. 30 | * @param $command: The command to run 31 | */ 32 | protected final function execBackground($command) 33 | { 34 | exec("echo \"{$command}\" | at now"); 35 | } 36 | 37 | /** 38 | * Send notifications to the web UI. 39 | * @param $message: The notification message 40 | */ 41 | protected final function notify($message) 42 | { 43 | $this->execBackground("notify {$message}"); 44 | } 45 | 46 | /** 47 | * Write a log to the portals log file. 48 | * These logs can be retrieved from the web UI for .logs in the portals directory. 49 | * The log file is automatically appended to so there is no reason to add new line characters to your message. 50 | * @param $message: The message to write to the log file. 51 | */ 52 | protected final function writeLog($message) 53 | { 54 | try { 55 | $reflector = new \ReflectionClass(get_class($this)); 56 | $logPath = dirname($reflector->getFileName()); 57 | file_put_contents("{$logPath}/.logs", "{$message}\n", FILE_APPEND); 58 | } catch (\ReflectionException $e) { 59 | // do nothing. 60 | } 61 | } 62 | 63 | /** 64 | * Creates an iptables rule allowing the client to access the internet and writes them to the authorized clients. 65 | * Override this method to add other authorization steps validation. 66 | * @param $clientIP: The IP address of the client to authorize 67 | * @return bool: True if the client was successfully authorized otherwise false. 68 | */ 69 | protected function authorizeClient($clientIP) 70 | { 71 | if (!$this->isClientAuthorized($clientIP)) { 72 | exec("iptables -t nat -I PREROUTING -s {$clientIP} -j ACCEPT"); 73 | // exec("{$this->BASE_EP_COMMAND} add {$clientIP}"); 74 | file_put_contents($this->AUTHORIZED_CLIENTS_FILE, "{$clientIP}\n", FILE_APPEND); 75 | } 76 | return true; 77 | } 78 | 79 | /** 80 | * Handle client authorization here. 81 | * By default it just checks that the redirection target is in the request. 82 | * Override this to perform your own validation. 83 | */ 84 | protected function handleAuthorization() 85 | { 86 | if (isset($this->request->target)) { 87 | $this->authorizeClient($_SERVER['REMOTE_ADDR']); 88 | $this->onSuccess(); 89 | $this->redirect(); 90 | } elseif ($this->isClientAuthorized($_SERVER['REMOTE_ADDR'])) { 91 | $this->redirect(); 92 | } else { 93 | $this->showError(); 94 | } 95 | } 96 | 97 | /** 98 | * Where to redirect to on successful authorization. 99 | */ 100 | protected function redirect() 101 | { 102 | header("Location: {$this->request->target}", true, 302); 103 | } 104 | 105 | /** 106 | * Override this to do something when the client is successfully authorized. 107 | * By default it just notifies the Web UI. 108 | */ 109 | protected function onSuccess() 110 | { 111 | $this->notify("New client authorized through EvilPortal!"); 112 | } 113 | 114 | /** 115 | * If an error occurs then do something here. 116 | * Override to provide your own functionality. 117 | */ 118 | protected function showError() 119 | { 120 | echo "You have not been authorized."; 121 | } 122 | 123 | /** 124 | * Checks if the client has been authorized. 125 | * @param $clientIP: The IP of the client to check. 126 | * @return bool|int: True if the client is authorized else false. 127 | */ 128 | protected function isClientAuthorized($clientIP) 129 | { 130 | $authorizeClients = file_get_contents($this->AUTHORIZED_CLIENTS_FILE); 131 | return strpos($authorizeClients, $clientIP); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | -------------------------------------------------------------------------------- /EvilPortal/js/module.js: -------------------------------------------------------------------------------- 1 | registerController("EvilPortalController", ['$api', '$scope', function ($api, $scope) { 2 | 3 | // status information about the module 4 | $scope.evilPortal = { 5 | "throbber": false, 6 | "sdAvailable": false, 7 | "running": false, 8 | "startOnBoot": false, 9 | "library": true 10 | }; 11 | 12 | // controls that belong in the Controls pane 13 | $scope.controls = [ 14 | { "title": "Captive Portal", "visible": true, "throbber": false, "status": "Start"}, 15 | {"title": "Start On Boot", "visible": true, "throbber": false, "status": "Enable"} 16 | ]; 17 | 18 | // messages to be displayed in the Messages pane 19 | $scope.messages = []; 20 | 21 | $scope.whiteList = {"clients": "", "toManipulate": null}; 22 | 23 | $scope.accessList = {"clients": "", "toManipulate": null}; 24 | 25 | // all of the portals that could be found 26 | $scope.portals = []; 27 | 28 | // a model of a new portal to create 29 | $scope.newPortal = {"type": "basic", "name": ""}; 30 | 31 | // deleting portal stuff 32 | $scope.portalToDelete = null; 33 | $scope.portalDeleteValidation = null; 34 | 35 | // the portal workshop 36 | $scope.workshop = {"portal": {}, "dirContents": null, "inRoot": true, "rootDirectory": null, 37 | "editFile": {"path": null, "isNewFile": true}, "onEnable": null, "onDisable": null, 38 | "concreteTargetedRules": null, "workingTargetedRules": null, "deleteFile": null 39 | }; 40 | 41 | // active log file 42 | $scope.activeLog = {"title": null, "path": null, "contents": null}; 43 | 44 | /** 45 | * Reset the workshop object to a blank slate with initial values. 46 | */ 47 | $scope.resetWorkshop = function () { 48 | $scope.workshop = {"portal": {}, "dirContents": null, "inRoot": true, "rootDirectory": null, "editFile": {"path": null, "isNewFile": true}, 49 | "onEnable": null, "onDisable": null, "concreteTargetedRules": null, "workingTargetedRules": null, "deleteFile": null 50 | }; 51 | }; 52 | 53 | /** 54 | * Push a message to the Evil Portal Messages Pane 55 | * @param t: The Title of the message 56 | * @param m: The message body 57 | */ 58 | $scope.sendMessage = function (t, m) { 59 | // Add a new message to the top of the list 60 | $scope.messages.unshift({title: t, msg: m}); 61 | 62 | // if there are 4 items in the list remove the 4th item 63 | if ($scope.messages.length === 4) { 64 | $scope.dismissMessage(3); 65 | } 66 | }; 67 | 68 | /** 69 | * Remove a message from the Evil Portal Messages pane 70 | * @param $index: The index of the message in the list to remove 71 | */ 72 | $scope.dismissMessage = function ($index) { 73 | $scope.messages.splice($index, 1); 74 | }; 75 | 76 | /** 77 | * Preform an action for a given control 78 | * This can be starting the captive portal or toggle on boot. 79 | * @param control: The control to handle 80 | */ 81 | $scope.handleControl = function(control) { 82 | control.throbber = true; 83 | var actionToPreform = null; 84 | switch(control.title) { 85 | case "Captive Portal": 86 | actionToPreform = "toggleCaptivePortal"; 87 | break; 88 | 89 | case "Start On Boot": 90 | actionToPreform = "toggleOnBoot"; 91 | break; 92 | } 93 | 94 | if (actionToPreform !== null) { 95 | $api.request({ 96 | module: "EvilPortal", 97 | action: actionToPreform 98 | }, function(response) { 99 | if (!response.success) { 100 | $scope.sendMessage(control.title, response.message); 101 | } 102 | getStatus(); 103 | }); 104 | } 105 | }; 106 | 107 | /** 108 | * Validates the information in the newPortal model and then makes an API request to create a new portal. 109 | * @param storage: The storage medium to create the portal on (internal or sd) 110 | */ 111 | $scope.createNewPortal = function(storage) { 112 | $api.request({ 113 | module: "EvilPortal", 114 | action: "createNewPortal", 115 | name: $scope.newPortal.name, 116 | type: $scope.newPortal.type, 117 | storage: storage 118 | }, function(response) { 119 | if (!response.success) { 120 | $scope.sendMessage('Error Creating Portal', response.message); 121 | return; 122 | } 123 | $scope.newPortal = {"type": "basic", "name": ""}; 124 | getPortals(); 125 | }); 126 | }; 127 | 128 | /** 129 | * Move a given portal between storage mediums if an SD card is present. 130 | * @param portal: The portal to move 131 | */ 132 | $scope.movePortal = function(portal) { 133 | if (!$scope.evilPortal.sdAvailable) { 134 | $scope.sendMessage("No SD Card.", "An SD card must be present to preform this action."); 135 | return; 136 | } 137 | 138 | $api.request({ 139 | module: "EvilPortal", 140 | action: "movePortal", 141 | name: portal.title, 142 | storage: portal.storage 143 | }, function(response) { 144 | if (response.success) { 145 | getPortals(); 146 | $scope.sendMessage("Moved Portal", response.message); 147 | } else { 148 | $scope.sendMessage("Error Moving " + portal.title, response.message); 149 | } 150 | }); 151 | }; 152 | 153 | /** 154 | * Delete a portal from the wifi pineapple 155 | * @param verified: Has the delete request been verified? If so then make the API request otherwise setup 156 | * @param portal: The portal to delete 157 | */ 158 | $scope.deletePortal = function(verified, portal) { 159 | if (!verified) { // if the request has not been verified then setup the shits 160 | $scope.portalToDelete = portal; 161 | return; 162 | } 163 | 164 | if ($scope.portalToDelete === null || $scope.portalToDelete.fullPath === null) { 165 | $scope.sendMessage("Unable To Delete Portal", "No portal was set for deletion."); 166 | return; 167 | } 168 | deleteFileOrDirectory($scope.portalToDelete.fullPath, function (response) { 169 | if (!response.success) { 170 | $scope.sendMessage("Error Deleting Portal", response.message); // push an error if deletion failed 171 | } else { 172 | $scope.sendMessage("Deleted Portal", "Successfully deleted " + $scope.portalToDelete.title + "."); 173 | $scope.portalToDelete = null; 174 | $scope.portalDeleteValidation = null; 175 | getPortals(); // refresh the library 176 | } 177 | }); 178 | }; 179 | 180 | /** 181 | * Activate a portal 182 | * @param portal: The portal to activate 183 | */ 184 | $scope.activatePortal = function(portal) { 185 | $api.request({ 186 | module: "EvilPortal", 187 | action: "activatePortal", 188 | name: portal.title, 189 | storage: portal.storage 190 | }, function(response) { 191 | console.log(response); 192 | if (response.success) { 193 | getPortals(); 194 | $scope.sendMessage("Activated Portal", portal.title + " has been activated successfully."); 195 | } else { 196 | $scope.sendMessage("Error Activating " + portal.title, response.message); 197 | } 198 | }); 199 | }; 200 | 201 | /** 202 | * Deactivate a given portal if its active 203 | * @param portal: The portal to deactivate 204 | */ 205 | $scope.deactivatePortal = function(portal) { 206 | $api.request({ 207 | module: "EvilPortal", 208 | action: "deactivatePortal", 209 | name: portal.title, 210 | storage: portal.storage 211 | }, function(response) { 212 | console.log(response); 213 | if (response.success) { 214 | getPortals(); 215 | $scope.sendMessage("Deactivated Portal", portal.title + " has been deactivated successfully."); 216 | } else { 217 | $scope.sendMessage("Error Deactivating " + portal.title, response.message); 218 | } 219 | }); 220 | }; 221 | 222 | /** 223 | * Load portal contents and open it up in the work bench 224 | * @param portal: The portal to get the contents of 225 | */ 226 | $scope.loadPortal = function (portal) { 227 | getFileOrDirectoryContent(portal.fullPath, function(response) { 228 | if (!response.success) { 229 | $scope.sendMessage("Error Getting Contents", response.message); 230 | return; 231 | } 232 | $scope.workshop.inRoot = true; 233 | $scope.workshop.portal = portal; 234 | $scope.workshop.dirContents = response.content; 235 | $scope.workshop.rootDirectory = portal.fullPath; 236 | $scope.evilPortal.library = false; 237 | console.log(response.content); 238 | }); 239 | }; 240 | 241 | /** 242 | * Load toggle commands for the current portal in the work bench. 243 | * These are the commands that are executed when a portal is enabled/disabled. 244 | */ 245 | $scope.loadToggleCommands = function() { 246 | [".enable", ".disable"].forEach(getScript); 247 | function getScript(scriptName) { 248 | getFileOrDirectoryContent($scope.workshop.rootDirectory + "/" + scriptName, function (response) { 249 | if (!response.success) { 250 | $scope.sendMessage("Error Getting Contents", response.message); 251 | return; 252 | } 253 | if (scriptName === ".enable") 254 | $scope.workshop.onEnable = response.content.fileContent; 255 | else 256 | $scope.workshop.onDisable = response.content.fileContent; 257 | }); 258 | } 259 | }; 260 | 261 | /** 262 | * Save toggle commands. 263 | * @param cmdFile: The commands to save (enable, disable) 264 | */ 265 | $scope.saveToggleCommands = function(cmdFile) { 266 | function sendData(f, content) { 267 | writeToFile($scope.workshop.rootDirectory + "/" + f, content, false, function (response) { 268 | if (!response.success) 269 | $scope.sendMessage("Error write to file " + f, response.message); 270 | }); 271 | } 272 | 273 | switch(cmdFile) { 274 | case "disable": 275 | sendData(".disable", $scope.workshop.onDisable); 276 | break; 277 | 278 | case "enable": 279 | sendData(".enable", $scope.workshop.onEnable); 280 | break; 281 | 282 | default: 283 | sendData(".enable", $scope.workshop.onEnable); 284 | sendData(".disable", $scope.workshop.onDisable); 285 | break; 286 | } 287 | 288 | }; 289 | 290 | /** 291 | * Load the rules for targeted portals 292 | */ 293 | $scope.loadTargetedRules = function() { 294 | $api.request({ 295 | module: "EvilPortal", 296 | action: "getRules", 297 | name: $scope.workshop.portal.title, 298 | storage: $scope.workshop.portal.storage 299 | }, function(response) { 300 | if (response.success) { 301 | $scope.workshop.concreteTargetedRules = response.data; 302 | $scope.workshop.workingTargetedRules = {"rules": {}}; 303 | 304 | // welcome to the realm of loops. I will be your guide 305 | // We have to turn each rule into a keyed set of rules with a rule index represented by var index 306 | // this is because we need a constant key for each rule when editing on the web interface 307 | // the index must be removed later before saving the results to the routes.json file 308 | // if you have a better way to do this you are my hero. Email me n3rdcav3@gmail.com or fork the repo :) 309 | 310 | // This first loop loops over each rule categories such as "mac", "ssid" and so on 311 | for (var key in response.data['rules']) { 312 | 313 | // we then create the a object with that key name in our workingData object 314 | $scope.workshop.workingTargetedRules['rules'][key] = {}; 315 | 316 | // Now its time to loop over each category specifier such as "exact" and "regex" 317 | for (var specifier in response.data['rules'][key]) { 318 | var index = 0; 319 | 320 | // We then create that specifier in our workingData 321 | $scope.workshop.workingTargetedRules['rules'][key][specifier] = {}; 322 | 323 | // finally we loop over the specific rules defined in the specifier 324 | for (var r in response.data['rules'][key][specifier]) { 325 | var obj = {}; 326 | obj['key'] = r; 327 | obj['destination'] = response.data['rules'][key][specifier][r]; 328 | $scope.workshop.workingTargetedRules['rules'][key][specifier][index] = obj; 329 | } 330 | // increment index 331 | index++; 332 | } 333 | } 334 | } else { 335 | $scope.sendMessage("Error", "There was an issue getting the portal rules."); 336 | } 337 | }); 338 | }; 339 | 340 | /** 341 | * Remove a targeted rule 342 | * @param rule 343 | * @param specifier 344 | * @param index 345 | */ 346 | $scope.removeTargetedRule = function(rule, specifier, index) { 347 | delete $scope.workshop.workingTargetedRules['rules'][rule][specifier][index]; 348 | }; 349 | 350 | /** 351 | * Create a new targeted rule 352 | * @param rule 353 | * @param specifier 354 | */ 355 | $scope.newTargetedRule = function(rule, specifier) { 356 | // make sure the specifier is set 357 | if ($scope.workshop.workingTargetedRules['rules'][rule][specifier] == undefined) { 358 | $scope.workshop.workingTargetedRules['rules'][rule][specifier] = {}; 359 | } 360 | 361 | var highest = 0; 362 | 363 | // get the highest index 364 | for (var i in $scope.workshop.workingTargetedRules['rules'][rule][specifier]) { 365 | if (parseInt(i) >= highest) { 366 | highest = i + 1; 367 | } 368 | } 369 | 370 | $scope.workshop.workingTargetedRules['rules'][rule][specifier][highest] = {"": ""}; 371 | }; 372 | 373 | /** 374 | * Build the targeted rules and sned them to the API for saving 375 | */ 376 | $scope.saveTargetedRules = function() { 377 | // build the rules 378 | for (var key in $scope.workshop.concreteTargetedRules.rules) { 379 | for (var specifier in $scope.workshop.concreteTargetedRules.rules[key]) { 380 | var obj = {}; 381 | for (var i in $scope.workshop.workingTargetedRules['rules'][key][specifier]) { 382 | obj[$scope.workshop.workingTargetedRules['rules'][key][specifier][i]['key']] = $scope.workshop.workingTargetedRules['rules'][key][specifier][i]['destination']; 383 | } 384 | $scope.workshop.concreteTargetedRules['rules'][key][specifier] = obj; 385 | } 386 | } 387 | 388 | console.log(JSON.stringify($scope.workshop.concreteTargetedRules)); 389 | 390 | $api.request({ 391 | module: "EvilPortal", 392 | action: "saveRules", 393 | name: $scope.workshop.portal.title, 394 | storage: $scope.workshop.portal.storage, 395 | rules: JSON.stringify($scope.workshop.concreteTargetedRules) 396 | }, function(response) { 397 | if (!response.success) { 398 | $scope.sendMessage("Error", response.message); 399 | } 400 | }); 401 | }; 402 | 403 | /** 404 | * Load logs for a given portal 405 | * @param portal: The portal to load logs for 406 | */ 407 | $scope.loadPortalLog = function(portal) { 408 | var basePath = (portal.storage === "sd") ? "/sd/portals/" : "/root/portals/"; 409 | var logPath = basePath + portal.title + "/.logs"; 410 | $scope.loadLog(logPath); 411 | } 412 | 413 | /** 414 | * check if a given object is empty. 415 | * @param obj: The object to check 416 | * @returns {boolean}: true if empty false if not empty 417 | */ 418 | $scope.isObjectEmpty = function(obj) { 419 | return (Object.keys(obj).length === 0); 420 | }; 421 | 422 | /** 423 | * Loads a file from filePath and puts the data into the activeLog object. 424 | */ 425 | $scope.loadLog = function(filePath) { 426 | getFileOrDirectoryContent(filePath, function(response) { 427 | console.log(response); 428 | if (!response.success) { 429 | $scope.activeLog = { 430 | "title": "Unknown", 431 | "path": null, 432 | "contents": null, 433 | "size": "0 Bytes" 434 | }; 435 | return; 436 | } 437 | $scope.activeLog = { 438 | "title": response.content.name, 439 | "path": response.content.path, 440 | "contents": response.content.fileContent, 441 | "size": response.content.size 442 | }; 443 | }); 444 | }; 445 | 446 | /** 447 | * Writes an empty string to whatever file path is in $scope.activeLog.path 448 | * On successful write, $scope.activeLog.contents is set to null. 449 | * 450 | * If $scope.activeLog.path is null, the request if not made. 451 | * 452 | */ 453 | $scope.clearLog = function() { 454 | if ($scope.activeLog.path == null) 455 | return; 456 | 457 | writeToFile($scope.activeLog.path, '', false, function(response) { 458 | if (!response.success) { 459 | $scope.sendMessage("Error Clearing Log", response.message); 460 | return; 461 | } 462 | $scope.activeLog.contents = response.content; 463 | $scope.activeLog.size = "0 Bytes"; 464 | }); 465 | } 466 | 467 | /** 468 | * Load the contents of a given file. 469 | * @param filePath: The path to the file to load 470 | */ 471 | $scope.loadFileContent = function(filePath) { 472 | getFileOrDirectoryContent(filePath, function(response) { 473 | if (!response.success) { 474 | $scope.sendMessage("Error Getting Contents", response.message); 475 | return; 476 | } 477 | $scope.workshop.editFile = { 478 | "name": response.content.name, 479 | "path": response.content.path, 480 | "size": response.content.size, 481 | "content": response.content.fileContent 482 | }; 483 | }); 484 | }; 485 | 486 | /** 487 | * Setup the workshop to create a new empty file. 488 | */ 489 | $scope.setupNewFile = function() { 490 | var basePath = ($scope.workshop.portal.storage === "sd") ? "/sd/portals/" : "/root/portals/"; 491 | $scope.workshop.editFile.path = basePath + $scope.workshop.portal.title + "/"; 492 | $scope.workshop.editFile.isNewFile = true; 493 | $scope.workshop.editFile.size = "0 Bytes"; 494 | }; 495 | 496 | /** 497 | * Write file content to the file system. 498 | * @param editFile: A portal.editFile object 499 | */ 500 | $scope.saveFileContent = function(editFile) { 501 | // new files wont have the filename in the path so make sure to set it here if needed. 502 | if (!editFile.path.includes(editFile.name)) 503 | editFile.path = editFile.path + editFile.name; 504 | 505 | writeToFile(editFile.path, editFile.content, false, function(response) { 506 | if (!response.success) 507 | $scope.sendMessage("Error write to file " + editFile.name, response.message); 508 | 509 | $scope.loadPortal($scope.workshop.portal); // refresh the portal 510 | }); 511 | }; 512 | 513 | /** 514 | * Delete a requested file. 515 | */ 516 | $scope.deleteFile = function() { 517 | deleteFileOrDirectory($scope.workshop.deleteFile.path, function(response){ 518 | if (!response.success) 519 | $scope.sendMessage("Error deleting file " + $scope.workshop.deleteFile.name, response.message); 520 | 521 | $scope.loadPortal($scope.workshop.portal); // refresh the portal 522 | }); 523 | }; 524 | 525 | $scope.download = function(filePath) { 526 | $api.request({ 527 | module: "EvilPortal", 528 | action: "download", 529 | filePath: filePath 530 | }, function (response) { 531 | if (!response.success) { 532 | $scope.sendMessage("Error", response.message); 533 | return; 534 | } 535 | window.location = "/api/?download=" + response.download; 536 | }) 537 | }; 538 | 539 | /** 540 | * Load either the white list or the authorized clients (access) list 541 | * @param listName: The name of the list: whiteList or accessList (authorized clients) 542 | */ 543 | $scope.getList = function (listName) { 544 | var whiteList = '/pineapple/modules/EvilPortal/data/allowed.txt'; 545 | var authorized = '/tmp/EVILPORTAL_CLIENTS.txt'; 546 | 547 | getFileOrDirectoryContent((listName === "whiteList") ? whiteList : authorized, function (response) { 548 | switch (listName) { 549 | case 'whiteList': 550 | $scope.whiteList.clients = response.content.fileContent; 551 | break; 552 | case 'accessList': 553 | $scope.accessList.clients = response.content.fileContent; 554 | break; 555 | } 556 | }) 557 | }; 558 | 559 | /** 560 | * Remove a client from either the white list (whiteList) the authorized clients list (accessList) 561 | * @param listName: whiteList or accessList 562 | */ 563 | $scope.removeClientFromList = function(listName) { 564 | var clientToRemove = (listName === 'whiteList') ? $scope.whiteList.toManipulate : $scope.accessList.toManipulate; 565 | console.log(clientToRemove); 566 | $api.request({ 567 | module: "EvilPortal", 568 | action: "removeClientFromList", 569 | clientIP: clientToRemove, 570 | listName: listName 571 | }, function(response) { 572 | if (!response.success) { 573 | $scope.sendMessage("Error", response.message); 574 | return; 575 | } 576 | $scope.getList(listName); 577 | switch (listName) { 578 | case 'whiteList': 579 | $scope.whiteList.toManipulate = null; 580 | break; 581 | case 'accessList': 582 | $scope.accessList.toManipulate = null; 583 | break; 584 | } 585 | }); 586 | }; 587 | 588 | /** 589 | * Add a new client to the white list 590 | */ 591 | $scope.addWhiteListClient = function() { 592 | writeToFile('/pineapple/modules/EvilPortal/data/allowed.txt', $scope.whiteList.toManipulate + "\n", true, function(response) { 593 | $scope.getList('whiteList'); 594 | }); 595 | $scope.whiteList.toManipulate = null; 596 | }; 597 | 598 | /** 599 | * Authorize a new client 600 | */ 601 | $scope.authorizeClient = function () { 602 | $api.request({ 603 | module: "EvilPortal", 604 | action: "authorizeClient", 605 | clientIP: $scope.accessList.toManipulate 606 | }, function (response) { 607 | $scope.getList('accessList'); 608 | $scope.accessList.toManipulate = null; 609 | }); 610 | }; 611 | 612 | /** 613 | * Get a line clicked in a text area and set that line as the text for a text input 614 | * @param textareaId: The id of the text area to grab from 615 | * @param inputname: The name of the input field to write to 616 | */ 617 | $scope.getClickedClient = function(textareaId, inputname) { 618 | var textarea = $('#' + textareaId); 619 | var lineNumber = textarea.val().substr(0, textarea[0].selectionStart).split('\n').length; 620 | var ssid = textarea.val().split('\n')[lineNumber-1].trim(); 621 | $("input[name='" + inputname + "']").val(ssid).trigger('input'); 622 | }; 623 | 624 | /** 625 | * Write given content to a given file on the file system. 626 | * @param filePath: The path to the file to write content to 627 | * @param fileContent: The content to write to the file 628 | * @param appendFile: Should the content be append to the file (true) or overwrite the file (false) 629 | * @param callback: A callback function to handle the API response 630 | */ 631 | function writeToFile(filePath, fileContent, appendFile, callback) { 632 | $api.request({ 633 | module: "EvilPortal", 634 | action: "writeFileContent", 635 | filePath: filePath, 636 | content: fileContent, 637 | append: appendFile 638 | }, function(response) { 639 | callback(response); 640 | }); 641 | } 642 | 643 | /** 644 | * Get the contents of a directory 645 | * @param pathToObject: The full path to the file or directory to get the contents of 646 | * @param callback: A function that handles the response from the API. 647 | */ 648 | function getFileOrDirectoryContent(pathToObject, callback) { 649 | $api.request({ 650 | module: "EvilPortal", 651 | action: "getFileContent", 652 | filePath: pathToObject 653 | }, function(response) { 654 | callback(response); 655 | }); 656 | } 657 | 658 | /** 659 | * Delete a file or directory from the pineapples filesystem. 660 | * This is intended to be used for only deleting portals and portal related files but anything can be delete. 661 | * @param fileOrDirectory: The path to the file to delete 662 | * @param callback: The callback function to handle the API response 663 | */ 664 | function deleteFileOrDirectory(fileOrDirectory, callback) { 665 | $api.request({ 666 | module: "EvilPortal", 667 | action: "deleteFile", 668 | filePath: fileOrDirectory 669 | }, function(response) { 670 | callback(response); 671 | }); 672 | } 673 | 674 | /** 675 | * Update the control models so they reflect the proper information 676 | */ 677 | function updateControls() { 678 | $scope.controls = [ 679 | { 680 | "title": "Captive Portal", 681 | "status": ($scope.evilPortal.running) ? "Stop" : "Start", 682 | "visible": true, 683 | "throbber": false 684 | }, 685 | { 686 | "title": "Start On Boot", 687 | "status": ($scope.evilPortal.startOnBoot) ? "Disable": "Enable", 688 | "visible": true, 689 | "throbber": false 690 | } 691 | ]; 692 | } 693 | 694 | /** 695 | * Get the status's for the controls in the Controls pane and other various information 696 | */ 697 | function getStatus() { 698 | $scope.evilPortal.throbber = true; 699 | $api.request({ 700 | module: "EvilPortal", 701 | action: "status" 702 | }, function (response) { 703 | for (var key in response) { 704 | if (response.hasOwnProperty(key) && $scope.evilPortal.hasOwnProperty(key)) { 705 | $scope.evilPortal[key] = response[key]; 706 | } 707 | } 708 | $scope.evilPortal.throbber = false; 709 | updateControls(); 710 | }); 711 | } 712 | 713 | /** 714 | * Get all of the portals on the Pineapple 715 | */ 716 | function getPortals() { 717 | $scope.evilPortal.throbber = true; 718 | $api.request({ 719 | module: "EvilPortal", 720 | action: "listAvailablePortals" 721 | }, function(response) { 722 | if (!response.success) { 723 | $scope.sendMessage("Error Listing Portals", "An error occurred while trying to get list of portals."); 724 | return; 725 | } 726 | $scope.portals = []; 727 | response.portals.forEach(function(item, index) { 728 | $scope.portals.unshift({ 729 | title: item.title, 730 | size: item.size, 731 | storage: item.storage, 732 | active: item.active, 733 | type: item.portalType, 734 | fullPath: item.location 735 | }); 736 | }); 737 | }); 738 | } 739 | 740 | // The status for the Evil Portal module as well as current portals should be retrieved when the controller loads. 741 | getStatus(); 742 | getPortals(); 743 | 744 | 745 | }]); -------------------------------------------------------------------------------- /EvilPortal/api/module.php: -------------------------------------------------------------------------------- 1 | "/sd/portals/", "internal" => "/root/portals/"); 10 | private $BASE_EP_COMMAND = 'module EvilPortal'; 11 | // CONSTANTS 12 | 13 | /** 14 | * An implementation of the route method from Module. 15 | * This method routes the request to the method that handles it. 16 | */ 17 | public function route() 18 | { 19 | $this->createPortalFolders(); // if the portal folders (/root/portals | /sd/portals) do not exist create them. 20 | 21 | switch ($this->request->action) { 22 | 23 | case 'status': 24 | $this->response = array( 25 | "running" => $this->checkEvilPortalRunning(), 26 | "startOnBoot" => $this->checkAutoStart(), 27 | "sdAvailable" => $this->isSDAvailable() 28 | ); 29 | break; 30 | 31 | case 'writeFileContent': 32 | $this->response = $this->writeFileContents($this->request->filePath, $this->request->content, $this->request->append); 33 | break; 34 | 35 | case 'deletePortal': 36 | case 'deleteDirectory': 37 | case 'deleteFile': 38 | $this->response = $this->deleteFileOrDirectory($this->request->filePath); 39 | break; 40 | 41 | case 'getDirectoryContent': 42 | case 'getFileContent': 43 | $this->response = $this->getFileOrDirectoryContents($this->request->filePath); 44 | break; 45 | 46 | case 'download': 47 | $this->response = $this->download($this->request->filePath); 48 | break; 49 | 50 | case 'listAvailablePortals': 51 | $this->response = $this->getListOfPortals(); 52 | break; 53 | 54 | case 'createNewPortal': 55 | $this->response = $this->createNewPortal($this->request->name, $this->request->type, $this->request->storage); 56 | break; 57 | 58 | case 'movePortal': 59 | $this->response = $this->movePortal($this->request->name, $this->request->storage); 60 | break; 61 | 62 | case 'activatePortal': 63 | $this->response = $this->activatePortal($this->request->name, $this->request->storage); 64 | break; 65 | 66 | case 'deactivatePortal': 67 | $this->response = $this->deactivatePortal($this->request->name, $this->request->storage); 68 | break; 69 | 70 | case 'getRules': 71 | $this->response = $this->getPortalRules($this->request->name, $this->request->storage); 72 | break; 73 | 74 | case 'saveRules': 75 | $this->response = $this->savePortalRules($this->request->name, $this->request->storage, $this->request->rules); 76 | break; 77 | 78 | case 'toggleOnBoot': 79 | $this->response = $this->autoStartEvilPortal(); 80 | break; 81 | 82 | case 'toggleCaptivePortal': 83 | $this->response = $this->toggleCaptivePortal(); 84 | break; 85 | 86 | case 'removeClientFromList': 87 | $this->response = $this->removeFromList($this->request->clientIP, $this->request->listName); 88 | break; 89 | 90 | case 'authorizeClient': 91 | $this->authorizeClient($this->request->clientIP); 92 | $this->response = array("success" => true); 93 | break; 94 | } 95 | } 96 | 97 | /** 98 | * Create the folders that portals are stored in if they don't exist 99 | */ 100 | private function createPortalFolders() 101 | { 102 | if (!is_dir($this->STORAGE_LOCATIONS["internal"])) { 103 | mkdir($this->STORAGE_LOCATIONS["internal"]); 104 | } 105 | 106 | if (!is_dir($this->STORAGE_LOCATIONS["sd"]) and $this->isSDAvailable()) { 107 | mkdir($this->STORAGE_LOCATIONS["sd"]); 108 | } 109 | } 110 | 111 | /** 112 | * Decide if a file for a given portal is "deletable" or not. 113 | * If it is not then the UI should not display a delete option for the file. 114 | * @param $file: The name of the file 115 | * @return bool: Is the file deletable or not 116 | */ 117 | private function isFileDeletable($file) 118 | { 119 | if (substr($file, -strlen(".ep")) == ".ep") 120 | return false; 121 | return !in_array($file, array("MyPortal.php", "default.php", "helper.php", "index.php")); 122 | } 123 | 124 | /** 125 | * Get the contents of a specified file or directory 126 | * 127 | * If this method is being called as the result of an HTTP request, make sure that "file" is specified as a 128 | * parameter of the request and includes the full path to the file that should have its contents returned. 129 | * 130 | * @param $file : The file or directory to get contents of 131 | * @return array 132 | */ 133 | private function getFileOrDirectoryContents($file) 134 | { 135 | 136 | if (!file_exists($file)) { 137 | $message = "No such file or directory {$file}."; 138 | $contents = null; 139 | $success = false; 140 | } else if (is_file($file)) { 141 | $message = "Found file {$file} and retrieved contents"; 142 | $contents = array( 143 | "name" => basename($file), 144 | "path" => $file, 145 | "size" => $this->readableFileSize($file), 146 | "fileContent" => file_get_contents($file) 147 | ); 148 | $success = true; 149 | } else if (is_dir($file)) { 150 | $contents = array(); 151 | $message = "Returning directory contents for {$file}"; 152 | foreach (preg_grep('/^([^.])/', scandir($file)) as $object) { 153 | // skip .ep files because they shouldn't be edited directly. 154 | if (substr($object, -strlen(".ep")) == ".ep") 155 | continue; 156 | 157 | $obj = array("name" => $object, "directory" => is_dir("{$file}/{$object}"), 158 | "path" => realpath("{$file}/{$object}"), 159 | "permissions" => substr(sprintf('%o', fileperms("{$file}/{$object}")), -4), 160 | "size" => $this->readableFileSize("{$file}/{$object}"), 161 | "deletable" => $this->isFileDeletable($object)); 162 | array_push($contents, $obj); 163 | } 164 | $success = true; 165 | } else { 166 | $contents = null; 167 | $success = false; 168 | $message = "Unknown case. This should never happen."; 169 | } 170 | return array("success" => $success, "message" => $message, "content" => $contents); 171 | } 172 | 173 | /** 174 | * Write given content to a given file. 175 | * @param $file : The file to write content to 176 | * @param $content : The content to write to the file 177 | * @param $append : Should the data be appended to the end of the file (true) or over-write the file (false) 178 | * @return array 179 | */ 180 | private function writeFileContents($file, $content, $append) 181 | { 182 | if ($append) 183 | file_put_contents($file, $content, FILE_APPEND); 184 | else 185 | file_put_contents($file, $content); 186 | return array("success" => true, "message" => null); 187 | } 188 | 189 | /** 190 | * Delete a given file or directory and check if it has been deleted. 191 | * If the file was deleted the success will be true otherwise success is false 192 | * @param $filePath 193 | * @return array 194 | */ 195 | private function deleteFileOrDirectory($filePath) 196 | { 197 | if ($this->isFileDeletable(basename($filePath))) { 198 | exec(escapeshellcmd("rm -rf {$filePath}")); 199 | 200 | $success = (!file_exists($filePath)); 201 | $message = (file_exists($filePath)) ? "Error deleting file {$filePath}." : "{$filePath} has been deleted."; 202 | } else { 203 | $success = false; 204 | $message = "{$filePath} can not be deleted!"; 205 | } 206 | return array("success" => $success, "message" => $message); 207 | } 208 | 209 | /** 210 | * Download a file 211 | * @param: The path to the file to download 212 | * @return array : array 213 | */ 214 | private function download($filePath) 215 | { 216 | if (file_exists($filePath)) { 217 | return array("success" => true, "message" => null, "download" => $this->downloadFile($filePath)); 218 | } else { 219 | return array("success" => false, "message" => "File does not exist", "download" => null); 220 | } 221 | } 222 | 223 | /** 224 | * Get a list of portals found on internal and sd storage. 225 | */ 226 | private function getListOfPortals() 227 | { 228 | 229 | // an array of all of the portals found 230 | $portals = array(); 231 | $availableMediums = array("internal"); 232 | 233 | // if the sd card is available add it to the availableMediums 234 | if ($this->isSDAvailable()) { 235 | array_push($availableMediums, "sd"); 236 | } 237 | 238 | foreach($availableMediums as $medium) { 239 | $storageLocation = $this->STORAGE_LOCATIONS[$medium]; 240 | foreach (preg_grep('/^([^.])/', scandir($storageLocation)) as $object) { 241 | if (!is_dir($storageLocation . $object)) // skip the object if it is not a directory. 242 | continue; 243 | 244 | $portal = array( 245 | "title" => $object, 246 | "portalType" => $this->getValueFromJSONFile(array("type"), "{$storageLocation}{$object}/{$object}.ep")["type"], 247 | "size" => $this->readableFileSize("{$storageLocation}{$object}"), 248 | "location" => "{$storageLocation}{$object}", 249 | "storage" => $medium, 250 | "active" => (file_exists("/www/{$object}.ep")) 251 | ); 252 | // push the portal object to the array of portals found 253 | array_push($portals, $portal); 254 | } 255 | } 256 | 257 | return array("success" => true, "portals" => $portals); 258 | } 259 | 260 | /** 261 | * Create a new Portal with a given name of a given type on a given storage medium. 262 | * @param $name : The name of the new portal 263 | * @param $type : The type of portal to create (targeted or basic) 264 | * @param $storage : The storage medium to save the portal to (sd or internal) 265 | * @return array 266 | */ 267 | private function createNewPortal($name, $type, $storage) 268 | { 269 | // force the name of the portal to be lower cased and replace spaces with underscores 270 | $name = strtolower(str_replace(' ', '_', $name)); 271 | 272 | // $storage should be equal to "sd" or "internal". If its anything else just make it "internal" 273 | $storage = ($storage == "sd" or $storage == "internal") ? $storage : "internal"; 274 | 275 | // the path to store the portal 276 | $portalPath = $this->STORAGE_LOCATIONS[$storage]; 277 | 278 | // verify that no portal with the same name already exists 279 | if (file_exists("{$this->STORAGE_LOCATIONS["internal"]}{$name}") or 280 | (file_exists("{$this->STORAGE_LOCATIONS["sd"]}{$name}") and $this->isSDAvailable())) { 281 | return array("success" => false, "message" => "A portal named {$name} already exists!"); 282 | } 283 | 284 | // if the portal is supposed to be stored on the SD card, make sure that it is indeed available first. 285 | if ($storage == "sd" and !$this->isSDAvailable()) { 286 | return array("success" => false, "message" => "There is no SD card available!"); 287 | } 288 | 289 | // create the directory for the portal 290 | mkdir($portalPath . $name); 291 | 292 | // handle the portal types. If anything other than "targeted" is specified then it will create a basic portal 293 | switch ($type) { 294 | case 'targeted': 295 | exec("cp /pineapple/modules/EvilPortal/includes/targeted_skeleton/* {$portalPath}{$name}/"); 296 | exec("cp /pineapple/modules/EvilPortal/includes/targeted_skeleton/.* {$portalPath}{$name}/"); 297 | exec("mv {$portalPath}{$name}/portalinfo.json {$portalPath}{$name}/{$name}.ep"); 298 | $this->updateJSONFile(array("name" => $name, "type" => "targeted"), "{$portalPath}{$name}/{$name}.ep"); 299 | exec("sed -i 's/\"portal_name_here\"/\"{$name}\"/g' {$portalPath}{$name}/index.php"); 300 | break; 301 | 302 | default: 303 | exec("cp /pineapple/modules/EvilPortal/includes/skeleton/* {$portalPath}{$name}/"); 304 | exec("cp /pineapple/modules/EvilPortal/includes/skeleton/.* {$portalPath}{$name}/"); 305 | exec("mv {$portalPath}{$name}/portalinfo.json {$portalPath}{$name}/{$name}.ep"); 306 | $this->updateJSONFile(array("name" => $name, "type" => "basic"), "{$portalPath}{$name}/{$name}.ep"); 307 | break; 308 | } 309 | 310 | // make these scripts executable 311 | exec("chmod +x {$portalPath}{$name}/.enable"); 312 | exec("chmod +x {$portalPath}{$name}/.disable"); 313 | 314 | return array("success" => true, "message" => "Created {$type} portal {$name}!"); 315 | } 316 | 317 | /** 318 | * Move a portal between one storage medium to another. 319 | * 320 | * If the current medium is "internal" then the portal will be moved to "sd" and visa-versa 321 | * 322 | * @param $name : The name of the portal to move 323 | * @param $storage : The current storage medium 324 | * @return array 325 | */ 326 | private function movePortal($name, $storage) 327 | { 328 | $storage = ($storage == "internal" || $storage == "sd") ? $storage : "internal"; 329 | $newMedium = ($storage == "internal") ? "sd" : "internal"; 330 | $newStorage = $this->STORAGE_LOCATIONS[$newMedium]; 331 | 332 | // active portals should not be moved so check if the portal is currently active 333 | if (file_exists("/www/{$name}.ep")) { 334 | return array("success" => false, "message" => "You can not move an active portal!"); 335 | } else 336 | 337 | // make sure that an SD card is inserted if it is going to be needed 338 | if (($storage == "sd" || $newMedium == "sd") && !$this->isSDAvailable()) { 339 | return array("success" => false, "message" => "Please insert a SD card to preform this action."); 340 | } 341 | 342 | // if the portal doesn't exist then return an error 343 | if (!file_exists($this->STORAGE_LOCATIONS[$storage] . $name)) { 344 | return array("success" => false, "message" => "Could not find portal named {$name} on {$storage} storage"); 345 | } 346 | 347 | // verify that a portal with the same name doesn't already exist in the new location. 348 | if (file_exists($newStorage . $name)) { 349 | return array("success" => false, "message" => "A portal named {$name} already exists on {$newMedium} storage"); 350 | } 351 | 352 | // all of the above conditions should have passed so lets move the damn portal. 353 | exec(escapeshellcmd("mv {$this->STORAGE_LOCATIONS[$storage]}{$name} {$newStorage}{$name}")); 354 | 355 | // verify that the directory was moved 356 | if (file_exists($newStorage . $name)) { 357 | return array("success" => true, "message" => "{$name} was moved to {$newMedium} storage!"); 358 | } else { 359 | return array("success" => false, "message" => "An error occurred moving {$name} to {$newMedium} storage"); 360 | } 361 | 362 | } 363 | 364 | /** 365 | * Set a given portal to "active". 366 | * This means to move the portals contents to /www so it can be access via HTTP port 80. 367 | * 368 | * If any file with the same name as one of the files being copied to /www already exists in /www 369 | * then that file will be renamed to {file_name}.ep_backup and restored when the portal is deactivated. 370 | * 371 | * If any portals are currently active when this method is called they will be deactivated. 372 | * 373 | * @param $name: The name of the portal to activate 374 | * @param $storage: The storage medium the portal is on 375 | * @return array 376 | */ 377 | private function activatePortal($name, $storage) 378 | { 379 | $dir = $this->STORAGE_LOCATIONS[$storage]; 380 | 381 | // check if there is a currently activate portal and deactivate it. 382 | foreach(scandir("/www") as $file) { 383 | if (substr($file, strlen($file) - strlen(".ep")) === ".ep") { // deactivate a portal if needed 384 | $portalName = rtrim($file, ".ep"); 385 | $realPath = realpath("/www/{$file}"); 386 | $storage = ($realPath == "{$this->STORAGE_LOCATIONS['internal']}{$portalName}/{$portalName}.ep") ? "internal": "sd"; 387 | $this->deactivatePortal($portalName, $storage); 388 | break; 389 | } 390 | } 391 | 392 | $success = false; 393 | $portalPath = escapeshellarg($dir . $name); 394 | if (file_exists($dir . $name)) { 395 | exec("ln -s /pineapple/modules/EvilPortal/includes/api /www/captiveportal"); 396 | $portal_files = scandir($dir . $name); 397 | foreach ($portal_files as $file) { 398 | if (file_exists("/www/{$file}")) { 399 | rename("/www/{$file}", "/www/{$file}.ep_backup"); 400 | } 401 | exec("ln -s {$portalPath}/{$file} /www/{$file}"); 402 | $success = true; 403 | } 404 | // holding off on toggle commands until a future release. 405 | // exec("echo {$portalPath}/.enable | at now"); 406 | $message = "{$name} is now active."; 407 | } else { 408 | $message = "Couldn't find {$portalPath}."; 409 | } 410 | 411 | return array("message" => $message, "success" => $success); 412 | } 413 | 414 | /** 415 | * Deactivate a given portal. 416 | * 417 | * To do this we remove all files associated with the given portal from /www 418 | * This method also renames any files with the extension ".ep_backup" to their original name 419 | * 420 | * @param $name: The name of the portal to deactivate 421 | * @param $storage: The storage medium the portal is on 422 | * @return array 423 | */ 424 | private function deactivatePortal($name, $storage) 425 | { 426 | $storage = ($storage == "internal" || $storage == "sd") ? $storage : "internal"; 427 | $dir = $this->STORAGE_LOCATIONS[$storage]; 428 | 429 | // if the portal is not active then return an error 430 | if (!(file_exists("/www/{$name}.ep"))) { 431 | return array("success" => false, "message" => "{$name} is not currently active."); 432 | } 433 | 434 | // if the portal does not exist then return an error 435 | if (!file_exists($dir . $name)) { 436 | return array("success" => false, "message" => "Unable to find the portal {$name}."); 437 | } 438 | 439 | // remove portal files from /www 440 | foreach(scandir($dir. $name) as $file) { 441 | unlink("/www/{$file}"); 442 | } 443 | 444 | // rename any files that may have been renamed back to their original name 445 | foreach(scandir("/www/") as $file) { 446 | if (substr($file, strlen($file) - strlen(".ep_backup")) === ".ep_backup") { 447 | $oldName = str_replace(".ep_backup", "", $file); 448 | rename("/www/{$file}", "www/{$oldName}"); 449 | } 450 | } 451 | 452 | // holding off on toggle commands until a future release. 453 | // exec("echo {$dir}{$name}/.disable | at now"); 454 | 455 | return array("success" => true, "message" => "Deactivated {$name}."); 456 | } 457 | 458 | /** 459 | * Attempt to get rules for a targeted portal 460 | * @param $name: The name of the portal 461 | * @param $storage: The storage medium of the portal 462 | * @return array 463 | */ 464 | private function getPortalRules($name, $storage) 465 | { 466 | $storage = ($storage == "internal" || $storage == "sd") ? $storage : "internal"; 467 | $path = $this->STORAGE_LOCATIONS[$storage]; 468 | 469 | 470 | if (is_file("{$path}{$name}/{$name}.ep")) { 471 | $rules = $this->getValueFromJSONFile(array("targeted_rules"), "{$path}{$name}/{$name}.ep")["targeted_rules"]; 472 | return array( 473 | "message" => "Found portal rules", 474 | "data" => $rules, 475 | "success" => true 476 | ); 477 | } else { 478 | return array("message" => "Unable to find portal.", "success" => false); 479 | } 480 | 481 | } 482 | 483 | /** 484 | * Save rules to a targeted portal 485 | * @param $name : The name of the portal 486 | * @param $storage : The storage medium of the portal 487 | * @param $rules : The rules to save 488 | * @return array 489 | */ 490 | private function savePortalRules($name, $storage, $rules) 491 | { 492 | $storage = ($storage == "internal" || $storage == "sd") ? $storage : "internal"; 493 | $path = $this->STORAGE_LOCATIONS[$storage]; 494 | 495 | if (is_file("{$path}{$name}/{$name}.ep")) { 496 | $this->updateJSONFile(array("targeted_rules" => json_decode($rules)), "{$path}{$name}/{$name}.ep")["targeted_rules"]; 497 | return array( 498 | "message" => "Saved portal rules", 499 | "success" => true 500 | ); 501 | } else { 502 | return array("message" => "Unable to find portal {$name}.", "success" => false); 503 | } 504 | 505 | } 506 | 507 | /** 508 | * Check if Evil Portal is currently running or not be checking iptables. 509 | * @return bool 510 | */ 511 | private function checkEvilPortalRunning() 512 | { 513 | return exec("iptables -t nat -L PREROUTING | grep 172.16.42.1") != ''; 514 | } 515 | 516 | /** 517 | * Check if EvilPortal is running when the Pineapple starts or not 518 | * @return bool 519 | */ 520 | public function checkAutoStart() 521 | { 522 | return !(exec("ls /etc/rc.d/ | grep evilportal") == ''); 523 | } 524 | 525 | /** 526 | * Grant a client access to the internet and stop blocking them with the captive portal 527 | * @param $client: The IP address of the client to authorize 528 | */ 529 | private function authorizeClient($client) 530 | { 531 | exec("iptables -t nat -I PREROUTING -s {$client} -j ACCEPT"); 532 | // exec("{$this->BASE_EP_COMMAND} add {$client}"); 533 | $this->writeFileContents($this->CLIENTS_FILE, "{$client}", true); 534 | } 535 | 536 | /** 537 | * Revoke a clients access to the internet and start blocking them with the captive portal 538 | * @param $client: The IP address of the client to revoke 539 | */ 540 | private function revokeClient($client) 541 | { 542 | // exec("{$this->BASE_EP_COMMAND} remove {$client}"); 543 | exec("iptables -t nat -D PREROUTING -s {$client}"); 544 | exec("iptables -t nat -D PREROUTING -s {$client} -j ACCEPT"); 545 | } 546 | 547 | /** 548 | * Start the captive portal portion of Evil Portal 549 | * 550 | * All clients in the White List should be automatically authorized when Evil Portal starts. 551 | * 552 | * @return array 553 | */ 554 | private function startEvilPortal() 555 | { 556 | // Delete client tracking file if it exists 557 | if (file_exists($this->CLIENTS_FILE)) { 558 | unlink($this->CLIENTS_FILE); 559 | } 560 | 561 | // Enable forwarding. It should already be enabled on the pineapple but do it anyways just to be safe 562 | exec("echo 1 > /proc/sys/net/ipv4/ip_forward"); 563 | exec("ln -s /pineapple/modules/EvilPortal/includes/api /www/captiveportal"); 564 | 565 | // Insert allowed clients into tracking file 566 | $allowedClients = file_get_contents($this->ALLOWED_FILE); 567 | file_put_contents($this->CLIENTS_FILE, $allowedClients); 568 | 569 | // Configure other rules 570 | exec("iptables -A INPUT -s 172.16.42.0/24 -j DROP"); 571 | exec("iptables -A OUTPUT -s 172.16.42.0/24 -j DROP"); 572 | exec("iptables -A INPUT -s 172.16.42.0/24 -p udp --dport 53 -j ACCEPT"); 573 | 574 | // Allow the pineapple 575 | exec("iptables -A INPUT -s 172.16.42.1 -j ACCEPT"); 576 | exec("iptables -A OUTPUT -s 172.16.42.1 -j ACCEPT"); 577 | 578 | //Block https till login 579 | exec("iptables -t nat -A PREROUTING -i br-lan -p tcp --dport 443 -j DNAT --to-destination 172.16.42.1:80"); 580 | //exec("iptables -A INPUT -i br-lan -p tcp --dport 443 -j DROP"); 581 | //exec("iptables -t nat -A PREROUTING -i br-lan -j DROP"); 582 | 583 | exec("iptables -t nat -A PREROUTING -i br-lan -p tcp --dport 80 -j DNAT --to-destination 172.16.42.1:80"); 584 | exec("iptables -t nat -A POSTROUTING -j MASQUERADE"); 585 | 586 | // exec("{$this->BASE_EP_COMMAND} init"); 587 | 588 | // Add rule for each allowed client 589 | $lines = file($this->CLIENTS_FILE); 590 | foreach ($lines as $client) { 591 | $this->authorizeClient($client); 592 | } 593 | 594 | $success = $this->checkEvilPortalRunning(); 595 | $message = ($success) ? "EvilPortal is now up and running!" : "EvilPortal failed to start."; 596 | 597 | return array("success" => $success, "message" => $message); 598 | } 599 | 600 | /** 601 | * Stop the captive portal portion of Evil Portal from running 602 | * @return mixed 603 | */ 604 | private function stopEvilPortal() 605 | { 606 | if (file_exists($this->CLIENTS_FILE)) { 607 | $lines = file($this->CLIENTS_FILE); 608 | foreach ($lines as $client) { 609 | $this->revokeClient($client); 610 | } 611 | unlink($this->CLIENTS_FILE); 612 | } 613 | 614 | exec("iptables -t nat -D PREROUTING -i br-lan -p tcp --dport 80 -j DNAT --to-destination 172.16.42.1:80"); 615 | exec("iptables -t nat -D PREROUTING -i br-lan -p tcp --dport 443 -j DNAT --to-destination 172.16.42.1:80"); //enable https again 616 | exec("iptables -D INPUT -p tcp --dport 53 -j ACCEPT"); 617 | exec("iptables -D INPUT -j DROP"); 618 | 619 | // exec("{$this->BASE_EP_COMMAND} purge"); 620 | 621 | $success = !$this->checkEvilPortalRunning(); 622 | $message = ($success) ? "EvilPortal has stopped running" : "There was an issue stopping EvilPortal"; 623 | 624 | return array("success" => $success, "messsage" => $message); 625 | } 626 | 627 | /** 628 | * If Evil Portal is running then stop it, otherwise start it. 629 | */ 630 | private function toggleCaptivePortal() { 631 | // Make the file executable. In the future the `module` command should do this for us. 632 | chmod("/pineapple/modules/EvilPortal/executable/executable", 0755); 633 | 634 | return $this->checkEvilPortalRunning() ? $this->stopEvilPortal() : $this->startEvilPortal(); 635 | } 636 | 637 | /** 638 | * Enable or Disable Evil Portal to run when the pineapple boots. 639 | * If Evil Portal is supposed to run when this method is called then it will be disabled 640 | * If Evil Portal is not supposed to run when this method is called then it will be enabled on boot. 641 | * 642 | * This method does not start nor stop current running instances of Evil Portal. 643 | * 644 | * @return array 645 | */ 646 | private function autoStartEvilPortal() 647 | { 648 | // if EvilPortal is not set to start on startup then set that shit 649 | if (!$this->checkAutoStart()) { 650 | copy("/pineapple/modules/EvilPortal/includes/evilportal.sh", "/etc/init.d/evilportal"); 651 | chmod("/etc/init.d/evilportal", 0755); 652 | exec("/etc/init.d/evilportal enable"); 653 | $enabled = $this->checkAutoStart(); 654 | $message = ($enabled) ? "EvilPortal is now enabled on start up" : "Error enabling EvilPotal on startup."; 655 | 656 | return array( 657 | "success" => $enabled, 658 | "message" => $message 659 | ); 660 | } else { // if evil portal is set to run on startup then disable that shit. 661 | exec("/etc/init.d/evilportal disable"); 662 | $enabled = !$this->checkAutoStart(); 663 | $message = ($enabled) ? "EvilPortal is now disabled on startup." : "Error disabling EvilPortal on startup."; 664 | 665 | return array( 666 | "success" => $enabled, 667 | "message" => $message 668 | ); 669 | } 670 | } 671 | 672 | /** 673 | * Removes a client from either the whiteList or authorizedList 674 | * @param $clientIP: The IP address of the client to be removed 675 | * @param $listName: The name of the list to remove the client from 676 | * @return array 677 | */ 678 | private function removeFromList($clientIP, $listName) 679 | { 680 | $valid = preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/', $clientIP); 681 | 682 | // if the IP address is invalid then return an error message 683 | if (!$valid) { 684 | return array("success" => false, "message" => "Invalid IP Address."); 685 | } 686 | 687 | $success = true; 688 | switch ($listName) { 689 | case "whiteList": 690 | $data = file_get_contents($this->ALLOWED_FILE); 691 | $data = str_replace("{$clientIP}\n", '', $data); 692 | file_put_contents("/root/removeFromList", $data); 693 | file_put_contents($this->ALLOWED_FILE, $data); 694 | break; 695 | 696 | case "accessList": 697 | $data = file_get_contents($this->CLIENTS_FILE); 698 | $data = str_replace("{$clientIP}\n", '', $data); 699 | file_put_contents($this->CLIENTS_FILE, $data); 700 | $this->revokeClient($clientIP); 701 | break; 702 | 703 | default: 704 | $success = false; 705 | break; 706 | 707 | } 708 | $message = ($success) ? "Successfully removed {$clientIP} from {$listName}" : "Error removing {$clientIP} from {$listName}"; 709 | return array("success" => $success, "message" => $message); 710 | } 711 | 712 | /** 713 | * Add a value to a json file 714 | * @param $keyValueArray: The data to add to the file 715 | * @param $file: The file to write the content to. 716 | */ 717 | private function updateJSONFile($keyValueArray, $file) { 718 | $data = json_decode(file_get_contents($file), true); 719 | foreach ($data as $key => $value) { 720 | if (isset($keyValueArray[$key])) { 721 | $data[$key] = $keyValueArray[$key]; 722 | } 723 | } 724 | file_put_contents($file, json_encode($data)); 725 | } 726 | 727 | /** 728 | * Get values from a JSON file 729 | * @param $keys: The key or keys you wish to get the value from 730 | * @param $file: The file to that contains the JSON data. 731 | * @return array 732 | */ 733 | private function getValueFromJSONFile($keys, $file) { 734 | $data = json_decode(file_get_contents($file), true); 735 | $values = array(); 736 | foreach ($data as $key => $value) { 737 | if (in_array($key, $keys)) { 738 | $values[$key] = $value; 739 | } 740 | } 741 | return $values; 742 | } 743 | 744 | /** 745 | * Get the size of a file and add a unit to the end of it. 746 | * @param $file: The file to get size of 747 | * @return string: File size plus unit. Exp: 3.14M 748 | */ 749 | private function readableFileSize($file) { 750 | $size = filesize($file); 751 | 752 | if ($size == null) 753 | return "0 Bytes"; 754 | 755 | if ($size < 1024) { 756 | return "{$size} Bytes"; 757 | } else if ($size >= 1024 && $size < 1024*1024) { 758 | return round($size / 1024, 2) . "K"; 759 | } else if ($size >= 1024*1024) { 760 | return round($size / (1024*1024), 2) . "M"; 761 | } 762 | return "{$size} Bytes"; 763 | } 764 | 765 | } 766 | -------------------------------------------------------------------------------- /EvilPortal/module.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 |
7 |

Controls

9 |
10 |
11 |
12 | 13 | 14 | 15 | 16 | 23 | 24 |
{{ control.title }} 17 | 21 | 22 |
25 |
26 |
27 |
28 | 29 | 30 |
31 | 35 |
36 |
37 |

No Messages.

38 | Clear 39 | All 40 | 41 | 42 | 49 | 50 |
43 |
44 |
{{ message.title }} Dismiss
46 |

{{ message.msg }}

47 |
48 |
51 |
52 |
53 |
54 |
55 | 56 |
57 |
58 | 59 |
60 | 64 |
65 |
66 |
67 | 68 | 69 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 |
82 |
83 |
84 |
85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 108 | 111 | 112 | 116 | 117 | 120 | 121 | 122 |
Portal NamePortal TypeLocationMove ToLogsActivateDelete
{{ portal.title }}{{ portal.type }}{{ portal.storage }} 105 | SD 106 | Internal 107 | 109 | View 110 | 113 | Activate 114 | Deactivate 115 | 118 | Delete 119 |
123 |
124 |
125 | 126 |
127 |

No Portals in Library to Display.

128 |
129 | 130 | 131 |
132 | 133 | 134 |
135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 |
147 | 148 |
149 |
150 | 151 | 152 |
153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 |
File NameSizeEditDelete
{{ file.name }}{{ file.size }}EditDelete
169 |
170 | 171 |
172 |
173 |
174 |
175 | 176 | 177 |
178 |
179 |
White List
182 |
183 |
184 |
185 |

186 | This is a list of clients who are allowed to connect to the internet without ever viewing the captive portal. 187 |

188 |

189 | 190 |

191 |
192 | 194 | 195 | 196 | 197 | 198 |
199 |
200 | 201 |
202 |
203 |
204 | 205 | 206 |
207 |
208 |
Authorized Clients
211 |
212 |
213 |
214 |
215 |

216 | This is a list of clients who have been authorized through the captive portal. 217 |

218 |

219 | 220 |

221 |
222 | 223 | 224 | 225 | 226 | 227 |
228 |
229 | 230 |
231 |
232 |

Evil Portal must be started first.

233 |
234 |
235 |
236 |
237 | 238 | 239 |
240 |
241 |
Live Preview
244 |
245 |
246 |
247 |
248 | 250 | 251 |
252 |
253 |

Evil Portal must be started first.

254 |
255 |
256 |
257 |
258 | 259 | 260 |
261 |
262 |
Evil Portal Info
264 |
265 |
266 |
267 |
268 | Disclaimer 269 |

Evil Portal and all of its components are intended for professional use only. No one associated with this project is an anyway liable for your actions.

270 |
271 |
272 | Help 273 |
Summary
274 |

Evil Portal is a captive portal program that enables you to easily create a captive portal for whatever your needs are. There are two kinds of portals Basic and Targeted. 275 | Basic portals are just a simple page that gets served to all clients. Targeted portals allow you to serve a unique page to different clients based on a given rule.

276 |
Basic Portals
277 |

A Basic Portal is a one-size serves all kind of portal. These portals are designed to be a single page that all clients will land on. This is the traditional way 278 | that captive portals work and the way Evil Portal as been doing things from the beginning.

279 |
Targeted Portals
280 |

A Targeted Portal allows you to serve a different page on a per-client basis based on a condition. This can be something like their mac address, user-agent, etc... 281 | Targeted Portals give you a whole new dynamic to what Evil Portal can do, if you want clients who are connected to the SSID "Coffee Shop" to see a "Coffee Shop" branded portal 282 | while at the same time clients with Android phones seeing an Android branded portal then this is what you want.

283 |
Useful Links
284 | Hak5 Forum Thread 285 |
286 | GitHub Project 287 |
288 |
289 | Change Log 290 |
    291 |
  • 3.1
  • 292 |
      293 |
    • Added ability to write and view logs on a per-portal basis
    • 294 |
    • Created method writeLog($message) that writes to the portal log file
    • 295 |
    • Created method notify($message) that sends a notification to the web ui
    • 296 |
    • Added ability to download files
    • 297 |
    • Tab button in file editor will now insert four spaces
    • 298 |
    • Revamped the file editor modal
    • 299 |
    • Showing file sizes in the portal work bench
    • 300 |
    • Various quality of life improvements
    • 301 |
    302 |
  • 3.0
  • 303 |
      304 |
    • Added ability to route clients to different portals based upon some identifier [ssid, mac vendor, ip, etc...]
    • 305 |
    • Updated the work bench so users can choose between targeted and non-targeted portals
    • 306 |
    • Created easy-to-use interface for creating targeting rules
    • 307 |
    • Created some consistency throughout the UI
    • 308 |
    • Added ability to create portals on an SD card and move between SD and Internal storage easily
    • 309 |
    • Made white listed and authorized clients IP addresses clickable like SSIDs in PineAP
    • 310 |
    • Created helper functions getClientMac, getClientSSID, getClientHostName
    • 311 |
    • Sending notification when a client goes through a portal.
    • 312 |
    • Various quality of life improvements
    • 313 |
    314 |
315 |
    316 |
  • 2.1
  • 317 |
      318 |
    • Removed un-needed verbosity
    • 319 |
    • Made tab key indent in the editor instead of change elements
    • 320 |
    • Added confirmation dialogue box when deleting a portal
    • 321 |
    • Created auto-start feature
    • 322 |
    • Various other quality of life updates
    • 323 |
    324 |
325 |
    326 |
  • 2.0
  • 327 |
      328 |
    • Captive Portal is now purely iptables (because F*** 329 | NoDogSplash) 330 |
    • 331 |
    332 |
333 |
    334 |
  • 1.0
  • 335 |
      336 |
    • Initial Pineapple Nano Release
    • 337 |
    338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 | 346 | 347 | 348 | 567 | 568 |
--------------------------------------------------------------------------------