The SSID you are connected to is =getClientSSID($_SERVER['REMOTE_ADDR']);?>
20 |
Your host name is =getClientHostName($_SERVER['REMOTE_ADDR']);?>
21 |
Your MAC Address is =getClientMac($_SERVER['REMOTE_ADDR']);?>
22 |
Your internal IP address is =$_SERVER['REMOTE_ADDR'];?>
23 |
24 |
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 |