├── .gitignore ├── LICENSE ├── README.md ├── bin └── setup ├── iptables-webui ├── iptables ├── enabled.json └── rules.json ├── lib └── iprules.js ├── package.json └── public ├── css ├── bootstrap.min.css ├── custom.css ├── port_forwarding.css ├── rules.css └── status_page.css ├── fonts ├── glyphicons-halflings-regular.eot ├── glyphicons-halflings-regular.svg ├── glyphicons-halflings-regular.ttf ├── glyphicons-halflings-regular.woff └── glyphicons-halflings-regular.woff2 ├── index.html ├── js ├── ajax.js ├── bootstrap.min.js ├── forwarding_status.js ├── iptables_list.js ├── jquery-1.10.0.min.js ├── jsx_transformer-0.12.2.js ├── page_control.js ├── react-0.12.2-min.js ├── react-0.12.2.js └── source_rules.js └── jsx ├── button.jsx ├── forwarding_status.jsx ├── iptables_list.jsx ├── port_forward_page.jsx ├── port_forward_rule.jsx ├── rule.jsx ├── rule_state_buttons.jsx ├── rules_list.jsx ├── rules_page.jsx ├── source_rules.jsx └── status_page.jsx /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | public/js/.module-cache 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Robert McLeod 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IP Tables WebUI 2 | 3 | A nice webui for the `iptables` command, written in NodeJS. Currently a work in progress. 4 | 5 | ## Security 6 | 7 | This WebUI is not meant to be used as a general access long running web server. Instead the following flow is assumed: 8 | 9 | 1. The User SSH's into a remote server with a port forward e.g: `ssh myserver.com -L 8099:localhost:8099` 10 | 1. The user starts the iptables web interface `iptables-webui start` 11 | 1. The user navigates to the address in their browser e.g: `http://localhost:8099` 12 | 13 | Pro-tip: Store the server/forwarding details in `~/.ssh/config`: 14 | 15 | ``` 16 | Host myserver 17 | Hostname myserver.com 18 | User me 19 | LocalForward 8099:localhost:8099 20 | ``` 21 | 22 | It would be possible to run it as a typical web server process, but it would not be recommended as good security practice to leave IPTables so wide open. Even if there was authentication and authorization built 23 | into this app. 24 | 25 | ## Planned Features 26 | 27 | * Modular handling of rules (enable and disable named groups of rules) 28 | * Raw rule editing 29 | * Flexible Port Forwarding table 30 | * Simple Pre-built rules (like enable/disable SSH or HTTP in/out) 31 | * Advanced Pre-built rules (internet connection sharing, load balancing) 32 | * Current Status of IP Tables 33 | * View Compiled rules vs Source rules 34 | * Help with enabling/disabling kernel network features (forwarding, masquerading) 35 | 36 | ## Rule Spec 37 | 38 | This is an example of what a rule looks like in JSON: 39 | 40 | * **name**: the name (user set) 41 | * **enabled**: whether the rule is enabled (user set) 42 | * **lines**: the lines that make up the rule, can be either (user set): 43 | * an object that can be parsed by the RuleParser 44 | * or a string of iptables arguments 45 | * **valid**: determined by the result of the last test 46 | * **test_lines**: the lines that made up the file used for the last test 47 | * **error**: the error that occured on the last test 48 | 49 | ```js 50 | { 51 | name: 'my_dumb-rule', 52 | enabled: false, 53 | lines: [ 54 | '# lines in the rule can be JSON or string', 55 | '-A INPUT --dport 22 -j ACCEPT', 56 | {chain: 'input', dport: 80, target: 'accept'} 57 | ] 58 | valid: true, 59 | test_lines: [ 60 | '# iptables-restore test file generated by iprules 2015-01-06 00:00', 61 | '*filter' 62 | '# my_dumb-rule' 63 | '# lines in the rule can be JSON or string', 64 | '-A INPUT --dport 22 -j ACCEPT', 65 | '-A INPUT --dport 80 -j ACCEPT', 66 | '#end' 67 | ], 68 | error: '' 69 | } 70 | ``` 71 | 72 | ### RuleParser spec 73 | 74 | The RuleParser can handle the following arguments (examples shown): 75 | 76 | ```js 77 | { 78 | chain: 'INPUT', 79 | protocol: 'tcp', // default is TCP 80 | sport: false, // this will be ignored during compile time 81 | dport: 8822, 82 | target: 'ACCEPT', 83 | src: '192.168.3.0/24', 84 | dst: '172.16.0.233', 85 | in: 'eth0', 86 | out: 'eth1', 87 | table: 'nat', // default is filter 88 | states: ['new'], 89 | to_dst: '172.16.0.233:22' 90 | } 91 | ``` 92 | 93 | ## API Spec 94 | 95 | This is the API so far, not everything is working: 96 | 97 | ```sh 98 | GET /rules # gets all the rules 99 | POST /rules # creates a rule 100 | GET /rules/:pattern # gets a rule by name or glob pattern 101 | PUT /rules/:name # updates the named rule 102 | DELETE /rules/:name # deletes the named rule 103 | GET /rules/:name/test # tests the named rule 104 | GET /iptables/list # gives iptables -L output 105 | GET /status # various statuses 106 | ``` 107 | 108 | ## You damn kids are just jamming javascript in everywhere! 109 | 110 | I did it in NodeJS and ReactJS because: 111 | 112 | * I don't want to learn a real language like C++ 113 | * I want to learn more about NodeJS an ReactJS 114 | * I want to use it on an ARM and Node is faster than ruby (dammit!) 115 | * I am a sadomasochist -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | home="$1"; 4 | 5 | # Precons 6 | [[ -z "$home" ]] && echo "Must specify setup folder" && exit 1; 7 | [[ -d "$home" ]] && [[ "$2" != "-f" ]] && exit 0; 8 | 9 | # Setup folders 10 | mkdir "$home"; 11 | mkdir "$home/tmp"; 12 | mkdir "$home/rules"; 13 | 14 | # Files 15 | cat "\[\]" > "$home/enabled.json"; 16 | cat "\[\]" > "$home/rules.json"; 17 | 18 | exit 0; -------------------------------------------------------------------------------- /iptables-webui: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nodejs 2 | 3 | /***************************************************\ 4 | * 5 | * IP Tables Web UI 6 | * 7 | * (c) 2015 Robert McLeod released under MIT Licence 8 | * 9 | * You may be thinking the start of this script is 10 | * a bit ugly, ignoring typical convention. This 11 | * is only to make sure thingss start as fast as 12 | * possible. No point waiting for everything to 13 | * load before finding that the port is in use. 14 | * 15 | \***************************************************/ 16 | 17 | // TODO: get command line options working 18 | var PORT = 8099, 19 | HOME = "./iptables", 20 | LOCK = "./iptables-webui.pid"; 21 | // LOCK = "/var/run/iptables-webui.pid"; 22 | 23 | var fs = require("fs"), 24 | exec = require("child_process").exec; 25 | 26 | // trap SIGINT to delete lockfile and exit 27 | process.on("exit", function(code) { 28 | if ( fs.existsSync(LOCK) ) fs.unlinkSync(LOCK); 29 | }); 30 | 31 | // check for root 32 | // if ( process.getuid() != 0 ) { 33 | // console.error("ERROR: Must run as root"); 34 | // process.exit(1); 35 | // } 36 | 37 | // TODO: check if the port is available 38 | 39 | // function for checking/creating log file 40 | var createLockFile = function(lockfile, callback) { 41 | fs.exists(lockfile, function(exists) { 42 | if (exists) { 43 | var pid = fs.readFileSync(lockfile); 44 | exec("ps -p "+pid, function(err, stdout) { // check if it's running 45 | if (err.code == 1) { // the process doesn't exist 46 | fs.writeFile(lockfile, process.pid, function(err){ 47 | callback(err, process.pid); 48 | }); 49 | } else { // the process exists 50 | var err = new Error("IP Tables WebUI is already running on PID "+process.pid); 51 | callback(err, process.pid); 52 | } 53 | }); 54 | } else { 55 | fs.writeFile(lockfile, process.pid, function(err){ 56 | callback(err, process.pid); 57 | }); 58 | } 59 | }); 60 | }; 61 | 62 | // TODO: MAKE THIS SYNCHRONOUS 63 | // check/set the lockfile 64 | createLockFile(LOCK, function(err, pid) { 65 | if (err) { 66 | console.error(err.message); 67 | process.exit(); 68 | } 69 | }); 70 | 71 | 72 | // TODO: run setup here to 73 | 74 | 75 | var http = require("http"), 76 | iprules = require("./lib/iprules"), 77 | express = require("express"); 78 | 79 | var app = express(), 80 | server = http.createServer(app); 81 | 82 | // require faye and attach it to the server 83 | // var _faye = require("faye"), 84 | // bayeux = new _faye.NodeAdapter({mount: '/faye', timeout: 45}), 85 | // faye = _faye.Client("http://localhost:"+PORT+"/faye"); 86 | 87 | // bayeux.attach(server); 88 | 89 | iprules.init(HOME); 90 | 91 | // setup static files 92 | app.use(express.static(__dirname + '/public')); 93 | 94 | app.get("/import", function(req, res) { 95 | if ( !req.params.path ) res.status(400).end(); 96 | 97 | iprules.importRules(req.params.path, function(results) { 98 | res.status(200).json(results).end(); 99 | }); 100 | }); 101 | 102 | app.get("/rules", function(req, res) { 103 | iprules.all(function(err, rules) { 104 | res.json(rules); 105 | }); 106 | }); 107 | 108 | app.get('/rules/:pattern', function(req, res) { 109 | iprules.find(req.params.pattern, function(rules) { 110 | var code = (rules.length > 0 ) ? 200 : 404; 111 | res.json(rules).status(code).end(); 112 | }); 113 | }); 114 | 115 | // create rule 116 | app.post("/rules/:name", function(req, res) { 117 | iprules.create(req.body, function(err, rules) { 118 | if ( !err ) { 119 | res.json({rules: rules}).status(201).end(); 120 | } else if ( err ) { 121 | res.json({error: err.message, rules: rules}).status(500).end(); 122 | } 123 | }); 124 | }); 125 | 126 | // update rule 127 | app.put("/rules/:name", function(req, res) { 128 | 129 | }); 130 | 131 | app.delete('/rules/:name', function(req, res) { 132 | 133 | }); 134 | 135 | app.get('/status', function(req, res) { 136 | var forwarding = {}; 137 | 138 | fs.readdirSync("/proc/sys/net/ipv4/conf").forEach(function(device) { 139 | switch(device) { 140 | case "default": 141 | case "all": 142 | return true; 143 | break; 144 | default: 145 | var state = parseInt(fs.readFileSync("/proc/sys/net/ipv4/conf/"+device+"/forwarding", 'utf-8').trim()); 146 | forwarding[device] = state ? true : false; 147 | return true; 148 | break; 149 | } 150 | }); 151 | 152 | res.json({forwarding: forwarding}).end();; 153 | }); 154 | 155 | app.get('/iptables/list', function(req, res) { 156 | exec('/sbin/iptables -L', function(err, stdout, stderr) { 157 | res.json({list: stdout, error: err && err.message, stderr: stderr}).end(); 158 | }); 159 | }); 160 | 161 | app.put("/rules/:name/enable", function(req, res) { 162 | iprules.enable(req.params.name, function(err) { 163 | res.status(err ? 500 : 200).end(); 164 | }); 165 | }); 166 | 167 | app.put("/rules/:name/disable", function(req, res) { 168 | iprules.disable(req.params.name, function(err) { 169 | res.status(err ? err.status || 500 : 200).end(); 170 | }); 171 | }); 172 | 173 | app.get("/reload", function(req, res) { 174 | iprules.reload(function(err, result) { 175 | if ( err ) { 176 | res.json(err).status(500).end(); 177 | } else { 178 | res.json(result).end(); 179 | } 180 | }); 181 | }); 182 | 183 | app.get("/rules/:name/test", function(req, res) { 184 | iprules.test(req.params.name, function(err) { 185 | if ( err ) { res.status(400).json({message: err.message}).end(); } 186 | res.status(200).end(); 187 | }); 188 | }); 189 | 190 | 191 | // Run setup first 192 | exec("bin/setup "+HOME, function(err, stdout) { 193 | if ( err ) { 194 | console.error(err.message); 195 | process.exit(1); 196 | } 197 | 198 | console.log(stdout); 199 | 200 | // Start the WebUI 201 | app.listen(PORT, function() { 202 | console.log("Started IPTables WebUI on port "+PORT); 203 | }); 204 | }); -------------------------------------------------------------------------------- /iptables/enabled.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /iptables/rules.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /lib/iprules.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | var fs = require("fs"), 3 | crypto = require("crypto"), 4 | JsonDB = require("node-json-db"), 5 | exec = require("child_process").exec; 6 | 7 | db = new JsonDB("rules", true, false); 8 | 9 | /** 10 | * Basic Interface to the iptables-restore command 11 | */ 12 | var IptablesRestore = { 13 | /** 14 | * Loads the argument text into iptables using 15 | * the iptables-restore command. Must be 16 | * syntactically correct 17 | * 18 | * @param text {string} text formatted into an iptables-restore file 19 | * @callback err 20 | */ 21 | load: function(text, callback) { 22 | // test the data to make sure it's good 23 | this.test(this.build(text), function(err) { 24 | if ( err ) callback(err); 25 | 26 | // test passed so write the file to /etc 27 | // TODO: save mode of 660 28 | var fn = "/etc/iptables.rules"; 29 | fs.writeFile(fn, text, function(err) { 30 | if ( err ) callback(err); 31 | 32 | // the file was written, so load it in for realz 33 | exec("/sbin/iptables-restore < "+fn, function(err, stdout, stderr) { 34 | if ( err ) callback(err); 35 | callback(); 36 | }); 37 | }); 38 | }); 39 | }, 40 | 41 | /** 42 | * Does a test run using the iptables-restore command 43 | * to test the given text 44 | * 45 | * @param text {string} text formatted into an iptables-restore file 46 | * @callback err 47 | * @callback text {string} the text that was used to test with 48 | */ 49 | test: function(text, callback) { 50 | // TODO: save mode of 660 51 | var fn = "/tmp/iptables.rules"; 52 | fs.writeFile(fn, text = this.build(text), function(err) { 53 | if ( err ) callback(err); 54 | 55 | // the file was written so test it 56 | exec("/sbin/iptables-restore --test < "+fn, function(err, stdout, stderr) { 57 | fs.unlink(fn); 58 | if ( err ) callback(err); 59 | callback(null, text); 60 | }); 61 | }); 62 | }, 63 | 64 | build: function(text) { 65 | var lines = [ 66 | "# Generated by ipgroups "+(new Date().toLocaleString()), 67 | "*filter" 68 | ]; 69 | lines.push(text); 70 | lines.push("#end"); 71 | return lines.join("\n"); 72 | } 73 | }; 74 | 75 | /** 76 | * A parser that can translate iptables rule arguments 77 | * into javascript objects and vice-versa. 78 | */ 79 | var RuleParser = { 80 | 81 | /** 82 | * Parses the given iptables rule arguments into 83 | * a JS object 84 | * 85 | * @param rule {string} the iptables rule args 86 | * @return {object} the args split into keys of a JS hash 87 | */ 88 | parse: function(rule) { 89 | var fm = this.firstMatch; 90 | return { 91 | chain: fm(/-A ([A-Z]+)/ , line), 92 | protocol: fm(/-p (\w+)/ , line), 93 | dport: fm(/--dport (\d+)/ , line), 94 | sport: fm(/--sport (\d+)/ , line), 95 | target: fm(/-j (\w+)/ , line), 96 | src: fm(/--src ([\.0-9]+)/ , line), 97 | dst: fm(/--dst ([\.0-9]+)/ , line), 98 | in: fm(/-o ([a-z0-9]+)/ , line), 99 | out: fm(/-i ([a-z0-9]+)/ , line), 100 | table: fm(/-t (\w+)/ , line), 101 | states: fm(/--state ([A-Z,]+)/ , line).split(','), 102 | to_dst: fm(/--to-destination (\S+)/, line) 103 | }; 104 | }, 105 | 106 | /** 107 | * Simply returns the first regexp match in a string 108 | * 109 | * @param regexp {regexp} the regexp to scan for 110 | * @param string {string} the string to scan 111 | * @return {string} the first match 112 | */ 113 | firstMatch: function(regexp, string) { 114 | return regexp.exec(string)[1]; 115 | }, 116 | 117 | /** 118 | * Renders a JS rule into the iptables rule arguments 119 | * 120 | * @param rule {object} the hash representing the rule args 121 | * @return {string} the string of iptables rule args 122 | */ 123 | render: function(rule) { 124 | var args = []; 125 | 126 | // set some defaults 127 | if (!rule.chain) rule.chain = 'INPUT'; 128 | if (!rule.protocol) rule.protocol = 'tcp'; 129 | 130 | // build the args up 131 | if (rule.table) args = args.contact(['-t', , rule.table]); 132 | if (rule.chain) args = args.concat([rule.action , rule.chain]); 133 | if (rule.protocol) args = args.concat(["-p" , rule.protocol]); 134 | if (rule.src) args = args.concat(["--src" , rule.src]); 135 | if (rule.dst) args = args.concat(["--dst" , rule.dst]); 136 | if (rule.sport) args = args.concat(["--sport" , rule.sport]); 137 | if (rule.dport) args = args.concat(["--dport" , rule.dport]); 138 | if (rule.in) args = args.concat(["-i" , rule.in]); 139 | if (rule.out) args = args.concat(["-o" , rule.out]); 140 | if (rule.target) args = args.concat(["-j" , rule.target]); 141 | if (rule.to_dst) args = args.concat(["--to-destination", rule.to_dst]); 142 | if (rule.states) args = args.concat(["-m state --state", rule.states.map(function(s) { return s.toUpperCase(); }).join(",")]) 143 | 144 | return args.join(" "); 145 | } 146 | }; 147 | 148 | module.exports = (function() { 149 | var home = null, 150 | parser = RuleParser, 151 | db = db; 152 | 153 | var init = function(_home) { 154 | home = _home; 155 | 156 | var buildIndex = function() { 157 | db.rules.forEach(function(rule, index) { 158 | db.index.by_name[rule.name] = index; 159 | }); 160 | }; 161 | 162 | /** 163 | * Returns the group by the given name 164 | * 165 | * @param name {string} name of the group 166 | * @return {object} 167 | */ 168 | var get = function(id) { 169 | return db.getData('/groups/'+id); 170 | }; 171 | 172 | /** 173 | * Returns an array of all groups from the db 174 | * 175 | * @return {array} 176 | */ 177 | var all = function() { 178 | return db.getData('/groups'); 179 | }; 180 | 181 | /** 182 | * Find rules by name or glob pattern 183 | * 184 | * @param pattern - the name or pattern to find 185 | * @callback rules - array of rules 186 | */ 187 | var find = function(pattern, callback) { 188 | var rule; 189 | var wildcard = pattern.indexOf("*"); 190 | 191 | if ( wildcard == -1 ) { 192 | if ( (rule = get(pattern)) != null) { 193 | callback(rule); 194 | } else { 195 | callback([]); 196 | } 197 | 198 | return; 199 | } 200 | 201 | var start = (wildcard == 0) ? true : false, 202 | end = (wildcard == pattern.length) ? true : false, 203 | pattern = pattern.replace("*", ''), 204 | rules = []; 205 | 206 | console.log("iprules FIND: "+pattern+" : "+wildcard+" : "+start+" : "+end); 207 | 208 | // check if there was a wildcard specified 209 | if ( wildcard == -1 ) { 210 | if ( db.rules[name] !== undefined ) { 211 | callback([db.rules[name]]); // simply search for the name 212 | } else { 213 | callback([]); // nothing found 214 | } 215 | } else { // there was a wildcard 216 | Object.keys(db.rules).forEach(function(name) { 217 | 218 | if ( start ) { 219 | if (name.indexOf(pattern) == 0) rules.push(db.rules[name]); 220 | } else if ( end ) { 221 | var pos = name.length - pattern.length; // determine the start of the pattern 222 | if (name.indexOf(pattern) == pos ) rules.push(db.rules[name]); 223 | } else { 224 | // TODO: add pattern in the middle support 225 | } 226 | 227 | }); 228 | } 229 | 230 | console.log("iprules FOUND: "+rules.length); 231 | console.log(rules); 232 | 233 | callback(rules); 234 | }; 235 | 236 | /** 237 | * Imports rules from a file or directory and creates groups 238 | * 239 | * @param path {string} 240 | * @callback err 241 | * @callback groups {array} of successfully imported groups 242 | */ 243 | var import = function(path, callback) { 244 | fs.exists(path, function(exists) { 245 | if ( !exists ) { 246 | callback(new Error("Path not found: "+path)); 247 | return false; 248 | } 249 | 250 | // determine if the path given is a directory or file 251 | fs.stat(path, function(err, stats) { 252 | if ( stats.isFile() ) { 253 | importFile(path, function(err, group) { 254 | callback(err, [group]); 255 | }); 256 | } else { 257 | importDirectory(path, callback); 258 | } 259 | }); 260 | 261 | }); 262 | }; 263 | 264 | /** 265 | * Imports rules from a file and creates a group 266 | * 267 | * @param path {string} the path to the file 268 | * @callback err 269 | * @callback group {object} the imported group 270 | */ 271 | var importFile = function(path, callback) { 272 | fs.readFile(path, function(err, text) { 273 | if (err) callback(err); 274 | text = text.replace(/^iptables /g, ''); 275 | 276 | var group = { 277 | name: path, 278 | enabled: false, 279 | lines: text.split("\n"); 280 | } 281 | 282 | create(group, callback); 283 | }); 284 | }; 285 | 286 | /** 287 | * Imports files from a directory 288 | * 289 | * @param path {string} the path name to import from 290 | * @callback err 291 | * @callback groups {array} of successfully imported groups 292 | */ 293 | 294 | var importDirectory = function(path, callback) { 295 | fs.readdir(path, function(err, files) { 296 | if (err) callback && callback(err); 297 | var groups = []; 298 | 299 | files.forEach(function(file) { 300 | importFile(file, function(err, group) { 301 | if (!err) groups.push(group); 302 | }); 303 | }); 304 | 305 | if ( groups.length == 0 ) err = new Error("No rules imported from "+path); 306 | 307 | callback && callback(err, groups); 308 | }); 309 | }; 310 | 311 | /** 312 | * Saves the given group in the DB 313 | * 314 | * @param group {object} 315 | * @callback err 316 | * @callback group {object} the saved group with test data added 317 | */ 318 | var create = function(group, callback) { 319 | if ( typeof group.enabled == undefined ) group.enabled = false; 320 | 321 | test([group], function(err, groups, failed) { // run tests and save results in the groups 322 | // create the group regardless of the test result 323 | var group = groups[0] 324 | 325 | if ( group.id == undefined ) { 326 | group.id = db.getData('/groups').length; 327 | } 328 | 329 | db.push('/groups/'+group.id, group); 330 | 331 | callback(err, group); 332 | }); 333 | }; 334 | 335 | /** 336 | * Updates the given group in the database 337 | * 338 | * @param group {object} the group to update 339 | * @callback err 340 | * @callback group {object} the group after being tested and saved 341 | */ 342 | var update = function(group, callback) { 343 | test([group], function(err, groups, failed) { 344 | if ( err ) callback && callback(err); 345 | db.push('/groups/'+groups[0].id, groups[0]); 346 | callback && callback(err, group[0]); 347 | }); 348 | }; 349 | 350 | /** 351 | * Destroys the rule with the given ID 352 | * 353 | * @param id {integer} 354 | */ 355 | var destroy = function(id) { 356 | db.delete('/groups/'+id); 357 | }; 358 | 359 | /** 360 | * Tests the given groups by using the iptables-restore command 361 | * 362 | * @param groups {array} the groups of rule groups to test 363 | * @callback err 364 | * @callback groups {array} the groups with test data added 365 | * @callback failed {integer} the total number of test failures 366 | */ 367 | var test = function(groups, callback) { 368 | var failed = 0; 369 | 370 | // run the test against each group and save it's results to 371 | groups = groups.map(function(group) { 372 | testGroup(group, function(err, test_lines) { 373 | group.valid = err == null; 374 | group.test_lines = test_lines; 375 | if ( err ) { 376 | failed++; 377 | group.error = err.message; 378 | if ( group.enabled ) group.enabled = false; // disable the group so it doesn't run 379 | } else { 380 | group.error = false; 381 | } 382 | }); 383 | 384 | return group; 385 | }); 386 | 387 | callback(null, groups, failed); 388 | }; 389 | 390 | /** 391 | * Tests a group of rules 392 | * @param group {object} 393 | * @callback err 394 | * @callback lines {array} lines of the file that was tested 395 | */ 396 | var testGroup = function(group, callback) { 397 | IptablesRestore.test(compile([group]), function(err, text) { 398 | callback(err, text.split("\n")); 399 | }); 400 | }; 401 | 402 | var enable = function(name) { 403 | var group = db.getData(name); 404 | group.enabled = true; 405 | db.push('/groups/'+name, group); 406 | }; 407 | 408 | var disable = function(name, callback) { 409 | var group = db.getData(name); 410 | group.enabled = false; 411 | db.push('/groups/'+name, group); 412 | }; 413 | 414 | /* 415 | * Compiles given groups into an iptables-rules file 416 | * represented as an array of lines 417 | * 418 | * @param groups 419 | * @return lines {array} 420 | */ 421 | var compile = function(groups, join) { 422 | var lines = []; 423 | 424 | groups.forEach(function(group) { 425 | lines.push("# "+group.name); 426 | 427 | group.lines.forEach(function(line) { 428 | if ( typeof(line) == "string" ) { 429 | lines.push(line); 430 | } else if ( typeof(line) == "object" ) { 431 | line = RuleParser.render(line); 432 | lines.push(line); 433 | } 434 | }); 435 | 436 | lines.push(""); 437 | }); 438 | 439 | return (join === false) lines : lines.join("\n"); // join by default 440 | }; 441 | 442 | /** 443 | * Reload enabled groups into IP Tables 444 | * 445 | * @callback err 446 | */ 447 | var reload = function(callback) { 448 | var groups = []; 449 | 450 | db.getData("/groups").forEach(function(group) { 451 | if ( group.enabled && group.valid ) groups.push(group); 452 | }); 453 | 454 | 455 | IptablesRestore.load(compile(groups), function(err) { 456 | callback && callback(err, lines); 457 | }); 458 | }; 459 | 460 | return { 461 | init: init, 462 | parser: parser, 463 | all: all, 464 | importRules: importRules, 465 | enable: enable, 466 | disable: disable, 467 | create: create, 468 | update: update, 469 | test: test, 470 | reload: reload, 471 | find: find, 472 | destroy: destroy 473 | } 474 | }()); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "iptables-webui", 3 | "version" : "0.0.1", 4 | "author" : "Robert McLeod", 5 | "licence" : "MIT", 6 | "dependencies": { 7 | "express": "~4.11.2" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /public/css/custom.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | } 4 | 5 | #container { 6 | padding: 0 100px 0 100px; 7 | } 8 | 9 | .page { 10 | padding-top: 20px; 11 | } 12 | 13 | -------------------------------------------------------------------------------- /public/css/port_forwarding.css: -------------------------------------------------------------------------------- 1 | /*.port_forward { 2 | width: 100%; 3 | background: lightblue; 4 | overflow: hidden; 5 | padding: 10px; 6 | border-radius: 10px; 7 | border: 2px solid black; 8 | } 9 | 10 | .port_forward .in, 11 | .port_forward .dport, 12 | .port_forward .dst, 13 | .port_forward .fport { 14 | float: left; 15 | margin-right: 10px; 16 | } 17 | 18 | .port_forward .controls { 19 | float:right; 20 | } 21 | 22 | .port_forward .dport input, 23 | .port_forward .fport input, 24 | .port_forward .in input { 25 | width: 100px; 26 | } 27 | 28 | 29 | .port_forward p { 30 | font-size: small; 31 | } 32 | 33 | */ -------------------------------------------------------------------------------- /public/css/rules.css: -------------------------------------------------------------------------------- 1 | .rule { 2 | width: 100%; 3 | padding: 5px; 4 | overflow: hidden; 5 | margin-bottom: 10px; 6 | } 7 | 8 | .rule.enabled { 9 | border: 2px solid green; 10 | border-radius: 5px; 11 | background: #D6FFBF; 12 | } 13 | 14 | .rule input.name { 15 | margin: 0px; 16 | width: 100%; 17 | padding: 0px 10px; 18 | background: transparent; 19 | border: 1px solid transparent; 20 | border-radius: 5px; 21 | font-size: 30px; 22 | } 23 | 24 | .rule textarea:hover, 25 | .rule textarea:focus, 26 | .rule input.name:focus, 27 | .rule input.name:hover { 28 | border: 1px solid #c0c0c0; 29 | background: white; 30 | } 31 | 32 | .rule textarea { 33 | width: 100%; 34 | font-size: 16px; 35 | border-radius: 5px; 36 | font-family: monospace; 37 | height: 100px; 38 | background: transparent; 39 | border: 1px solid transparent; 40 | } 41 | 42 | .rule div.btn-group { 43 | /*display: inline-block;*/ 44 | float: right; 45 | } 46 | -------------------------------------------------------------------------------- /public/css/status_page.css: -------------------------------------------------------------------------------- 1 | #device_forwarding { 2 | width: 300px; 3 | overflow: hidden; 4 | } 5 | 6 | #device_forwarding .device { 7 | overflow: hidden; 8 | } 9 | 10 | #device_forwarding .device p { 11 | float: left; 12 | font-weight: bold; 13 | } 14 | 15 | #device_forwarding .device p.state { 16 | float: right; 17 | } 18 | 19 | #device_forwarding .device.on p.state { 20 | color: green; 21 | } 22 | 23 | #device_forwarding .device.off p.state { 24 | color: red; 25 | } 26 | -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penguinpowernz/iptables-webui/f94182e3f2e22143cbabcab1ca4437dc8c3b189e/public/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penguinpowernz/iptables-webui/f94182e3f2e22143cbabcab1ca4437dc8c3b189e/public/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penguinpowernz/iptables-webui/f94182e3f2e22143cbabcab1ca4437dc8c3b189e/public/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penguinpowernz/iptables-webui/f94182e3f2e22143cbabcab1ca4437dc8c3b189e/public/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | IP Tables 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 49 | 50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 79 | 80 | 81 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /public/js/ajax.js: -------------------------------------------------------------------------------- 1 | var API = "http://localhost:8099"; 2 | 3 | var Ajax = (function() { 4 | var get = function(uri) { 5 | return $.getJSON(API+uri); 6 | }; 7 | 8 | var post = function(uri, data) { 9 | return $.ajax({url: API+uri, data: data, method: 'POST', dataType: 'json'}); 10 | }; 11 | 12 | var put = function(uri, data) { 13 | return $.ajax({url: API+uri, data: data, method: 'PUT', dataTye: 'json'}); 14 | }; 15 | 16 | var del = function(uri, data) { 17 | return $.ajax({url: API+uri, data: data, method: 'DELETE', dataTye: 'json'}); 18 | }; 19 | 20 | return { 21 | get: get, 22 | post: post, 23 | put: put, 24 | delete: del 25 | } 26 | }()); -------------------------------------------------------------------------------- /public/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.2 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.2",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a(f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.2",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active"));a&&this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),c.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.2",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));return a>this.$items.length-1||0>a?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&"show"==b&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a(this.options.trigger).filter('[href="#'+b.id+'"], [data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.2",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0,trigger:'[data-toggle="collapse"]'},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":a.extend({},e.data(),{trigger:this});c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){b&&3===b.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=c(d),f={relatedTarget:this};e.hasClass("open")&&(e.trigger(b=a.Event("hide.bs.dropdown",f)),b.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger("hidden.bs.dropdown",f)))}))}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.2",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('