├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── create_user.py ├── pfweb ├── __init__.py ├── app.py ├── config.py ├── constants.py ├── static │ ├── css │ │ ├── bootstrap.min.css │ │ ├── datatables.min.css │ │ ├── jquery-ui.min.css │ │ └── main.css │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ └── js │ │ ├── bootstrap.min.js │ │ ├── datatables.min.js │ │ ├── jquery-3.1.1.min.js │ │ ├── jquery-ui.min.js │ │ └── main.js └── templates │ ├── base.html │ ├── dash.html │ ├── edit.html │ ├── edit_rule.html │ ├── edit_table.html │ ├── error.html │ ├── login.html │ ├── pfinfo.html │ ├── rules.html │ ├── states.html │ └── tables.html └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.1.0dev4] - 2016-12-10 4 | [Full Changelog](https://github.com/nahun/pfweb/compare/v0.1.0dev3...v0.1.0dev4) 5 | 6 | ### Added 7 | - Session lifetime 8 | - Direction icons in rules list 9 | - Table name validation 10 | - States details popover for each rule 11 | - Table details popover in rules list 12 | - Allow IPv6 NAT and RDR rules 13 | - Interface selection for source and destination 14 | 15 | ### Fixed 16 | - Fix default values in form fields 17 | - Error 400 page receives username 18 | - Fix reading interface media on dashboard 19 | - Fix default answer in create_user script 20 | - Fix IPv6 prefix length of 32 not showing 21 | 22 | ### Changed 23 | - Formatting on dashboard for numbers 24 | - Rename pfweb.py to app.py 25 | - Show deprecated IPv6 addresses in dashboard 26 | - Combine source and destination fields in states 27 | - Only allow NAT when direction is set to out 28 | 29 | ## [0.1.0dev3] - 2016-11-29 30 | [Full Changelog](https://github.com/nahun/pfweb/compare/0.1.0.dev2...v0.1.0dev3) 31 | 32 | ### Added 33 | - Support for rdr-to and nat-to rules 34 | - Initial support for global PF options in config file. State-policy is first. 35 | 36 | ### Fixed 37 | - Fix save order button being enabled even when no row was selected 38 | - Fix glyphicon font files not being installed with setuptools 39 | - Some formatting and sorting when listing states 40 | - Fix a previous fix of bootstrap css as now CSS and JS use their own directories 41 | 42 | ### Changed 43 | - Disable and enable the port fields. Easier to remember to change the port type. 44 | 45 | ## [0.1.0dev2] - 2016-11-25 46 | [Full Changelog](https://github.com/nahun/pfweb/compare/b6f7396...0.1.0.dev2) 47 | 48 | ### Added 49 | - Ability to change the order of rules 50 | - PF information on dashboard 51 | - pf.conf is now saved after each change making them persistent 52 | - Status menu items. Lists out entire PF info and the PF state table. 53 | 54 | ### Fixed 55 | - Fix setting ICMP type to ANY 56 | 57 | ### Changed 58 | - Autofocus on username field 59 | - Move to all local JS and CSS files instead of CDNs 60 | 61 | ## [0.1.0dev1] - 2016-11-21 62 | - Initial Release 63 | 64 | [0.1.0dev4]: https://github.com/nahun/pfweb/tree/v0.1.0dev4 65 | [0.1.0dev3]: https://github.com/nahun/pfweb/tree/v0.1.0dev3 66 | [0.1.0dev2]: https://github.com/nahun/pfweb/tree/0.1.0.dev2 67 | [0.1.0dev1]: https://github.com/nahun/pfweb/commit/b6f7396 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2016, Nathan Wheeler 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.md 2 | recursive-include pfweb *.html *.js *.css *.eot *.svg *.ttf *.woff *.woff2 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pfweb 2 | 3 | pfweb is a python web application to manage the OpenBSD Packet Filter (PF). It 4 | uses *py-pf* to interface with PF and Flask for the web framework. The look 5 | and feel is based on pfSense and a lot of the ideas are ripped off from them. 6 | 7 | ## Warning! 8 | There are a lot of people that would say running a web interface for PF is a 9 | bad idea. There are many reasons why, but here are a couple good ones: 10 | 11 | - **Security**: Running pfweb requires the user running the application to have 12 | write access to */dev/pf* to make changes. This gives access to the kernel. 13 | - **Features**: When using a web application to manage PF instead of 14 | just modifying *pf.conf*, you lose massive amounts of powerful features and 15 | flexibility. 16 | 17 | So why would use you use pfweb? Maybe a home network or an already secured lab. 18 | I don't judge though, use it how you want. I've had fun making it as well. 19 | 20 | ### Development 21 | 22 | As of Nov 2016 pfweb is under initial development. Use at your own risk. 23 | 24 | ## Dependencies 25 | 26 | - [OpenBSD 6.0+](http://www.openbsd.org/): Only tested on OpenBSD 6.0 amd64 27 | - [py-pf](http://www.kernel-panic.it/software/py-pf/): Python module for 28 | managing OpenBSD's Packet Filter 29 | - [Flask](http://flask.pocoo.org/): A microframework for Python based on 30 | Werkzeug and Jinja 2 31 | - [Flask-Login](https://flask-login.readthedocs.io/): User session management 32 | for Flask 33 | 34 | ## Installation 35 | 36 | Installation under a virtualenv will work fine. 37 | 38 | pfweb utilizes the well written 39 | [py-pf module](http://www.kernel-panic.it/software/py-pf/). The version in 40 | PyPi is not up to date so you'll need to clone from the 41 | [py-pf github repo](https://github.com/dotpy/py-pf) and install. 42 | 43 | ```sh 44 | $ git clone https://github.com/dotpy/py-pf.git 45 | $ cd py-pf 46 | $ python setup.py install 47 | ``` 48 | 49 | pfweb is under heavy development right now, so it is probably best to clone 50 | from github. First install Flask and Flask-Login then install pfweb. 51 | 52 | ```sh 53 | $ pip install Flask flask-login 54 | $ git clone https://github.com/nahun/pfweb.git 55 | $ cd pfweb 56 | $ python setup.py install 57 | ``` 58 | 59 | Or if the version on PyPi is actually current you can use pip: 60 | 61 | ```sh 62 | $ pip install pfweb 63 | ``` 64 | 65 | ## Setup 66 | 67 | After installation, you'll have to decide how you want to run it. There are 68 | many options for python web applications such as FastCGI, mod_wsgi, uWSGI, 69 | etc... You can refer to 70 | [Flask's documentation](http://flask.pocoo.org/docs/0.11/deploying/#deployment) 71 | for more detail, but this guide will concentrate on FastCGI, flup, and 72 | OpenBSD's httpd 73 | 74 | ### Install flup 75 | 76 | You'll need to install [flup](https://pypi.python.org/pypi/flup/1.0.2), the 77 | FastCGI server: 78 | 79 | ```sh 80 | $ pip install flup 81 | ``` 82 | 83 | ### Create the FastCGI Server 84 | 85 | Then you need to create a FastCGI server file such as *pfweb.fcgi*: 86 | 87 | ```python 88 | from flup.server.fcgi import WSGIServer 89 | import pfweb 90 | 91 | if __name__ == '__main__': 92 | WSGIServer(pfweb.app, bindAddress='/var/www/run/pfweb.sock').run() 93 | ``` 94 | 95 | Make sure the socket file path is in httpd's chroot of /var/www otherwise 96 | httpd won't be able to read it. 97 | 98 | ### Setup httpd 99 | 100 | Setup `/etc/httpd.conf` to use the fastcgi socket and listen on your IP. Edit 101 | the certificate paths with your own. 102 | 103 | ``` 104 | domain="example.com" 105 | 106 | server $domain { 107 | listen on 1.2.3.4 port 80 108 | block return 301 "https://$SERVER_NAME$REQUEST_URI" 109 | } 110 | 111 | server $domain { 112 | listen on 1.2.3.4 tls port 443 113 | fastcgi socket "/run/pfweb.sock" 114 | 115 | tls { 116 | certificate "/etc/ssl/example.com.crt" 117 | key "/etc/ssl/private/example.com.key" 118 | } 119 | } 120 | ``` 121 | 122 | Remember, httpd runs in a chroot under /var/www so set your fastcgi socket 123 | accordingly. 124 | 125 | ### PF Permissions 126 | 127 | You'll need to give a user access to /dev/pf and /etc/pf.conf so we don't run 128 | anything as root. Create or use whichever group you want, we'll use *pfweb*. 129 | Also make sure the user running your webserver and FastCGI server is a member 130 | of that group. 131 | 132 | ``` 133 | # chown root:pfweb /dev/pf /etc/pf.conf 134 | # chmod g+rw /dev/pf /etc/pf.conf 135 | ``` 136 | 137 | ### Create a Config File 138 | 139 | Now lets create the config file and username and password used to login to 140 | pfweb. The *pfweb.ini* file can exist at: 141 | 142 | - ~/.pfweb.ini 143 | - /etc/pfweb.ini 144 | - /usr/local/etc/pfweb.ini 145 | 146 | pfweb will choose from that order. There are two required parameters that you 147 | must set manually, the Flask *secret_key* used in sessions and a *salt* to hash 148 | the password we'll be setting for authentication. Create the pfweb.ini with 149 | random strings used for these two parameters. 150 | 151 | ```ini 152 | [main] 153 | secret_key = longrandomstring 154 | salt = anotherrandomstring 155 | ``` 156 | 157 | ### Create a Username and Password 158 | 159 | There are a few ways we can accomplish creating the username and password: 160 | 161 | #### 1. Use `create_user.py` 162 | 163 | The script is distributed in the package so you will need to find it in your 164 | installation or just [download](create_user.py?raw=true) it from the 165 | repo. 166 | 167 | ```sh 168 | $ python create_user.py 169 | Enter username: admin 170 | Enter Password: 171 | Confirm Password: 172 | User tester created successfully 173 | ``` 174 | 175 | #### 2. pfweb Config() manually 176 | 177 | You can import the pfweb Config object and create the credentials manually: 178 | 179 | ```python 180 | >>> from pfweb.config import Config 181 | >>> c = Config() 182 | >>> c.create_user('your_username', 'your_password') 183 | ``` 184 | 185 | #### 3. Manually hash your password 186 | 187 | Using python you can hash your password and enter it in manually to the config 188 | file. This will hash the password the same way the Config.create_user() method 189 | does. 190 | 191 | ```python 192 | >>> import hashlib, binascii 193 | >>> salt='your_salt' 194 | >>> password='your_password' 195 | >>> dk = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 100000) 196 | >>> binascii.hexlify(dk) 197 | '26383a0418be31cb6906418b367e19eb404cb8296e8f03521244b21cc079b82c' 198 | ``` 199 | 200 | Copy that hash and paste it into your config file with your username: 201 | 202 | ```ini 203 | [main] 204 | secret_key = longrandomstring 205 | salt = anotherrandomstring 206 | username = admin 207 | password = 26383a0418be31cb6906418b367e19eb404cb8296e8f03521244b21cc079b82c 208 | ``` 209 | 210 | ### Run the servers 211 | 212 | Run the FastCGI server and (re)start httpd. The httpd_flags="" command is 213 | obviously optional if you already have it running. Make sure to run the 214 | FastCGI server as the correct user that has access to PF. 215 | ```sh 216 | $ python pfweb.fcgi 217 | ``` 218 | 219 | ``` 220 | # echo httpd_flags="" >> /etc/rc.conf.local 221 | # rcctl restart httpd 222 | ``` 223 | 224 | You should now be able be able to reach pfweb in your browser 225 | 226 | ## Considerations 227 | 228 | ### FastCGI Process Managers 229 | 230 | Just as the 231 | [Flask docs](http://flask.pocoo.org/docs/0.11/deploying/fastcgi/#running-fastcgi-processes) 232 | say, you may want to use a process manager for your FastCGI server. 233 | [Supervisor](http://supervisord.org/configuration.html#fcgi-program-x-section-settings) 234 | works and OpenBSD has a package. Install with `pkg_add supervisor` then create 235 | a config file at `/etc/supervisord.d/pfweb.ini` 236 | 237 | ```ini 238 | [fcgi-program:pfweb] 239 | socket=unix:///var/www/run/pfweb.sock 240 | command=/path/to/python /path/to/pfweb.fcgi 241 | socket_owner=www 242 | user=www 243 | process_name=%(program_name)s_%(process_num)02d 244 | numprocs=5 245 | autostart=true 246 | autorestart=true 247 | ``` 248 | 249 | Edit `/etc/supervisord.conf` and uncomment the two lines at the end: 250 | 251 | ```ini 252 | [include] 253 | files = supervisord.d/*.ini 254 | ``` 255 | 256 | Restart supervisord and you should be good to go. 257 | 258 | ## Screenshots 259 | 260 | [Imgur Album](http://imgur.com/a/JsqCF) 261 | -------------------------------------------------------------------------------- /create_user.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python2.7 2 | from pfweb.config import Config 3 | 4 | from getpass import getpass 5 | 6 | config = Config() 7 | 8 | username = "" 9 | while True: 10 | prompt = 'Enter username: ' 11 | if username != "": 12 | prompt = 'Enter username [{}]: '.format(username) 13 | 14 | username_input = raw_input(prompt) 15 | if username_input != "": 16 | username = username_input 17 | 18 | if username == "": 19 | print "Username cannot be blank" 20 | continue 21 | 22 | password1 = getpass('Enter Password: ') 23 | password2 = getpass('Confirm Password: ') 24 | 25 | if password1 == password2: 26 | break 27 | else: 28 | print "Passwords do not match" 29 | 30 | config.create_user(username, password1) 31 | 32 | print "User " + username + " created successfully" -------------------------------------------------------------------------------- /pfweb/__init__.py: -------------------------------------------------------------------------------- 1 | from pfweb.app import app 2 | __all__ = ['app', 'config', 'constants'] 3 | -------------------------------------------------------------------------------- /pfweb/app.py: -------------------------------------------------------------------------------- 1 | import pf 2 | import socket 3 | import json 4 | import platform, subprocess, time, shutil 5 | from datetime import timedelta 6 | 7 | from flask import Flask, session, render_template, redirect, url_for, request, jsonify 8 | import flask_login 9 | 10 | from pfweb.config import Config 11 | from pfweb.constants import * 12 | 13 | # Get config settings 14 | settings = Config() 15 | settings.get_settings() 16 | 17 | # Setup Flask 18 | app = Flask(__name__) 19 | app.secret_key = settings.secret_key 20 | # Set session lifetime 21 | app.permanent_session_lifetime = timedelta(minutes=240) 22 | 23 | # Setup Login Manager extension 24 | login_manager = flask_login.LoginManager() 25 | login_manager.session_protection = "strong" 26 | login_manager.init_app(app) 27 | 28 | # Load packet filter to be used in views 29 | packetfilter = pf.PacketFilter() 30 | 31 | class BadRequestError(Exception): 32 | """HTTP 400""" 33 | 34 | class User(flask_login.UserMixin): 35 | """Flask Login User Class""" 36 | 37 | @login_manager.user_loader 38 | def user_loader(username): 39 | """Flask Login User Loader""" 40 | 41 | if username != settings.username: 42 | return None 43 | 44 | user = User() 45 | user.id = username 46 | return user 47 | 48 | @login_manager.unauthorized_handler 49 | def unauthorized_handler(): 50 | """Redirect to the login page when not authenticated""" 51 | return redirect(url_for('login')) 52 | 53 | @app.before_request 54 | def before_request(): 55 | """Operations performed on every request""" 56 | 57 | # Reset session timer 58 | session.modified = True 59 | 60 | @app.route("/login", methods=['GET', 'POST']) 61 | def login(): 62 | """Show login page and authenticate user""" 63 | 64 | if not settings.username or not settings.password: 65 | return "Config Error: username or password is not set" 66 | 67 | # Initialize alert message 68 | message = None 69 | 70 | # Process form fields 71 | if request.method == 'POST': 72 | # Process user login 73 | if request.form.get('login.submitted'): 74 | username = request.form.get('username') 75 | if username == settings.username and settings.hash_password(request.form.get('password')) == settings.password: 76 | user = User() 77 | user.id = username 78 | flask_login.login_user(user) 79 | return redirect(url_for('dash')) 80 | else: 81 | message = "Bad username or password" 82 | 83 | if settings.username == None or settings.password == None: 84 | # Show error about no initial user. Should never actually happen 85 | message = "Initial user not yet created" 86 | return render_template('login.html', no_login=True, message=message) 87 | 88 | return render_template('login.html', message=message) 89 | 90 | @app.route('/logout') 91 | def logout(): 92 | """Logout user and redirect to home""" 93 | 94 | flask_login.logout_user() 95 | return redirect(url_for('dash')) 96 | 97 | @app.route("/") 98 | @flask_login.login_required 99 | def dash(): 100 | """Show home dashboard""" 101 | 102 | # Get uptime 103 | current_time = int(time.time()) 104 | uptime_seconds = int(subprocess.check_output(["/sbin/sysctl", "-n", "kern.boottime"])) 105 | uptime_delta = timedelta(seconds=(current_time - uptime_seconds)) 106 | 107 | # Place info in a dict 108 | sys_info = { 109 | 'hostname': socket.getfqdn(), 110 | 'os': "{} {} ({})".format(platform.system(), platform.release(), platform.machine()), 111 | 'uptime': str(uptime_delta), 112 | 'current_time': time.strftime("%a, %b %d %Y %H:%M:%S %Z", time.localtime()) 113 | } 114 | 115 | # Interfaces to skip for stats 116 | skip_ifaces = ['all', 'carp', 'egress', 'enc', 'enc0', 'lo', 'lo0', 'pflog', 'pflog0'] 117 | # Type of stats to use 118 | stats = ['Rules', 'States', 'Packets In', 'Packets Out', 'Bytes In', 'Bytes Out'] 119 | # Initialize the structures to hold the data 120 | if_stats = dict() 121 | if_info = list() 122 | 123 | # Start the table string for stats 124 | ifstats_output = ""; 125 | 126 | # Go through each interface 127 | for iface in packetfilter.get_ifaces(): 128 | if iface.name not in skip_ifaces: 129 | # Add up all the packet and bytes 130 | packets_in = iface.packets["in"][pf.PF_PASS][0] + iface.packets["in"][pf.PF_DROP][0] + iface.packets["in"][pf.PF_PASS][1] + iface.packets["in"][pf.PF_DROP][1] 131 | packets_out = iface.packets["out"][pf.PF_PASS][0] + iface.packets["out"][pf.PF_DROP][0] + iface.packets["out"][pf.PF_PASS][1] + iface.packets["out"][pf.PF_DROP][1] 132 | bytes_in = iface.bytes["in"][pf.PF_PASS][0] + iface.bytes["in"][pf.PF_DROP][0] + iface.bytes["in"][pf.PF_PASS][1] + iface.bytes["in"][pf.PF_DROP][1] 133 | bytes_out = iface.bytes["out"][pf.PF_PASS][0] + iface.bytes["out"][pf.PF_DROP][0] + iface.bytes["out"][pf.PF_PASS][1] + iface.bytes["out"][pf.PF_DROP][1] 134 | 135 | # Store each into a dict 136 | if_stats[iface.name] = { 137 | 'name': iface.name, 138 | 'Rules': iface.rules, 139 | 'States': iface.states, 140 | 'Packets In': sizeof_fmt(packets_in, num_type='int'), 141 | 'Packets Out': sizeof_fmt(packets_out, num_type='int'), 142 | 'Bytes In': sizeof_fmt(bytes_in), 143 | 'Bytes Out': sizeof_fmt(bytes_out) 144 | } 145 | 146 | # Add interface header to table 147 | ifstats_output += "{}".format(iface.name) 148 | 149 | # Gather interface info from ifconfig and split into array of lines 150 | ifconfig = subprocess.check_output(["/sbin/ifconfig", str(iface.name)]).splitlines() 151 | # Initialize dict for interface info. Sets used on IPs so only shows uniques 152 | if_info_dict = { 153 | 'ipv4': set(), 154 | 'ipv6': set(), 155 | 'media': None, 156 | 'status': None, 157 | 'name': iface.name 158 | } 159 | # Run through each line of ifconfig output 160 | for line in ifconfig: 161 | # Remove first tab and split line into fields of words 162 | line = line.replace('\t', '', 1).split(" ") 163 | if line[0] == 'inet': 164 | # IPv4 addresses 165 | if_info_dict['ipv4'].add(line[1]) 166 | elif line[0] == 'inet6' and not line[1].startswith('fe80'): 167 | # IPv6 addresses. Skip local and deprecated IPs 168 | if_info_dict['ipv6'].add((line[1], True if 'deprecated' in line else False)) 169 | elif line[0] == 'media:': 170 | # Get speed and duplex 171 | if line[2] == 'autoselect': 172 | if_info_dict['media'] = "{} {}".format(line[3].strip("("), line[4].strip(")").split(",")[0]) 173 | else: 174 | if_info_dict['media'] = "{} {}".format(line[2], line[3].split(",")[0]) 175 | elif line[0] == 'status:': 176 | # Show whether interface is up or down 177 | if line[1] == 'active': 178 | if_info_dict['status'] = True 179 | else: 180 | if_info_dict['status'] = False 181 | 182 | # Add info to overall list 183 | if_info.append(if_info_dict) 184 | 185 | # Continue to body in stats table 186 | ifstats_output += "" 187 | for s in stats: 188 | # Add stat header in new row 189 | ifstats_output += "{}".format(s) 190 | # Run through each interface and add their stats 191 | for iface in sorted(if_stats): 192 | ifstats_output += "{}".format(if_stats[iface][s]) 193 | ifstats_output += "" 194 | 195 | ifstats_output += "" 196 | 197 | # Get overall PF stats 198 | pf_status = packetfilter.get_status() 199 | pf_status_since = pf._utils.uptime() - int(pf_status.since) 200 | pf_info = { 201 | 'enabled': pf_status.running, 202 | 'since': timedelta(seconds=pf_status_since), 203 | 'states': pf_status.states, 204 | 'match': { 'total': sizeof_fmt(pf_status.cnt['match'], num_type='int'), 'rate': "{:.1f}".format(pf_status.cnt['match'] / float(pf_status_since)) }, 205 | 'searches': { 'total': sizeof_fmt(pf_status.fcnt['searches'], num_type='int'), 'rate': "{:.1f}".format(pf_status.fcnt['searches'] / float(pf_status_since)) }, 206 | 'inserts': { 'total': sizeof_fmt(pf_status.fcnt['inserts'], num_type='int'), 'rate': "{:.1f}".format(pf_status.fcnt['inserts'] / float(pf_status_since)) }, 207 | 'removals': { 'total': sizeof_fmt(pf_status.fcnt['removals'], num_type='int'), 'rate': "{:.1f}".format(pf_status.fcnt['removals'] / float(pf_status_since)) } 208 | } 209 | 210 | return render_template('dash.html', sys_info=sys_info, pf_info=pf_info, if_stats=ifstats_output, if_info=if_info, logged_in=flask_login.current_user.get_id(), hometab='active') 211 | 212 | @app.route("/firewall/rules", methods=['GET', 'POST']) 213 | @app.route("/firewall/rules/", methods=['GET', 'POST']) 214 | @flask_login.login_required 215 | def rules(message=None): 216 | """Gather pf rules and show rules page""" 217 | 218 | if request.method == 'POST': 219 | # Remove rules 220 | if request.form.get('delete_rules') == "true": 221 | # Create list of rules to remove 222 | remove_list = list() 223 | for item, value in request.form.iteritems(): 224 | if item[:5] == 'rule_': 225 | remove_list.append(int(value)) 226 | 227 | # Remove each rule with the higher IDs first 228 | ruleset = packetfilter.get_ruleset() 229 | for value in sorted(remove_list, reverse=True): 230 | ruleset.remove(value) 231 | # Load edited ruleset 232 | packetfilter.load_ruleset(ruleset) 233 | message = PFWEB_ALERT_SUCCESS_DEL 234 | # Save pf.conf 235 | save_pfconf(packetfilter) 236 | # Set new order of rules 237 | elif request.form.get('save_order'): 238 | # Load JSON list 239 | form_order = json.loads(request.form['save_order']) 240 | # Load ruleset and rules. Create new ruleset to load 241 | old_ruleset = packetfilter.get_ruleset() 242 | old_rules = old_ruleset.rules 243 | new_ruleset = pf.PFRuleset() 244 | 245 | # Add the tables to the new ruleset 246 | new_ruleset.append(*old_ruleset.tables) 247 | 248 | # Run through new order and append each rule into their new spot 249 | for old_index in form_order: 250 | new_ruleset.append(old_rules[old_index]) 251 | 252 | # Load the ruleset back in 253 | packetfilter.load_ruleset(new_ruleset) 254 | # Save pf.conf 255 | save_pfconf(packetfilter) 256 | 257 | message = PFWEB_ALERT_SUCCESS_ORDER 258 | 259 | return redirect(url_for('rules', message=message), code=302) 260 | 261 | if message == PFWEB_ALERT_SUCCESS_DEL: 262 | message = { 'alert': 'success', 'msg': 'Successfully deleted rule(s)' } 263 | elif message == PFWEB_ALERT_SUCCESS_ORDER: 264 | message = { 'alert': 'success', 'msg': 'Successfully reordered rules' } 265 | elif message == PFWEB_ALERT_SUCCESS_EDIT: 266 | message = { 'alert': 'success', 'msg': 'Successfully edited rule' } 267 | elif message == PFWEB_ALERT_SUCCESS_ADD: 268 | message = { 'alert': 'success', 'msg': 'Successfully added rule' } 269 | 270 | rules = get_rules(packetfilter) 271 | 272 | # Create a dictionary of tables 273 | table_list = get_tables(packetfilter) 274 | tables = { t['name']: t['addrs'] for t in table_list } 275 | 276 | return render_template('rules.html', logged_in=flask_login.current_user.get_id(), fw_tab='active', rules=rules, tables=tables, port_ops=PFWEB_PORT_OPS, message=message) 277 | 278 | @app.route("/firewall/rules/remove/") 279 | @flask_login.login_required 280 | def remove_rule(rule_id): 281 | """Remove a single rule""" 282 | ruleset = packetfilter.get_ruleset() 283 | ruleset.remove(rule_id) 284 | packetfilter.load_ruleset(ruleset) 285 | save_pfconf(packetfilter) 286 | return redirect(url_for('rules', message=PFWEB_ALERT_SUCCESS_DEL), code=302) 287 | 288 | @app.route("/firewall/rules/edit/", methods=['GET', 'POST']) 289 | @app.route("/firewall/rules/edit", methods=['GET', 'POST']) 290 | @flask_login.login_required 291 | def edit_rule(rule_id=None): 292 | """Edit a single rule""" 293 | 294 | # Save edit or new rule 295 | if request.method == 'POST': 296 | # Get all form items into simple dict 297 | fields = dict() 298 | for item, val in request.form.iteritems(): 299 | fields[item] = val 300 | 301 | # Parse user input into pf.PFRule object 302 | rule = translate_rule(packetfilter, id=rule_id, **fields) 303 | 304 | if not isinstance(rule, pf.PFRule): 305 | raise BadRequestError(rule) 306 | 307 | ruleset = packetfilter.get_ruleset() 308 | message = None 309 | if rule_id or rule_id == 0: 310 | ruleset.remove(rule_id) 311 | ruleset.insert(rule_id, rule) 312 | message = PFWEB_ALERT_SUCCESS_EDIT 313 | else: 314 | ruleset.append(rule) 315 | message = PFWEB_ALERT_SUCCESS_ADD 316 | 317 | packetfilter.load_ruleset(ruleset) 318 | save_pfconf(packetfilter) 319 | 320 | # redirect to rules page 321 | return redirect(url_for('rules', message=message), code=302) 322 | 323 | # Load existing or create new rule 324 | if rule_id or rule_id == 0: 325 | # Load the current ruleset and parse existing rule 326 | ruleset = packetfilter.get_ruleset() 327 | rule = get_rule(ruleset.rules[rule_id]) 328 | # Set the ID 329 | rule['id'] = rule_id 330 | else: 331 | # Create new blank rule 332 | blank_rule = pf.PFRule() 333 | # Direction is in by default 334 | blank_rule.direction = pf.PF_IN 335 | # Use IPv4 by default 336 | blank_rule.af = socket.AF_INET 337 | # Enable keep_state by default 338 | blank_rule.keep_state = pf.PF_STATE_NORMAL 339 | rule = get_rule(blank_rule) 340 | 341 | tables = get_tables(packetfilter) 342 | 343 | return render_template('edit_rule.html', logged_in=flask_login.current_user.get_id(), fw_tab='active', 344 | rule=rule, tables=tables, ifaces=get_ifaces(packetfilter), 345 | icmp_types=PFWEB_ICMP_TYPES, icmp6_types=PFWEB_ICMP6_TYPES, port_ops=PFWEB_PORT_OPS) 346 | 347 | @app.route("/firewall/tables", methods=['GET', 'POST']) 348 | @app.route("/firewall/tables/remove/") 349 | @flask_login.login_required 350 | def tables(table_name=None): 351 | """ 352 | Show existing pf tables 353 | 354 | Removes tables when form is submitted 355 | """ 356 | 357 | remove_error = list() 358 | # Remove multiple tables 359 | if request.method == 'POST': 360 | if 'delete_tables' in request.form and request.form['delete_tables'] == "true": 361 | # Create list of tables to remove 362 | remove_list = list() 363 | for t in packetfilter.get_tables(): 364 | if 'table_' + t.name in request.form and request.form['table_' + t.name] == t.name: 365 | if table_in_use(packetfilter, t.name): 366 | remove_error.append(t.name) 367 | else: 368 | remove_list.append(t) 369 | 370 | if len(remove_error) == 0: 371 | packetfilter.del_tables(*remove_list) 372 | save_pfconf(packetfilter) 373 | return redirect(url_for('tables'), code=302) 374 | 375 | tables = get_tables(packetfilter) 376 | return render_template('tables.html', logged_in=flask_login.current_user.get_id(), fw_tab='active', tables=tables, remove_error=remove_error) 377 | 378 | @app.route("/firewall/tables/edit/", methods=['GET', 'POST']) 379 | @app.route("/firewall/tables/edit", methods=['GET', 'POST']) 380 | @flask_login.login_required 381 | def edit_table(table_name=None): 382 | """Edit a table""" 383 | 384 | # Save edit or new table 385 | if request.method == 'POST': 386 | table_addrs = translate_table(request.form) 387 | 388 | if not isinstance(table_addrs, list): 389 | raise BadRequestError(table) 390 | 391 | if ' ' in str(table_name).strip(): 392 | raise BadRequestError('Table name cannot contain spaces') 393 | elif table_name: 394 | try: 395 | packetfilter.set_addrs(table_name, *table_addrs) 396 | except IOError: 397 | raise BadRequestError('Invalid address') 398 | else: 399 | if is_blank(request.form.get('name')): 400 | raise BadRequestError('Table name cannot be empty') 401 | elif ' ' in request.form.get('name').strip(): 402 | raise BadRequestError('Table name cannot contain spaces') 403 | packetfilter.add_tables(pf.PFTable(request.form['name'].strip(), *table_addrs, flags=pf.PFR_TFLAG_PERSIST)) 404 | 405 | save_pfconf(packetfilter) 406 | 407 | return redirect(url_for('tables'), code=302) 408 | 409 | # Load existing or create new rule 410 | if table_name: 411 | # Load the current tableset and parse existing table 412 | tables = packetfilter.get_tables() 413 | for t in tables: 414 | if str(t.name) == str(table_name): 415 | table = get_table(t) 416 | break 417 | else: 418 | raise BadRequestError("No such table") 419 | else: 420 | # Create new blank table 421 | blank_table = pf.PFTable() 422 | table = get_table(blank_table) 423 | 424 | return render_template('edit_table.html', logged_in=flask_login.current_user.get_id(), fw_tab='active', 425 | table=table) 426 | 427 | @app.route('/status/pfinfo') 428 | @flask_login.login_required 429 | def pfinfo(): 430 | """Display most information that `pfctl -s info -v` would""" 431 | 432 | status = { 433 | 'info': packetfilter.get_status(), 434 | 'ifaces': packetfilter.get_ifaces(), 435 | 'limits': packetfilter.get_limit(), 436 | 'timeouts': packetfilter.get_timeout() 437 | } 438 | 439 | return render_template('pfinfo.html', logged_in=flask_login.current_user.get_id(), status_tab='active', status=status) 440 | 441 | @app.route('/status/states', methods=['GET', 'POST']) 442 | @flask_login.login_required 443 | def states(): 444 | """Show all contents of the state table and allow a state to be removed""" 445 | 446 | if request.method == 'POST': 447 | # Remove individual state 448 | if request.form.get('action') == 'remove': 449 | # Make sure correct parameters were sent 450 | if request.form.get('src') and request.form.get('dst'): 451 | if '[' in request.form.get('src') or '.' not in request.form.get('src'): 452 | # Handle IPv6 addresses 453 | src_addr_port = request.form.get('src').split('[') 454 | dst_addr_port = request.form.get('dst').split('[') 455 | # Make sure there was a port set 456 | if len(src_addr_port) == 2: 457 | src_addr_port[1] = src_addr_port[1].split(']')[0] 458 | else: 459 | src_addr_port.append(0) 460 | if len(dst_addr_port) == 2: 461 | dst_addr_port[1] = dst_addr_port[1].split(']')[0] 462 | else: 463 | dst_addr_port.append(0) 464 | 465 | else: 466 | # IPv4 address and port 467 | src_addr_port = request.form.get('src').split(':') 468 | dst_addr_port = request.form.get('dst').split(':') 469 | 470 | # Create PFRuleAddr object from address and ports 471 | src_addr = pf.PFRuleAddr(pf.PFAddr(src_addr_port[0]), 472 | pf.PFPort(src_addr_port[1], 0, pf.PF_OP_EQ)) 473 | dst_addr = pf.PFRuleAddr(pf.PFAddr(dst_addr_port[0]), 474 | pf.PFPort(dst_addr_port[1], 0, pf.PF_OP_EQ)) 475 | 476 | packetfilter.kill_states(src=src_addr, dst=dst_addr) 477 | 478 | # Return a JSON object of just the src and dst we removed 479 | return jsonify({ 'src': "{}".format(request.form.get('src')), 'dst': "{}".format(request.form.get('dst')) }) 480 | else: 481 | # Return a simple 400 response when src or dst were not provided 482 | message = { 483 | 'status': 400, 484 | 'message': 'Invalid parameters' 485 | } 486 | resp = jsonify(message) 487 | resp.status_code = 400 488 | return resp 489 | else: 490 | # Return a simple 400 response the wrong action provided 491 | message = { 492 | 'status': 400, 493 | 'message': 'Unknown action' 494 | } 495 | resp = jsonify(message) 496 | resp.status_code = 400 497 | return resp 498 | 499 | states = list() 500 | 501 | for state in packetfilter.get_states(): 502 | # Set direction for src and dst 503 | (src, dst) = (1, 0) if state.direction == pf.PF_OUT else (0, 1) 504 | 505 | # Set the source address and port. Only set port if it is not 0 506 | src_line = "{}".format(state.nk.addr[src]) 507 | if str(state.nk.port[src]): 508 | src_line += (":{}" if state.af == socket.AF_INET else "[{}]").format(state.nk.port[src]) 509 | # Show and NAT (or rdr) address 510 | if (state.nk.addr[src] != state.sk.addr[src] or state.nk.port[src] != state.sk.port[src]): 511 | src_line += " ({}".format(state.sk.addr[src]) 512 | if str(state.sk.port[src]): 513 | src_line += (":{})" if state.af == socket.AF_INET else "[{}])").format(state.sk.port[src]) 514 | 515 | # Repeat for destination 516 | dst_line = "{}".format(state.nk.addr[dst]) 517 | if str(state.nk.port[dst]): 518 | dst_line += (":{}" if state.af == socket.AF_INET else "[{}]").format(state.nk.port[dst]) 519 | 520 | if (state.nk.addr[dst] != state.sk.addr[dst] or state.nk.port[dst] != state.sk.port[dst]): 521 | dst_line += " ({}".format(state.sk.addr[dst]) 522 | if str(state.sk.port[dst]): 523 | dst_line += (":{})" if state.af == socket.AF_INET else "[{}])").format(state.sk.port[dst]) 524 | 525 | state_desc = "" 526 | if state.proto == socket.IPPROTO_TCP: 527 | state_desc = "{}:{}".format(PFWEB_TCP_STATES[state.src.state], PFWEB_TCP_STATES[state.dst.state]) 528 | elif state.proto == socket.IPPROTO_UDP: 529 | state_desc = "{}:{}".format(PFWEB_UDP_STATES[state.src.state], PFWEB_UDP_STATES[state.dst.state]) 530 | else: 531 | state_desc = "{}:{}".format(PFWEB_OTHER_STATES[state.src.state], PFWEB_OTHER_STATES[state.dst.state]) 532 | 533 | state_struct = { 534 | 'ifname': state.ifname, 535 | 'proto': PFWEB_IPPROTO[state.proto], 536 | 'src': src_line, 537 | 'dst': dst_line, 538 | 'state': state_desc, 539 | 'packets': [int(sum(state.packets)) ,"TX: {}
RX: {}".format(sizeof_fmt(state.packets[0], num_type='int'), sizeof_fmt(state.packets[1], num_type='int'))], 540 | 'bytes': [int(sum(state.bytes)), "TX: {}
RX: {}".format(sizeof_fmt(state.bytes[0]), sizeof_fmt(state.bytes[1]))], 541 | 'expires': [int(state.expire), timedelta(seconds=state.expire)] 542 | } 543 | states.append(state_struct) 544 | 545 | return render_template('states.html', logged_in=flask_login.current_user.get_id(), status_tab='active', states=states) 546 | 547 | @app.errorhandler(BadRequestError) 548 | @flask_login.login_required 549 | def bad_request(error): 550 | """Show HTTP 400 page when BadRequestError is raised""" 551 | return render_template('error.html', logged_in=flask_login.current_user.get_id(), msg=error.message), 400 552 | 553 | def get_rules(pfilter): 554 | """Return list of rules for template""" 555 | web_rules = list() 556 | ruleset = pfilter.get_ruleset() 557 | count = 0 558 | 559 | # Parse each rule into human readable values 560 | for rule in ruleset.rules: 561 | new = get_rule(rule) 562 | 563 | new['id'] = count 564 | 565 | web_rules.append(new) 566 | count += 1 567 | return web_rules 568 | 569 | def get_rule(rule): 570 | """Gather rule information into a data structure for rendering to a template""" 571 | new = dict() 572 | 573 | # Rule Action 574 | if rule.action == pf.PF_PASS: 575 | new['action'] = "pass" 576 | elif rule.action == pf.PF_DROP: 577 | block_return = rule.rule_flag & pf.PFRULE_RETURN 578 | if block_return: 579 | new['action'] = "reject" 580 | else: 581 | new['action'] = "block" 582 | elif rule.action == pf.PF_MATCH: 583 | new['action'] = "match" 584 | else: 585 | new['action'] = "else" 586 | 587 | # Direction 588 | if rule.direction == pf.PF_IN: 589 | new['direction'] = "in" 590 | elif rule.direction == pf.PF_OUT: 591 | new['direction'] = "out" 592 | elif rule.direction == pf.PF_INOUT: 593 | new['direction'] = "both" 594 | 595 | # Interface 596 | if rule.ifname: 597 | new['iface'] = rule.ifname 598 | else: 599 | new['iface'] = "All" 600 | 601 | # AF Protocol 602 | if rule.af == socket.AF_INET: 603 | new['af'] = "IPv4" 604 | elif rule.af == socket.AF_INET6: 605 | new['af'] = "IPv6" 606 | elif rule.af == socket.AF_UNSPEC: 607 | new['af'] = "*" 608 | 609 | # Layer 4 Protocol 610 | if rule.proto == socket.IPPROTO_UDP: 611 | new['proto'] = "UDP" 612 | elif rule.proto == socket.IPPROTO_TCP: 613 | new['proto'] = "TCP" 614 | elif rule.proto == socket.IPPROTO_ICMP: 615 | new['proto'] = "ICMP" 616 | elif rule.proto == socket.IPPROTO_ICMPV6: 617 | new['proto'] = "ICMPV6" 618 | else: 619 | new['proto'] = '*' 620 | 621 | # ICMP 622 | new['icmp_type'] = rule.type 623 | 624 | # Source 625 | (new['src_addr'], new['src_addr_type'], new['src_port_op'], new['src_port']) = get_addr_port(rule.src) 626 | 627 | # Destination 628 | (new['dst_addr'], new['dst_addr_type'], new['dst_port_op'], new['dst_port']) = get_addr_port(rule.dst) 629 | 630 | # NAT 631 | new['trans_type'] = False 632 | if rule.nat.addr.type != pf.PF_ADDR_NONE and rule.nat.id == pf.PF_POOL_NAT: 633 | (new['trans_addr'], new['trans_addr_type'], new['trans_port_op'], new['trans_port']) = get_addr_port(rule.nat) 634 | new['nat_static_port'] = True if rule.nat.proxy_port.num[0] == rule.nat.proxy_port.num[1] == 0 else False 635 | new['trans_type'] = 'NAT' 636 | # RDR 637 | elif rule.rdr.addr.type != pf.PF_ADDR_NONE and rule.rdr.id == pf.PF_POOL_RDR: 638 | (new['trans_addr'], new['trans_addr_type'], new['trans_port_op'], new['trans_port']) = get_addr_port(rule.rdr) 639 | new['trans_type'] = 'RDR' 640 | 641 | if new.get('trans_port'): 642 | if (new['trans_port'][0] != 0 and new['trans_port'][1] == 0) or new['trans_port'][0] == new['trans_port'][1]: 643 | new['trans_port_op'] = pf.PF_OP_EQ 644 | else: 645 | new['trans_port_op'] = pf.PF_OP_RRG 646 | 647 | # Stats 648 | new['evaluations'] = sizeof_fmt(int(rule.evaluations), num_type='int') 649 | new['packets'] = sizeof_fmt(int(sum(rule.packets)), num_type='int') 650 | new['bytes'] = sizeof_fmt(int(sum(rule.bytes))) 651 | new['states'] = sizeof_fmt(int(rule.states_cur), num_type='int') 652 | new['states_creations'] = sizeof_fmt(int(rule.states_tot), num_type='int') 653 | 654 | # Label 655 | new['label'] = rule.label 656 | 657 | # Log 658 | if rule.log == pf.PF_LOG: 659 | new['log'] = True 660 | else: 661 | new['log'] = False 662 | 663 | # Keep State 664 | if rule.keep_state == pf.PF_STATE_NORMAL: 665 | new['keep_state'] = True 666 | else: 667 | new['keep_state'] = False 668 | 669 | # Quick 670 | new['quick'] = rule.quick 671 | 672 | return new 673 | 674 | def get_addr_port(rule_addr): 675 | """Return address and port information from a pf.PFRuleAddr object""" 676 | addr = "" 677 | addr_type = "" 678 | 679 | if rule_addr.addr.type == pf.PF_ADDR_ADDRMASK: 680 | # IPv4 or IPv6 Address 681 | if rule_addr.addr.addr is None: 682 | addr = "*" 683 | addr_type = 'any' 684 | else: 685 | # Convert mask to prefix length 686 | cidr = ntoc(rule_addr.addr.mask, rule_addr.addr.af) 687 | 688 | # Address in CIDR format 689 | if (cidr == 32 and rule_addr.addr.af == socket.AF_INET) or cidr == 128: 690 | addr = rule_addr.addr.addr 691 | else: 692 | addr = "{0.addr}/{1}".format(rule_addr.addr, cidr) 693 | addr_type = 'addrmask' 694 | elif rule_addr.addr.type == pf.PF_ADDR_RANGE: 695 | # Address range 696 | addr = "{0[0]} - {0[1]}".format(rule.src.addr) 697 | addr_type = 'range' 698 | elif rule_addr.addr.type == pf.PF_ADDR_TABLE: 699 | addr = rule_addr.addr.tblname 700 | addr_type = 'table' 701 | elif rule_addr.addr.type == pf.PF_ADDR_DYNIFTL: 702 | addr_type = 'dynif' 703 | addr = rule_addr.addr.ifname 704 | 705 | # PFPool objects use proxy_port 706 | try: 707 | port_op = rule_addr.port.op 708 | port_num = rule_addr.port.num 709 | except AttributeError: 710 | port_op = rule_addr.proxy_port.op 711 | port_num = rule_addr.proxy_port.num 712 | 713 | return (addr, addr_type, port_op, port_num) 714 | 715 | def ntoc(mask, af): 716 | """Convert netmask to prefix bit length""" 717 | 718 | if af == socket.AF_INET6: 719 | # IPv6 720 | return sum([bin(int(x, 16)).count("1") for x in mask.split(":") if x]) 721 | elif af == socket.AF_INET: 722 | # IPv4 723 | return sum([bin(int(x)).count("1") for x in mask.split(".")]) 724 | else: 725 | # Just return the mask if AF is unknown 726 | return mask 727 | 728 | 729 | def get_ifaces(pfilter): 730 | """Return all interfaces we care about""" 731 | skip_ifaces = ['carp', 'egress', 'enc', 'enc0', 'lo', 'pflog', 'pflog0'] 732 | all_ifaces = list() 733 | for iface in pfilter.get_ifaces(): 734 | if iface.name not in skip_ifaces: 735 | all_ifaces.append(iface.name) 736 | return all_ifaces 737 | 738 | def translate_rule(pfilter, **fields): 739 | """Parse form fields into a pf.PFRule""" 740 | 741 | # Load existing or create new rule 742 | if fields['id'] or fields['id'] == 0: 743 | ruleset = pfilter.get_ruleset() 744 | rule = ruleset.rules[fields['id']] 745 | else: 746 | rule = pf.PFRule() 747 | 748 | # Set action attribute 749 | if fields['action'] == 'pass': 750 | rule.action = pf.PF_PASS 751 | elif fields['action'] == 'block': 752 | rule.action = pf.PF_DROP 753 | rule.rule_flag = pf.PFRULE_DROP | 0 754 | elif fields['action'] == 'reject': 755 | rule.action = pf.PF_DROP 756 | rule.rule_flag = pf.PFRULE_RETURN | 0 757 | elif fields['action'] == 'match': 758 | rule.action = pf.PF_MATCH 759 | else: 760 | return "Action is not recognized" 761 | 762 | # Set direction attribute 763 | if fields['direction'] == 'in': 764 | rule.direction = pf.PF_IN 765 | elif fields['direction'] == 'out': 766 | rule.direction = pf.PF_OUT 767 | elif fields['direction'] == 'both': 768 | rule.direction = pf.PF_INOUT 769 | else: 770 | return "Direction is not recognized" 771 | 772 | # Set interface attribute 773 | if fields['iface'] in get_ifaces(pfilter): 774 | rule.ifname = fields['iface'] 775 | else: 776 | return "Unknown interface specified" 777 | 778 | # Set address family attribute 779 | if fields['af'] == '*': 780 | rule.af = socket.AF_UNSPEC 781 | elif fields['af'] == 'IPv4': 782 | rule.af = socket.AF_INET 783 | elif fields['af'] == 'IPv6': 784 | rule.af = socket.AF_INET6 785 | else: 786 | return "Unknown address family type" 787 | 788 | # Set protocol attribute 789 | if fields['proto'] == '*': 790 | rule.proto = socket.IPPROTO_IP 791 | elif fields['proto'] == 'TCP': 792 | rule.proto = socket.IPPROTO_TCP 793 | elif fields['proto'] == 'UDP': 794 | rule.proto = socket.IPPROTO_UDP 795 | elif fields['proto'] == 'ICMP' and fields['af'] == 'IPv4': 796 | rule.proto = socket.IPPROTO_ICMP 797 | elif fields['proto'] == 'ICMP' and fields['af'] == 'IPv6': 798 | rule.proto = socket.IPPROTO_ICMPV6 799 | else: 800 | return "Protocol is not supported" 801 | 802 | # ICMP Type 803 | if fields['proto'] == "ICMP": 804 | # Use ICMP or ICMP6 depending on AF 805 | if rule.af == socket.AF_INET: 806 | if fields['icmptype'] == 'any': 807 | rule.type = 0 808 | elif int(fields['icmptype']) in PFWEB_ICMP_TYPES: 809 | rule.type = int(fields['icmptype']) + 1 810 | else: 811 | return "Invalid ICMP Type" 812 | elif rule.af == socket.AF_INET6: 813 | if fields['icmp6type'] == 'any': 814 | rule.type = 0 815 | elif int(fields['icmp6type']) in PFWEB_ICMP6_TYPES: 816 | rule.type = int(fields['icmp6type']) + 1 817 | else: 818 | return "Invalid ICMP Type" 819 | else: 820 | return "Must specifiy IPv4 or IPv6 when using ICMP" 821 | 822 | # Source Address Rule 823 | rule.src = translate_addr_rule( 824 | fields.get('src_addr'), 825 | fields.get('src_addr_type'), 826 | fields.get('src_addr_table'), 827 | fields.get('src_port_op', pf.PF_OP_NONE), 828 | fields.get('src_port_from', 0), 829 | fields.get('src_port_to', 0), 830 | rule.proto, 831 | fields.get('src_addr_iface'), 832 | rule.af) 833 | # Destination Address Rule 834 | rule.dst = translate_addr_rule( 835 | fields.get('dst_addr'), 836 | fields.get('dst_addr_type'), 837 | fields.get('dst_addr_table'), 838 | fields.get('dst_port_op', pf.PF_OP_NONE), 839 | fields.get('dst_port_from', 0), 840 | fields.get('dst_port_to', 0), 841 | rule.proto, 842 | fields.get('dst_addr_iface'), 843 | rule.af) 844 | 845 | # Set any translation used NAT or RDR 846 | if fields.get('trans_type', 'none') != 'none' and (rule.af == socket.AF_INET or rule.af == socket.AF_INET6): 847 | pool = translate_pool_rule( 848 | fields.get('trans_type'), 849 | fields.get('trans_addr'), 850 | fields.get('trans_addr_type'), 851 | fields.get('trans_addr_table'), 852 | fields.get('trans_port_from'), 853 | fields.get('trans_port_to'), 854 | rule.proto, 855 | fields.get('trans_addr_iface'), 856 | rule.af) 857 | 858 | if fields['trans_type'].lower() == 'rdr': 859 | rule.rdr = pool 860 | rule.nat.addr.type = pf.PF_ADDR_NONE 861 | else: 862 | # Enable static port option 863 | if 'nat_static_port' in fields: 864 | pool.proxy_port = pf.PFPort((0, 0)) 865 | rule.nat = pool 866 | rule.rdr.addr.type = pf.PF_ADDR_NONE 867 | elif fields.get('trans_type', 'none') != 'none': 868 | return "Must specify IPv4 or IPv6 with translation" 869 | else: 870 | # Translation is disabled 871 | rule.rdr.addr.type = pf.PF_ADDR_NONE 872 | rule.nat.addr.type = pf.PF_ADDR_NONE 873 | 874 | # Log checkbox 875 | if 'log' in fields: 876 | rule.log = pf.PF_LOG 877 | else: 878 | rule.log = 0 879 | 880 | # Quick checkbox 881 | if 'quick' in fields: 882 | rule.quick = True 883 | else: 884 | rule.quick = False 885 | 886 | # Keep state checkbox 887 | if 'keep_state' in fields: 888 | rule.keep_state = pf.PF_STATE_NORMAL 889 | else: 890 | rule.keep_state = 0 891 | 892 | if 'label' in fields: 893 | rule.label = fields['label'] 894 | 895 | return rule 896 | 897 | def translate_addr_rule(addr, addr_type, addr_table, port_op, port_from, port_to, proto, addr_iface, af): 898 | """Parses fields given in the pfweb form to a pf.PFRuleAddr object""" 899 | pfaddr = False 900 | if addr_type == "addrmask" and af != socket.AF_UNSPEC: 901 | # Validate IP address 902 | pfaddr = translate_addrmask(af, addr) 903 | elif addr_type == "table": 904 | # Set addr to a table 905 | if not addr_table: 906 | return "Table cannot be empty" 907 | pfaddr = pf.PFAddr("<{}>".format(addr_table)) 908 | elif addr_type == "dynif": 909 | # Set addr to an interface 910 | if not addr_iface: 911 | return "Interface cannot be empty" 912 | pfaddr = pf.PFAddr("({})".format(addr_iface), af) 913 | 914 | # Do not set if ANY or proto is ICMP 915 | port = False 916 | if int(port_op) != pf.PF_OP_NONE and proto != socket.IPPROTO_ICMP and proto != socket.IPPROTO_ICMPV6: 917 | # Confirm port op 918 | if int(port_op) not in PFWEB_PORT_OPS: 919 | return "Invalid port op" 920 | 921 | # port from 922 | pfport_from = 0 923 | try: 924 | # Confirm input is a number 925 | if port_from != '': 926 | pfport_from = int(port_from) 927 | except ValueError: 928 | return "Invalid port number" 929 | 930 | pfport_to = 0 931 | # Set range 932 | if int(port_op) == pf.PF_OP_RRG or int(port_op) == pf.PF_OP_IRG or int(port_op) == pf.PF_OP_XRG: 933 | # Port to 934 | try: 935 | if port_to != '': 936 | pfport_to = int(port_to) 937 | except ValueError: 938 | return "Invalid port number" 939 | 940 | port = pf.PFPort((pfport_from, pfport_to), proto, int(port_op)) 941 | 942 | # Create and set the PFRuleAddr 943 | rule_addr = pf.PFRuleAddr() 944 | if pfaddr: 945 | rule_addr.addr = pfaddr 946 | if port: 947 | rule_addr.port = port 948 | 949 | return rule_addr 950 | 951 | def translate_pool_rule(trans_type, addr, addr_type, addr_table, port_from, port_to, proto, addr_iface, af): 952 | """Parses fields given in the pfweb form to a pf.PFPool object""" 953 | pfaddr = False 954 | if addr_type == 'addrmask': 955 | pfaddr = translate_addrmask(af, addr) 956 | elif addr_type == 'table': 957 | if not addr_table: 958 | return "Table cannot be empty" 959 | pfaddr = pf.PFAddr("<{}>".format(addr_table)) 960 | elif addr_type == 'dynif': 961 | if trans_type.lower() == 'rdr': 962 | return "Cannot RDR to an interface" 963 | if not addr_iface: 964 | return "Interface cannot be empty" 965 | # Set PFAddr to interface and IPv4 966 | pfaddr = pf.PFAddr("({})".format(addr_iface), af) 967 | 968 | pool_id = pf.PF_POOL_NAT 969 | port = False 970 | if trans_type.lower() == 'rdr' and (proto == socket.IPPROTO_TCP or proto == socket.IPPROTO_UDP): 971 | # Set ports to 0 if they were left blank 972 | if port_from == '': 973 | port_from = 0 974 | port_to = 0 975 | if port_to == '': 976 | port_to = 0 977 | 978 | pool_id = pf.PF_POOL_RDR 979 | try: 980 | port = pf.PFPort((int(port_from), int(port_to))) 981 | except ValueError: 982 | # The user didn't give us a valid number 983 | return "Invalid port number" 984 | elif trans_type.lower() == 'rdr' and not (proto == socket.IPPROTO_TCP or proto == socket.IPPROTO_UDP): 985 | return "TCP or UDP must be used for RDR" 986 | 987 | pool = pf.PFPool(pool_id, pfaddr) 988 | if port: 989 | pool.proxy_port = port 990 | 991 | return pool 992 | 993 | def translate_addrmask(af, addr): 994 | """Validate IP address""" 995 | addr_mask = addr.split("/") 996 | try: 997 | socket.inet_pton(af, addr_mask[0]) 998 | except socket.error: 999 | raise BadRequestError("Invalid IP address") 1000 | 1001 | # Test v4 or v6 mask 1002 | max_cidr_prefix = 32 if af == socket.AF_INET else 128 1003 | 1004 | if len(addr_mask) == 2 and int(addr_mask[1]) and (int(addr_mask[1]) < 0 or int(addr_mask[1]) > max_cidr_prefix): 1005 | raise BadRequestError("Invalid CIDR prefix") 1006 | 1007 | return pf.PFAddr(addr) 1008 | 1009 | def get_tables(pfilter): 1010 | """Return a list of tables for rendering a template""" 1011 | web_tables = list() 1012 | tables = pfilter.get_tables() 1013 | count = 0 1014 | 1015 | # Parse each table into dict for web UI 1016 | for table in tables: 1017 | new = get_table(table) 1018 | 1019 | new['id'] = count 1020 | 1021 | web_tables.append(new) 1022 | count += 1 1023 | 1024 | return web_tables 1025 | 1026 | def get_table(table): 1027 | """Gather table information into a data structure for rendering to a template""" 1028 | new = dict() 1029 | 1030 | new['name'] = table.name 1031 | new['addrs'] = list() 1032 | 1033 | for addr in table.addrs: 1034 | cidr = ntoc(addr.mask, addr.af) 1035 | if cidr == 32 or cidr == 128: 1036 | new['addrs'].append(addr.addr) 1037 | else: 1038 | new['addrs'].append("{}/{}".format(addr.addr, cidr)) 1039 | 1040 | return new 1041 | 1042 | def translate_table(fields): 1043 | """Parse form fields into a list addresses""" 1044 | addrs = list() 1045 | 1046 | for f in sorted(fields): 1047 | if f.startswith('addr') and fields[f]: 1048 | addrs.append(fields[f]) 1049 | 1050 | return addrs 1051 | 1052 | def table_in_use(pfilter, table): 1053 | """Return boolean if a table is in use by a rule""" 1054 | for t in pfilter.get_tstats(): 1055 | if t.table.name == table: 1056 | tstats = t 1057 | break 1058 | if tstats.refcnt['rules'] == 0 and tstats.refcnt['anchors'] == 0: 1059 | return False 1060 | else: 1061 | return True 1062 | 1063 | def sizeof_fmt(num, suffix='B', num_type='data'): 1064 | """ 1065 | Convert bytes into a human readable format 1066 | 1067 | Straight rip from stackoverflow 1068 | """ 1069 | if num_type == 'data': 1070 | for unit in ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']: 1071 | if abs(num) < 1024.0: 1072 | return "%3.1f %s%s" % (num, unit, suffix) 1073 | num /= 1024.0 1074 | return "%.1f %s%s" % (num, 'Yi', suffix) 1075 | else: 1076 | if abs(num) < 1000: 1077 | return num 1078 | for unit in ['', 'K', 'M', 'B']: 1079 | if abs(num) < 1000.0: 1080 | return "%3.1f %s" % (num, unit) 1081 | num /= 1000.0 1082 | return "%.1f %s" % (num, 'T') 1083 | 1084 | def save_pfconf(pfilter): 1085 | """Save the pf.conf file""" 1086 | 1087 | # Supported global options from config file 1088 | 1089 | # state-policy 1090 | global_options = list() 1091 | try: 1092 | valid_state_policy = ['if-bound', 'floating'] 1093 | if settings.state_policy not in valid_state_policy: 1094 | raise ValueError("Invalid state-policy setting '{}'".format(settings.state_policy)) 1095 | 1096 | global_options.append("set state-policy {}".format(settings.state_policy)) 1097 | except AttributeError: 1098 | pass 1099 | 1100 | # Gather the tables 1101 | tables = pfilter.get_tables() 1102 | tables_pfconf = list() 1103 | # Convert into strings 1104 | for t in tables: 1105 | tables_pfconf.append("table <{}> persist {{ {} }}".format(t.name, " ".join("{}/{}".format(ta.addr, ntoc(ta.mask, ta.af)) for ta in t.addrs))) 1106 | 1107 | pfconf_text = "{}\n\n{}\n\n{}\n".format("\n".join(global_options), "\n".join(tables_pfconf), str(pfilter.get_ruleset())) 1108 | 1109 | with open("/tmp/pf.conf.pfweb", 'w+') as pfconf_f: 1110 | pfconf_f.write(pfconf_text) 1111 | 1112 | shutil.copyfile("/tmp/pf.conf.pfweb", "/etc/pf.conf") 1113 | 1114 | def is_blank(val): 1115 | if str(val) == "" or not val: 1116 | return True 1117 | else: 1118 | return False 1119 | 1120 | if __name__ == "__main__": 1121 | app.debug = True 1122 | app.run(host='0.0.0.0', port=80) 1123 | -------------------------------------------------------------------------------- /pfweb/config.py: -------------------------------------------------------------------------------- 1 | import ConfigParser 2 | import os 3 | import hashlib, binascii 4 | 5 | class Config(): 6 | def __init__(self): 7 | self.username = None 8 | self.password = None 9 | 10 | def _read_file(self): 11 | """Read configuration file from one of multiple locations""" 12 | _config_parse = ConfigParser.ConfigParser() 13 | _configset = _config_parse.read([ 14 | os.path.expanduser('~') + "/.pfweb.ini", 15 | '/etc/pfweb.ini', 16 | '/usr/local/etc/pfweb.ini']) 17 | 18 | if len(_configset) < 1: 19 | raise ValueError("Cannot read any pfweb.ini file") 20 | 21 | self.config_file = _configset[0] 22 | 23 | return _config_parse 24 | 25 | def get_settings(self): 26 | """Store config settings into the class""" 27 | _config_parse = self._read_file() 28 | 29 | _required = { 'main': ['secret_key', 'salt'] } 30 | _optional = { 31 | 'main': ['username', 'password'], 32 | 'global': ['state_policy'] 33 | } 34 | 35 | for section, params in _required.iteritems(): 36 | for param in params: 37 | setattr(self, param, _config_parse.get(section, param)) 38 | 39 | for section, params in _optional.iteritems(): 40 | try: 41 | for param in params: 42 | setattr(self, param, _config_parse.get(section, param)) 43 | except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): 44 | pass 45 | 46 | def create_user(self, username, password): 47 | """Create and store an initial user""" 48 | _config_parse = self._read_file() 49 | 50 | hashed = self.hash_password(password, _config_parse.get('main', 'salt')) 51 | 52 | _config_parse.set('main', 'username', username) 53 | _config_parse.set('main', 'password', hashed) 54 | 55 | self.username = username 56 | self.password = hashed 57 | 58 | with open(self.config_file, 'wb') as configfile: 59 | _config_parse.write(configfile) 60 | 61 | def hash_password(self, password, salt=None): 62 | """Return a hash of the given password/string""" 63 | 64 | if salt == None: 65 | salt = self.salt 66 | 67 | # Use PKCS#5 and sha256 to hash the password 68 | dk = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 100000) 69 | # Return the ascii hex value of the hash 70 | return binascii.hexlify(dk) -------------------------------------------------------------------------------- /pfweb/constants.py: -------------------------------------------------------------------------------- 1 | from pf.constants import * 2 | import socket 3 | 4 | """pfweb constants used in the templates""" 5 | 6 | # Port ops dict 7 | # List: 8 | # 0: Sort options in HTML select 9 | # 1: Description 10 | # 2: str.format syntax 11 | PFWEB_PORT_OPS = { 12 | PF_OP_NONE: [0, "Any", "*"], 13 | PF_OP_EQ: [1, "Equal =", "{0[0]}"], 14 | PF_OP_RRG: [2, "Inclusive Range :", "{0[0]}:{0[1]}"], 15 | PF_OP_IRG: [3, "Range ><", "{0[0]} >< {0[1]}"], 16 | PF_OP_XRG: [4, "Inverse Range <>", "{0[0]} <> {0[1]}"], 17 | PF_OP_NE: [5, "Not Equal !=", "{0[0]}"], 18 | PF_OP_LT: [6, "Less Than <", "{0[0]}"], 19 | PF_OP_LE: [7, "Less Than or equal <=", "{0[0]}"], 20 | PF_OP_GT: [8, "Greater Than >", "{0[0]}"], 21 | PF_OP_GE: [9, "Greater than or equal >=", "{0[0]}"] 22 | } 23 | 24 | # IPv4 ICMP Types and descriptions 25 | PFWEB_ICMP_TYPES = { 26 | ICMP_ECHO: "Echo", 27 | ICMP_ECHOREPLY: "Echo Reply", 28 | ICMP_UNREACH: "Destination Unreachable", 29 | ICMP_SOURCEQUENCH: "Source Quench", 30 | ICMP_REDIRECT: "Redirect", 31 | ICMP_ALTHOSTADDR: "Alternate Host Address", 32 | ICMP_ROUTERADVERT: "Router Advertisement", 33 | ICMP_ROUTERSOLICIT: "Router Solicitation", 34 | ICMP_TIMXCEED: "Time Exceeded", 35 | ICMP_PARAMPROB: "Parameter Problem", 36 | ICMP_TSTAMP: "Timestamp", 37 | ICMP_TSTAMPREPLY: "Timestamp Reply", 38 | ICMP_IREQ: "Information Request", 39 | ICMP_IREQREPLY: "Information Reply", 40 | ICMP_MASKREQ: "Address Mask Request", 41 | ICMP_MASKREPLY: "Address Mask Reply", 42 | ICMP_TRACEROUTE: "Traceroute", 43 | ICMP_DATACONVERR: "Datagram Conversion Error", 44 | ICMP_MOBILE_REDIRECT: "Mobile Host Redirect ", 45 | ICMP_IPV6_WHEREAREYOU: "IPv6 Where-Are-You", 46 | ICMP_IPV6_IAMHERE: "IPv6 I-Am-Here", 47 | ICMP_MOBILE_REGREQUEST: "Mobile Registration Request", 48 | ICMP_MOBILE_REGREPLY: "Mobile Registration Reply", 49 | ICMP_SKIP: "SKIP", 50 | ICMP_PHOTURIS: "Photuris" 51 | } 52 | 53 | # IPv6 ICMP Types and descriptions 54 | PFWEB_ICMP6_TYPES = { 55 | ICMP6_DST_UNREACH: "Destination Unreachable", 56 | ICMP6_PACKET_TOO_BIG: "Packet Too Big", 57 | ICMP6_TIME_EXCEEDED: "Time Exceeded", 58 | ICMP6_PARAM_PROB: "Parameter Problem", 59 | ICMP6_ECHO_REQUEST: "Echo Request", 60 | ICMP6_ECHO_REPLY: "Echo Reply", 61 | ICMP6_MEMBERSHIP_QUERY: "Multicast Listener Query", 62 | ICMP6_MEMBERSHIP_REPORT: "Multicast Listener Report", 63 | ICMP6_MEMBERSHIP_REDUCTION: "Multicast Listener Done", 64 | ND_ROUTER_SOLICIT: "Router Solicitation", 65 | ND_ROUTER_ADVERT: "Router Advertisement", 66 | ND_NEIGHBOR_SOLICIT: "Neighbor Solicitation", 67 | ND_NEIGHBOR_ADVERT: "Router Advertisement", 68 | ND_REDIRECT: "Redirect Message", 69 | ICMP6_ROUTER_RENUMBERING: "Router Renumbering", 70 | ICMP6_WRUREQUEST: "Who are you request", 71 | ICMP6_WRUREPLY: "Who are you reply", 72 | ICMP6_FQDN_QUERY: "FQDN Query", 73 | ICMP6_FQDN_REPLY: "FQDN Reply", 74 | ICMP6_NI_QUERY: "ICMP Node Information Query", 75 | ICMP6_NI_REPLY: "ICMP Node Information Response", 76 | MLD_MTRACE_RESP: "mtrace Response", 77 | MLD_MTRACE: "mtrace Messages", 78 | } 79 | 80 | # Protocol Types Descriptions 81 | PFWEB_IPPROTO = { 82 | socket.IPPROTO_IP: "IP", 83 | socket.IPPROTO_ICMP: "ICMP", 84 | socket.IPPROTO_IGMP: "IGMP", 85 | socket.IPPROTO_IPIP: "IPIP", 86 | socket.IPPROTO_TCP: "TCP", 87 | socket.IPPROTO_EGP: "EGP", 88 | socket.IPPROTO_PUP: "PUP", 89 | socket.IPPROTO_UDP: "UDP", 90 | socket.IPPROTO_IDP: "IDP", 91 | socket.IPPROTO_TP: "TP", 92 | socket.IPPROTO_IPV6: "IPv6", 93 | socket.IPPROTO_ROUTING: "Routing", 94 | socket.IPPROTO_FRAGMENT: "Fragment", 95 | socket.IPPROTO_RSVP: "RSVP", 96 | socket.IPPROTO_GRE: "GRE", 97 | socket.IPPROTO_ESP: "ESP", 98 | socket.IPPROTO_AH: "AH", 99 | socket.IPPROTO_ICMPV6: "ICMPv6", 100 | socket.IPPROTO_NONE: "None", 101 | socket.IPPROTO_DSTOPTS: "DSTOPTS", 102 | socket.IPPROTO_PIM: "PIM", 103 | socket.IPPROTO_RAW: "RAW" 104 | } 105 | 106 | PFWEB_AF = { 107 | socket.AF_UNSPEC: "UNSPEC", 108 | socket.AF_UNIX: "UNIX", 109 | socket.AF_INET: "INET", 110 | socket.AF_SNA: "SNA", 111 | socket.AF_DECnet: "DECnet", 112 | socket.AF_APPLETALK: "APPLETALK", 113 | socket.AF_ROUTE: "ROUTE", 114 | socket.AF_IPX: "IPX", 115 | socket.AF_INET6: "INET6", 116 | socket.AF_KEY: "KEY" 117 | } 118 | 119 | PFWEB_TCP_STATES = { 120 | TCPS_CLOSED: "CLOSED", 121 | TCPS_LISTEN: "LISTEN", 122 | TCPS_SYN_SENT: "SYN_SENT", 123 | TCPS_SYN_RECEIVED: "SYN_RECEIVED", 124 | TCPS_ESTABLISHED: "ESTABLISHED", 125 | TCPS_CLOSE_WAIT: "CLOSE_WAIT", 126 | TCPS_FIN_WAIT_1: "FIN_WAIT_1", 127 | TCPS_CLOSING: "CLOSING", 128 | TCPS_LAST_ACK: "LAST_ACK", 129 | TCPS_FIN_WAIT_2: "FIN_WAIT_2", 130 | TCPS_TIME_WAIT: "TIME_WAIT", 131 | TCP_NSTATES: "NSTATES" 132 | } 133 | 134 | PFWEB_UDP_STATES = { 135 | PFUDPS_NO_TRAFFIC: "NO_TRAFFIC", 136 | PFUDPS_SINGLE: "SINGLE", 137 | PFUDPS_MULTIPLE: "MULTIPLE", 138 | PFUDPS_NSTATES: "NSTATES" 139 | } 140 | 141 | PFWEB_OTHER_STATES = { 142 | PFOTHERS_NO_TRAFFIC: "NO_TRAFFIC", 143 | PFOTHERS_SINGLE: "SINGLE", 144 | PFOTHERS_MULTIPLE: "MULTIPLE", 145 | PFOTHERS_NSTATES: "NSTATES", 146 | } 147 | 148 | # Messages for the user 149 | PFWEB_ALERT_SUCCESS_DEL = 1 150 | PFWEB_ALERT_SUCCESS_ORDER = 2 151 | PFWEB_ALERT_SUCCESS_EDIT = 3 152 | PFWEB_ALERT_SUCCESS_ADD = 4 153 | -------------------------------------------------------------------------------- /pfweb/static/css/datatables.min.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This combined file was created by the DataTables downloader builder: 3 | * https://datatables.net/download 4 | * 5 | * To rebuild or modify this file with the latest versions of the included 6 | * software please visit: 7 | * https://datatables.net/download/#bs/dt-1.10.12 8 | * 9 | * Included libraries: 10 | * DataTables 1.10.12 11 | */ 12 | 13 | table.dataTable{clear:both;margin-top:6px !important;margin-bottom:6px !important;max-width:none !important;border-collapse:separate !important}table.dataTable td,table.dataTable th{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}table.dataTable td.dataTables_empty,table.dataTable th.dataTables_empty{text-align:center}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}div.dataTables_wrapper div.dataTables_length label{font-weight:normal;text-align:left;white-space:nowrap}div.dataTables_wrapper div.dataTables_length select{width:75px;display:inline-block}div.dataTables_wrapper div.dataTables_filter{text-align:right}div.dataTables_wrapper div.dataTables_filter label{font-weight:normal;white-space:nowrap;text-align:left}div.dataTables_wrapper div.dataTables_filter input{margin-left:0.5em;display:inline-block;width:auto}div.dataTables_wrapper div.dataTables_info{padding-top:8px;white-space:nowrap}div.dataTables_wrapper div.dataTables_paginate{margin:0;white-space:nowrap;text-align:right}div.dataTables_wrapper div.dataTables_paginate ul.pagination{margin:2px 0;white-space:nowrap}div.dataTables_wrapper div.dataTables_processing{position:absolute;top:50%;left:50%;width:200px;margin-left:-100px;margin-top:-26px;text-align:center;padding:1em 0}table.dataTable thead>tr>th.sorting_asc,table.dataTable thead>tr>th.sorting_desc,table.dataTable thead>tr>th.sorting,table.dataTable thead>tr>td.sorting_asc,table.dataTable thead>tr>td.sorting_desc,table.dataTable thead>tr>td.sorting{padding-right:30px}table.dataTable thead>tr>th:active,table.dataTable thead>tr>td:active{outline:none}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{cursor:pointer;position:relative}table.dataTable thead .sorting:after,table.dataTable thead .sorting_asc:after,table.dataTable thead .sorting_desc:after,table.dataTable thead .sorting_asc_disabled:after,table.dataTable thead .sorting_desc_disabled:after{position:absolute;bottom:8px;right:8px;display:block;font-family:'Glyphicons Halflings';opacity:0.5}table.dataTable thead .sorting:after{opacity:0.2;content:"\e150"}table.dataTable thead .sorting_asc:after{content:"\e155"}table.dataTable thead .sorting_desc:after{content:"\e156"}table.dataTable thead .sorting_asc_disabled:after,table.dataTable thead .sorting_desc_disabled:after{color:#eee}div.dataTables_scrollHead table.dataTable{margin-bottom:0 !important}div.dataTables_scrollBody table{border-top:none;margin-top:0 !important;margin-bottom:0 !important}div.dataTables_scrollBody table thead .sorting:after,div.dataTables_scrollBody table thead .sorting_asc:after,div.dataTables_scrollBody table thead .sorting_desc:after{display:none}div.dataTables_scrollBody table tbody tr:first-child th,div.dataTables_scrollBody table tbody tr:first-child td{border-top:none}div.dataTables_scrollFoot table{margin-top:0 !important;border-top:none}@media screen and (max-width: 767px){div.dataTables_wrapper div.dataTables_length,div.dataTables_wrapper div.dataTables_filter,div.dataTables_wrapper div.dataTables_info,div.dataTables_wrapper div.dataTables_paginate{text-align:center}}table.dataTable.table-condensed>thead>tr>th{padding-right:20px}table.dataTable.table-condensed .sorting:after,table.dataTable.table-condensed .sorting_asc:after,table.dataTable.table-condensed .sorting_desc:after{top:6px;right:6px}table.table-bordered.dataTable th,table.table-bordered.dataTable td{border-left-width:0}table.table-bordered.dataTable th:last-child,table.table-bordered.dataTable th:last-child,table.table-bordered.dataTable td:last-child,table.table-bordered.dataTable td:last-child{border-right-width:0}table.table-bordered.dataTable tbody th,table.table-bordered.dataTable tbody td{border-bottom-width:0}div.dataTables_scrollHead table.table-bordered{border-bottom-width:0}div.table-responsive>div.dataTables_wrapper>div.row{margin:0}div.table-responsive>div.dataTables_wrapper>div.row>div[class^="col-"]:first-child{padding-left:0}div.table-responsive>div.dataTables_wrapper>div.row>div[class^="col-"]:last-child{padding-right:0} 14 | 15 | 16 | -------------------------------------------------------------------------------- /pfweb/static/css/jquery-ui.min.css: -------------------------------------------------------------------------------- 1 | /*! jQuery UI - v1.12.1 - 2016-11-23 2 | * http://jqueryui.com 3 | * Includes: theme.css 4 | * To view and modify this theme, visit http://jqueryui.com/themeroller/?scope=&folderName=base&cornerRadiusShadow=8px&offsetLeftShadow=0px&offsetTopShadow=0px&thicknessShadow=5px&opacityShadow=30&bgImgOpacityShadow=0&bgTextureShadow=flat&bgColorShadow=666666&opacityOverlay=30&bgImgOpacityOverlay=0&bgTextureOverlay=flat&bgColorOverlay=aaaaaa&iconColorError=cc0000&fcError=5f3f3f&borderColorError=f1a899&bgTextureError=flat&bgColorError=fddfdf&iconColorHighlight=777620&fcHighlight=777620&borderColorHighlight=dad55e&bgTextureHighlight=flat&bgColorHighlight=fffa90&iconColorActive=ffffff&fcActive=ffffff&borderColorActive=003eff&bgTextureActive=flat&bgColorActive=007fff&iconColorHover=555555&fcHover=2b2b2b&borderColorHover=cccccc&bgTextureHover=flat&bgColorHover=ededed&iconColorDefault=777777&fcDefault=454545&borderColorDefault=c5c5c5&bgTextureDefault=flat&bgColorDefault=f6f6f6&iconColorContent=444444&fcContent=333333&borderColorContent=dddddd&bgTextureContent=flat&bgColorContent=ffffff&iconColorHeader=444444&fcHeader=333333&borderColorHeader=dddddd&bgTextureHeader=flat&bgColorHeader=e9e9e9&cornerRadius=3px&fwDefault=normal&fsDefault=1em&ffDefault=Arial%2CHelvetica%2Csans-serif 5 | * Copyright jQuery Foundation and other contributors; Licensed MIT */ 6 | 7 | .ui-widget{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget.ui-widget-content{border:1px solid #c5c5c5}.ui-widget-content{border:1px solid #ddd;background:#fff;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #ddd;background:#e9e9e9;color:#333;font-weight:bold}.ui-widget-header a{color:#333}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default,.ui-button,html .ui-button.ui-state-disabled:hover,html .ui-button.ui-state-disabled:active{border:1px solid #c5c5c5;background:#f6f6f6;font-weight:normal;color:#454545}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited,a.ui-button,a:link.ui-button,a:visited.ui-button,.ui-button{color:#454545;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus,.ui-button:hover,.ui-button:focus{border:1px solid #ccc;background:#ededed;font-weight:normal;color:#2b2b2b}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited,a.ui-button:hover,a.ui-button:focus{color:#2b2b2b;text-decoration:none}.ui-visual-focus{box-shadow:0 0 3px 1px rgb(94,158,214)}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active,a.ui-button:active,.ui-button:active,.ui-button.ui-state-active:hover{border:1px solid #003eff;background:#007fff;font-weight:normal;color:#fff}.ui-icon-background,.ui-state-active .ui-icon-background{border:#003eff;background-color:#fff}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#fff;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #dad55e;background:#fffa90;color:#777620}.ui-state-checked{border:1px solid #dad55e;background:#fffa90}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#777620}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #f1a899;background:#fddfdf;color:#5f3f3f}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#5f3f3f}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#5f3f3f}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon,.ui-button:hover .ui-icon,.ui-button:focus .ui-icon{background-image:url("images/ui-icons_555555_256x240.png")}.ui-state-active .ui-icon,.ui-button:active .ui-icon{background-image:url("images/ui-icons_ffffff_256x240.png")}.ui-state-highlight .ui-icon,.ui-button .ui-state-highlight.ui-icon{background-image:url("images/ui-icons_777620_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_cc0000_256x240.png")}.ui-button .ui-icon{background-image:url("images/ui-icons_777777_256x240.png")}.ui-icon-blank{background-position:16px 16px}.ui-icon-caret-1-n{background-position:0 0}.ui-icon-caret-1-ne{background-position:-16px 0}.ui-icon-caret-1-e{background-position:-32px 0}.ui-icon-caret-1-se{background-position:-48px 0}.ui-icon-caret-1-s{background-position:-65px 0}.ui-icon-caret-1-sw{background-position:-80px 0}.ui-icon-caret-1-w{background-position:-96px 0}.ui-icon-caret-1-nw{background-position:-112px 0}.ui-icon-caret-2-n-s{background-position:-128px 0}.ui-icon-caret-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-65px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-65px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:1px -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:3px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:3px}.ui-widget-overlay{background:#aaa;opacity:.3;filter:Alpha(Opacity=30)}.ui-widget-shadow{-webkit-box-shadow:0 0 5px #666;box-shadow:0 0 5px #666} -------------------------------------------------------------------------------- /pfweb/static/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #F5F5F5; 3 | font-family: 'Roboto', sans-serif; 4 | } 5 | 6 | .logout { 7 | color: #BDBDBD; 8 | padding-left: 10px; 9 | } 10 | 11 | .logout-glyph:hover span { 12 | color: #FFFFFF; 13 | } 14 | 15 | .panel-default>.panel-heading { 16 | color: #fff; 17 | background-color: #424242; 18 | } 19 | 20 | .rule-pass, .iface-up { 21 | color: #4CAF50; 22 | } 23 | 24 | .rule-block, .rule-reject, .iface-down { 25 | color: #B71C1C; 26 | } 27 | 28 | .rule-match { 29 | color: #337ab7; 30 | } 31 | 32 | .glyph-link { 33 | cursor: pointer; 34 | padding-left: 2px; 35 | padding-right: 2px; 36 | color: #337ab7; 37 | } 38 | 39 | .text-danger { 40 | color: #a94442; 41 | } 42 | 43 | .form-control { 44 | height: 24px; 45 | padding: 0 6px; 46 | } 47 | 48 | .col-sm-10 .form-control { 49 | width: calc(50% - 15px); 50 | } 51 | 52 | .col-sm-10 { 53 | padding-top: 7px; 54 | } 55 | 56 | .action-buttons { 57 | text-align: right; 58 | margin-top: 10px; 59 | margin-bottom: 20px; 60 | } 61 | 62 | .help-block { 63 | margin-bottom: 2px; 64 | } 65 | 66 | .form-group { 67 | margin-bottom: 0; 68 | padding: 7px 5px 7px 5px; 69 | border-bottom: 1px solid #E0E0E0; 70 | } 71 | 72 | .no-border { 73 | border-bottom: 0px; 74 | } 75 | 76 | .panel-body { 77 | padding: 0px 15px 0px 15px; 78 | } 79 | 80 | .table-addresses { 81 | padding-bottom: 10px; 82 | } 83 | 84 | .table>tbody>tr.selected { 85 | background-color: #BDBDBD; 86 | } 87 | 88 | .cell-right { 89 | text-align: right; 90 | padding-right: 20px !important; 91 | } 92 | 93 | .modal-header-danger { 94 | color: #fff; 95 | padding:9px 15px; 96 | border-bottom:1px solid #eee; 97 | background-color: #d9534f; 98 | -webkit-border-top-left-radius: 5px; 99 | -webkit-border-top-right-radius: 5px; 100 | -moz-border-radius-topleft: 5px; 101 | -moz-border-radius-topright: 5px; 102 | border-top-left-radius: 5px; 103 | border-top-right-radius: 5px; 104 | } 105 | 106 | .panel-heading a { 107 | cursor: pointer; 108 | } 109 | 110 | .panel-heading a:after { 111 | font-family:'Glyphicons Halflings'; 112 | content:"\e114"; 113 | float: right; 114 | color: grey; 115 | } 116 | 117 | .panel-heading a.collapsed:after { 118 | content:"\e080"; 119 | } 120 | 121 | .cursor-help { 122 | cursor: help; 123 | } 124 | 125 | a:hover.no-underline { 126 | text-decoration: none; 127 | } 128 | 129 | .monospace { 130 | font-family: "Lucida Console", Monaco, monospace; 131 | font-size: 12px; 132 | } 133 | -------------------------------------------------------------------------------- /pfweb/static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nahun/pfweb/d32fd768ec6f5c3296af33f4101f3438b8eaebb3/pfweb/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /pfweb/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nahun/pfweb/d32fd768ec6f5c3296af33f4101f3438b8eaebb3/pfweb/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /pfweb/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nahun/pfweb/d32fd768ec6f5c3296af33f4101f3438b8eaebb3/pfweb/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /pfweb/static/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nahun/pfweb/d32fd768ec6f5c3296af33f4101f3438b8eaebb3/pfweb/static/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /pfweb/static/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.7 (http://getbootstrap.com) 3 | * Copyright 2011-2016 Twitter, Inc. 4 | * Licensed under the MIT 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||b[0]>3)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher, but lower than version 4")}(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){if(a(b.target).is(this))return b.handleObj.handler.apply(this,arguments)}})})}(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.7",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?[]: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.7",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).prop(c,!0)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c).prop(c,!1))},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")&&(a=!1),b.find(".active").removeClass("active"),this.$element.addClass("active")):"checkbox"==c.prop("type")&&(c.prop("checked")!==this.$element.hasClass("active")&&(a=!1),this.$element.toggleClass("active")),c.prop("checked",this.$element.hasClass("active")),a&&c.trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active")),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).closest(".btn");b.call(d,"toggle"),a(c.target).is('input[type="radio"], input[type="checkbox"]')||(c.preventDefault(),d.is("input,button")?d.trigger("focus"):d.find("input:visible,button:visible").first().trigger("focus"))}).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=null,this.sliding=null,this.interval=null,this.$active=null,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.7",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"));if(!(a>this.$items.length-1||a<0))return 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(){if(!this.sliding)return this.slide("next")},c.prototype.prev=function(){if(!this.sliding)return 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|hide/.test(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('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][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.7",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},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":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(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 c(c){c&&3===c.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=b(d),f={relatedTarget:this};e.hasClass("open")&&(c&&"click"==c.type&&/input|textarea/i.test(c.target.tagName)&&a.contains(e[0],c.target)||(e.trigger(c=a.Event("hide.bs.dropdown",f)),c.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger(a.Event("hidden.bs.dropdown",f)))))}))}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.7",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=b(e),g=f.hasClass("open");if(c(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(a(this)).on("click",c);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),f.toggleClass("open").trigger(a.Event("shown.bs.dropdown",h))}return!1}},g.prototype.keydown=function(c){if(/(38|40|27|32)/.test(c.which)&&!/input|textarea/i.test(c.target.tagName)){var d=a(this);if(c.preventDefault(),c.stopPropagation(),!d.is(".disabled, :disabled")){var e=b(d),g=e.hasClass("open");if(!g&&27!=c.which||g&&27==c.which)return 27==c.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.disabled):visible a",i=e.find(".dropdown-menu"+h);if(i.length){var j=i.index(c.target);38==c.which&&j>0&&j--,40==c.which&&jdocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:""})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},c.prototype.checkScrollbar=function(){var a=window.innerWidth;if(!a){var b=document.documentElement.getBoundingClientRect();a=b.right-Math.abs(b.left)}this.bodyIsOverflowing=document.body.clientWidth
',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusin"==b.type?"focus":"hover"]=!0),c.tip().hasClass("in")||"in"==c.hoverState?void(c.hoverState="in"):(clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);if(c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusout"==b.type?"focus":"hover"]=!1),!c.isInStateTrue())return clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h="bottom"==h&&k.bottom+m>o.bottom?"top":"top"==h&&k.top-mo.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null,a.$element=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;!e&&/destroy|hide/.test(b)||(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.7",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.7",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.7",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return e=a-d&&"bottom"},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); -------------------------------------------------------------------------------- /pfweb/static/js/jquery-ui.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery UI - v1.12.1 - 2016-11-23 2 | * http://jqueryui.com 3 | * Includes: effect.js, effects/effect-highlight.js 4 | * Copyright jQuery Foundation and other contributors; Licensed MIT */ 5 | 6 | (function(t){"function"==typeof define&&define.amd?define(["jquery"],t):t(jQuery)})(function(t){t.ui=t.ui||{},t.ui.version="1.12.1";var e="ui-effects-",i="ui-effects-style",s="ui-effects-animated",n=t;t.effects={effect:{}},function(t,e){function i(t,e,i){var s=u[e.type]||{};return null==t?i||!e.def?null:e.def:(t=s.floor?~~t:parseFloat(t),isNaN(t)?e.def:s.mod?(t+s.mod)%s.mod:0>t?0:t>s.max?s.max:t)}function s(i){var s=h(),n=s._rgba=[];return i=i.toLowerCase(),f(l,function(t,o){var a,r=o.re.exec(i),l=r&&o.parse(r),h=o.space||"rgba";return l?(a=s[h](l),s[c[h].cache]=a[c[h].cache],n=s._rgba=a._rgba,!1):e}),n.length?("0,0,0,0"===n.join()&&t.extend(n,o.transparent),s):o[i]}function n(t,e,i){return i=(i+1)%1,1>6*i?t+6*(e-t)*i:1>2*i?e:2>3*i?t+6*(e-t)*(2/3-i):t}var o,a="backgroundColor borderBottomColor borderLeftColor borderRightColor borderTopColor color columnRuleColor outlineColor textDecorationColor textEmphasisColor",r=/^([\-+])=\s*(\d+\.?\d*)/,l=[{re:/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,parse:function(t){return[t[1],t[2],t[3],t[4]]}},{re:/rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,parse:function(t){return[2.55*t[1],2.55*t[2],2.55*t[3],t[4]]}},{re:/#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})/,parse:function(t){return[parseInt(t[1],16),parseInt(t[2],16),parseInt(t[3],16)]}},{re:/#([a-f0-9])([a-f0-9])([a-f0-9])/,parse:function(t){return[parseInt(t[1]+t[1],16),parseInt(t[2]+t[2],16),parseInt(t[3]+t[3],16)]}},{re:/hsla?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,space:"hsla",parse:function(t){return[t[1],t[2]/100,t[3]/100,t[4]]}}],h=t.Color=function(e,i,s,n){return new t.Color.fn.parse(e,i,s,n)},c={rgba:{props:{red:{idx:0,type:"byte"},green:{idx:1,type:"byte"},blue:{idx:2,type:"byte"}}},hsla:{props:{hue:{idx:0,type:"degrees"},saturation:{idx:1,type:"percent"},lightness:{idx:2,type:"percent"}}}},u={"byte":{floor:!0,max:255},percent:{max:1},degrees:{mod:360,floor:!0}},d=h.support={},p=t("

")[0],f=t.each;p.style.cssText="background-color:rgba(1,1,1,.5)",d.rgba=p.style.backgroundColor.indexOf("rgba")>-1,f(c,function(t,e){e.cache="_"+t,e.props.alpha={idx:3,type:"percent",def:1}}),h.fn=t.extend(h.prototype,{parse:function(n,a,r,l){if(n===e)return this._rgba=[null,null,null,null],this;(n.jquery||n.nodeType)&&(n=t(n).css(a),a=e);var u=this,d=t.type(n),p=this._rgba=[];return a!==e&&(n=[n,a,r,l],d="array"),"string"===d?this.parse(s(n)||o._default):"array"===d?(f(c.rgba.props,function(t,e){p[e.idx]=i(n[e.idx],e)}),this):"object"===d?(n instanceof h?f(c,function(t,e){n[e.cache]&&(u[e.cache]=n[e.cache].slice())}):f(c,function(e,s){var o=s.cache;f(s.props,function(t,e){if(!u[o]&&s.to){if("alpha"===t||null==n[t])return;u[o]=s.to(u._rgba)}u[o][e.idx]=i(n[t],e,!0)}),u[o]&&0>t.inArray(null,u[o].slice(0,3))&&(u[o][3]=1,s.from&&(u._rgba=s.from(u[o])))}),this):e},is:function(t){var i=h(t),s=!0,n=this;return f(c,function(t,o){var a,r=i[o.cache];return r&&(a=n[o.cache]||o.to&&o.to(n._rgba)||[],f(o.props,function(t,i){return null!=r[i.idx]?s=r[i.idx]===a[i.idx]:e})),s}),s},_space:function(){var t=[],e=this;return f(c,function(i,s){e[s.cache]&&t.push(i)}),t.pop()},transition:function(t,e){var s=h(t),n=s._space(),o=c[n],a=0===this.alpha()?h("transparent"):this,r=a[o.cache]||o.to(a._rgba),l=r.slice();return s=s[o.cache],f(o.props,function(t,n){var o=n.idx,a=r[o],h=s[o],c=u[n.type]||{};null!==h&&(null===a?l[o]=h:(c.mod&&(h-a>c.mod/2?a+=c.mod:a-h>c.mod/2&&(a-=c.mod)),l[o]=i((h-a)*e+a,n)))}),this[n](l)},blend:function(e){if(1===this._rgba[3])return this;var i=this._rgba.slice(),s=i.pop(),n=h(e)._rgba;return h(t.map(i,function(t,e){return(1-s)*n[e]+s*t}))},toRgbaString:function(){var e="rgba(",i=t.map(this._rgba,function(t,e){return null==t?e>2?1:0:t});return 1===i[3]&&(i.pop(),e="rgb("),e+i.join()+")"},toHslaString:function(){var e="hsla(",i=t.map(this.hsla(),function(t,e){return null==t&&(t=e>2?1:0),e&&3>e&&(t=Math.round(100*t)+"%"),t});return 1===i[3]&&(i.pop(),e="hsl("),e+i.join()+")"},toHexString:function(e){var i=this._rgba.slice(),s=i.pop();return e&&i.push(~~(255*s)),"#"+t.map(i,function(t){return t=(t||0).toString(16),1===t.length?"0"+t:t}).join("")},toString:function(){return 0===this._rgba[3]?"transparent":this.toRgbaString()}}),h.fn.parse.prototype=h.fn,c.hsla.to=function(t){if(null==t[0]||null==t[1]||null==t[2])return[null,null,null,t[3]];var e,i,s=t[0]/255,n=t[1]/255,o=t[2]/255,a=t[3],r=Math.max(s,n,o),l=Math.min(s,n,o),h=r-l,c=r+l,u=.5*c;return e=l===r?0:s===r?60*(n-o)/h+360:n===r?60*(o-s)/h+120:60*(s-n)/h+240,i=0===h?0:.5>=u?h/c:h/(2-c),[Math.round(e)%360,i,u,null==a?1:a]},c.hsla.from=function(t){if(null==t[0]||null==t[1]||null==t[2])return[null,null,null,t[3]];var e=t[0]/360,i=t[1],s=t[2],o=t[3],a=.5>=s?s*(1+i):s+i-s*i,r=2*s-a;return[Math.round(255*n(r,a,e+1/3)),Math.round(255*n(r,a,e)),Math.round(255*n(r,a,e-1/3)),o]},f(c,function(s,n){var o=n.props,a=n.cache,l=n.to,c=n.from;h.fn[s]=function(s){if(l&&!this[a]&&(this[a]=l(this._rgba)),s===e)return this[a].slice();var n,r=t.type(s),u="array"===r||"object"===r?s:arguments,d=this[a].slice();return f(o,function(t,e){var s=u["object"===r?t:e.idx];null==s&&(s=d[e.idx]),d[e.idx]=i(s,e)}),c?(n=h(c(d)),n[a]=d,n):h(d)},f(o,function(e,i){h.fn[e]||(h.fn[e]=function(n){var o,a=t.type(n),l="alpha"===e?this._hsla?"hsla":"rgba":s,h=this[l](),c=h[i.idx];return"undefined"===a?c:("function"===a&&(n=n.call(this,c),a=t.type(n)),null==n&&i.empty?this:("string"===a&&(o=r.exec(n),o&&(n=c+parseFloat(o[2])*("+"===o[1]?1:-1))),h[i.idx]=n,this[l](h)))})})}),h.hook=function(e){var i=e.split(" ");f(i,function(e,i){t.cssHooks[i]={set:function(e,n){var o,a,r="";if("transparent"!==n&&("string"!==t.type(n)||(o=s(n)))){if(n=h(o||n),!d.rgba&&1!==n._rgba[3]){for(a="backgroundColor"===i?e.parentNode:e;(""===r||"transparent"===r)&&a&&a.style;)try{r=t.css(a,"backgroundColor"),a=a.parentNode}catch(l){}n=n.blend(r&&"transparent"!==r?r:"_default")}n=n.toRgbaString()}try{e.style[i]=n}catch(l){}}},t.fx.step[i]=function(e){e.colorInit||(e.start=h(e.elem,i),e.end=h(e.end),e.colorInit=!0),t.cssHooks[i].set(e.elem,e.start.transition(e.end,e.pos))}})},h.hook(a),t.cssHooks.borderColor={expand:function(t){var e={};return f(["Top","Right","Bottom","Left"],function(i,s){e["border"+s+"Color"]=t}),e}},o=t.Color.names={aqua:"#00ffff",black:"#000000",blue:"#0000ff",fuchsia:"#ff00ff",gray:"#808080",green:"#008000",lime:"#00ff00",maroon:"#800000",navy:"#000080",olive:"#808000",purple:"#800080",red:"#ff0000",silver:"#c0c0c0",teal:"#008080",white:"#ffffff",yellow:"#ffff00",transparent:[null,null,null,0],_default:"#ffffff"}}(n),function(){function e(e){var i,s,n=e.ownerDocument.defaultView?e.ownerDocument.defaultView.getComputedStyle(e,null):e.currentStyle,o={};if(n&&n.length&&n[0]&&n[n[0]])for(s=n.length;s--;)i=n[s],"string"==typeof n[i]&&(o[t.camelCase(i)]=n[i]);else for(i in n)"string"==typeof n[i]&&(o[i]=n[i]);return o}function i(e,i){var s,n,a={};for(s in i)n=i[s],e[s]!==n&&(o[s]||(t.fx.step[s]||!isNaN(parseFloat(n)))&&(a[s]=n));return a}var s=["add","remove","toggle"],o={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};t.each(["borderLeftStyle","borderRightStyle","borderBottomStyle","borderTopStyle"],function(e,i){t.fx.step[i]=function(t){("none"!==t.end&&!t.setAttr||1===t.pos&&!t.setAttr)&&(n.style(t.elem,i,t.end),t.setAttr=!0)}}),t.fn.addBack||(t.fn.addBack=function(t){return this.add(null==t?this.prevObject:this.prevObject.filter(t))}),t.effects.animateClass=function(n,o,a,r){var l=t.speed(o,a,r);return this.queue(function(){var o,a=t(this),r=a.attr("class")||"",h=l.children?a.find("*").addBack():a;h=h.map(function(){var i=t(this);return{el:i,start:e(this)}}),o=function(){t.each(s,function(t,e){n[e]&&a[e+"Class"](n[e])})},o(),h=h.map(function(){return this.end=e(this.el[0]),this.diff=i(this.start,this.end),this}),a.attr("class",r),h=h.map(function(){var e=this,i=t.Deferred(),s=t.extend({},l,{queue:!1,complete:function(){i.resolve(e)}});return this.el.animate(this.diff,s),i.promise()}),t.when.apply(t,h.get()).done(function(){o(),t.each(arguments,function(){var e=this.el;t.each(this.diff,function(t){e.css(t,"")})}),l.complete.call(a[0])})})},t.fn.extend({addClass:function(e){return function(i,s,n,o){return s?t.effects.animateClass.call(this,{add:i},s,n,o):e.apply(this,arguments)}}(t.fn.addClass),removeClass:function(e){return function(i,s,n,o){return arguments.length>1?t.effects.animateClass.call(this,{remove:i},s,n,o):e.apply(this,arguments)}}(t.fn.removeClass),toggleClass:function(e){return function(i,s,n,o,a){return"boolean"==typeof s||void 0===s?n?t.effects.animateClass.call(this,s?{add:i}:{remove:i},n,o,a):e.apply(this,arguments):t.effects.animateClass.call(this,{toggle:i},s,n,o)}}(t.fn.toggleClass),switchClass:function(e,i,s,n,o){return t.effects.animateClass.call(this,{add:i,remove:e},s,n,o)}})}(),function(){function n(e,i,s,n){return t.isPlainObject(e)&&(i=e,e=e.effect),e={effect:e},null==i&&(i={}),t.isFunction(i)&&(n=i,s=null,i={}),("number"==typeof i||t.fx.speeds[i])&&(n=s,s=i,i={}),t.isFunction(s)&&(n=s,s=null),i&&t.extend(e,i),s=s||i.duration,e.duration=t.fx.off?0:"number"==typeof s?s:s in t.fx.speeds?t.fx.speeds[s]:t.fx.speeds._default,e.complete=n||i.complete,e}function o(e){return!e||"number"==typeof e||t.fx.speeds[e]?!0:"string"!=typeof e||t.effects.effect[e]?t.isFunction(e)?!0:"object"!=typeof e||e.effect?!1:!0:!0}function a(t,e){var i=e.outerWidth(),s=e.outerHeight(),n=/^rect\((-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto)\)$/,o=n.exec(t)||["",0,i,s,0];return{top:parseFloat(o[1])||0,right:"auto"===o[2]?i:parseFloat(o[2]),bottom:"auto"===o[3]?s:parseFloat(o[3]),left:parseFloat(o[4])||0}}t.expr&&t.expr.filters&&t.expr.filters.animated&&(t.expr.filters.animated=function(e){return function(i){return!!t(i).data(s)||e(i)}}(t.expr.filters.animated)),t.uiBackCompat!==!1&&t.extend(t.effects,{save:function(t,i){for(var s=0,n=i.length;n>s;s++)null!==i[s]&&t.data(e+i[s],t[0].style[i[s]])},restore:function(t,i){for(var s,n=0,o=i.length;o>n;n++)null!==i[n]&&(s=t.data(e+i[n]),t.css(i[n],s))},setMode:function(t,e){return"toggle"===e&&(e=t.is(":hidden")?"show":"hide"),e},createWrapper:function(e){if(e.parent().is(".ui-effects-wrapper"))return e.parent();var i={width:e.outerWidth(!0),height:e.outerHeight(!0),"float":e.css("float")},s=t("

").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}),n={width:e.width(),height:e.height()},o=document.activeElement;try{o.id}catch(a){o=document.body}return e.wrap(s),(e[0]===o||t.contains(e[0],o))&&t(o).trigger("focus"),s=e.parent(),"static"===e.css("position")?(s.css({position:"relative"}),e.css({position:"relative"})):(t.extend(i,{position:e.css("position"),zIndex:e.css("z-index")}),t.each(["top","left","bottom","right"],function(t,s){i[s]=e.css(s),isNaN(parseInt(i[s],10))&&(i[s]="auto")}),e.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})),e.css(n),s.css(i).show()},removeWrapper:function(e){var i=document.activeElement;return e.parent().is(".ui-effects-wrapper")&&(e.parent().replaceWith(e),(e[0]===i||t.contains(e[0],i))&&t(i).trigger("focus")),e}}),t.extend(t.effects,{version:"1.12.1",define:function(e,i,s){return s||(s=i,i="effect"),t.effects.effect[e]=s,t.effects.effect[e].mode=i,s},scaledDimensions:function(t,e,i){if(0===e)return{height:0,width:0,outerHeight:0,outerWidth:0};var s="horizontal"!==i?(e||100)/100:1,n="vertical"!==i?(e||100)/100:1;return{height:t.height()*n,width:t.width()*s,outerHeight:t.outerHeight()*n,outerWidth:t.outerWidth()*s}},clipToBox:function(t){return{width:t.clip.right-t.clip.left,height:t.clip.bottom-t.clip.top,left:t.clip.left,top:t.clip.top}},unshift:function(t,e,i){var s=t.queue();e>1&&s.splice.apply(s,[1,0].concat(s.splice(e,i))),t.dequeue()},saveStyle:function(t){t.data(i,t[0].style.cssText)},restoreStyle:function(t){t[0].style.cssText=t.data(i)||"",t.removeData(i)},mode:function(t,e){var i=t.is(":hidden");return"toggle"===e&&(e=i?"show":"hide"),(i?"hide"===e:"show"===e)&&(e="none"),e},getBaseline:function(t,e){var i,s;switch(t[0]){case"top":i=0;break;case"middle":i=.5;break;case"bottom":i=1;break;default:i=t[0]/e.height}switch(t[1]){case"left":s=0;break;case"center":s=.5;break;case"right":s=1;break;default:s=t[1]/e.width}return{x:s,y:i}},createPlaceholder:function(i){var s,n=i.css("position"),o=i.position();return i.css({marginTop:i.css("marginTop"),marginBottom:i.css("marginBottom"),marginLeft:i.css("marginLeft"),marginRight:i.css("marginRight")}).outerWidth(i.outerWidth()).outerHeight(i.outerHeight()),/^(static|relative)/.test(n)&&(n="absolute",s=t("<"+i[0].nodeName+">").insertAfter(i).css({display:/^(inline|ruby)/.test(i.css("display"))?"inline-block":"block",visibility:"hidden",marginTop:i.css("marginTop"),marginBottom:i.css("marginBottom"),marginLeft:i.css("marginLeft"),marginRight:i.css("marginRight"),"float":i.css("float")}).outerWidth(i.outerWidth()).outerHeight(i.outerHeight()).addClass("ui-effects-placeholder"),i.data(e+"placeholder",s)),i.css({position:n,left:o.left,top:o.top}),s},removePlaceholder:function(t){var i=e+"placeholder",s=t.data(i);s&&(s.remove(),t.removeData(i))},cleanUp:function(e){t.effects.restoreStyle(e),t.effects.removePlaceholder(e)},setTransition:function(e,i,s,n){return n=n||{},t.each(i,function(t,i){var o=e.cssUnit(i);o[0]>0&&(n[i]=o[0]*s+o[1])}),n}}),t.fn.extend({effect:function(){function e(e){function n(){l.removeData(s),t.effects.cleanUp(l),"hide"===i.mode&&l.hide(),r()}function r(){t.isFunction(h)&&h.call(l[0]),t.isFunction(e)&&e()}var l=t(this);i.mode=u.shift(),t.uiBackCompat===!1||a?"none"===i.mode?(l[c](),r()):o.call(l[0],i,n):(l.is(":hidden")?"hide"===c:"show"===c)?(l[c](),r()):o.call(l[0],i,r)}var i=n.apply(this,arguments),o=t.effects.effect[i.effect],a=o.mode,r=i.queue,l=r||"fx",h=i.complete,c=i.mode,u=[],d=function(e){var i=t(this),n=t.effects.mode(i,c)||a;i.data(s,!0),u.push(n),a&&("show"===n||n===a&&"hide"===n)&&i.show(),a&&"none"===n||t.effects.saveStyle(i),t.isFunction(e)&&e()};return t.fx.off||!o?c?this[c](i.duration,h):this.each(function(){h&&h.call(this)}):r===!1?this.each(d).each(e):this.queue(l,d).queue(l,e)},show:function(t){return function(e){if(o(e))return t.apply(this,arguments);var i=n.apply(this,arguments);return i.mode="show",this.effect.call(this,i)}}(t.fn.show),hide:function(t){return function(e){if(o(e))return t.apply(this,arguments);var i=n.apply(this,arguments);return i.mode="hide",this.effect.call(this,i)}}(t.fn.hide),toggle:function(t){return function(e){if(o(e)||"boolean"==typeof e)return t.apply(this,arguments);var i=n.apply(this,arguments);return i.mode="toggle",this.effect.call(this,i)}}(t.fn.toggle),cssUnit:function(e){var i=this.css(e),s=[];return t.each(["em","px","%","pt"],function(t,e){i.indexOf(e)>0&&(s=[parseFloat(i),e])}),s},cssClip:function(t){return t?this.css("clip","rect("+t.top+"px "+t.right+"px "+t.bottom+"px "+t.left+"px)"):a(this.css("clip"),this)},transfer:function(e,i){var s=t(this),n=t(e.to),o="fixed"===n.css("position"),a=t("body"),r=o?a.scrollTop():0,l=o?a.scrollLeft():0,h=n.offset(),c={top:h.top-r,left:h.left-l,height:n.innerHeight(),width:n.innerWidth()},u=s.offset(),d=t("
").appendTo("body").addClass(e.className).css({top:u.top-r,left:u.left-l,height:s.innerHeight(),width:s.innerWidth(),position:o?"fixed":"absolute"}).animate(c,e.duration,e.easing,function(){d.remove(),t.isFunction(i)&&i()})}}),t.fx.step.clip=function(e){e.clipInit||(e.start=t(e.elem).cssClip(),"string"==typeof e.end&&(e.end=a(e.end,e.elem)),e.clipInit=!0),t(e.elem).cssClip({top:e.pos*(e.end.top-e.start.top)+e.start.top,right:e.pos*(e.end.right-e.start.right)+e.start.right,bottom:e.pos*(e.end.bottom-e.start.bottom)+e.start.bottom,left:e.pos*(e.end.left-e.start.left)+e.start.left})}}(),function(){var e={};t.each(["Quad","Cubic","Quart","Quint","Expo"],function(t,i){e[i]=function(e){return Math.pow(e,t+2)}}),t.extend(e,{Sine:function(t){return 1-Math.cos(t*Math.PI/2)},Circ:function(t){return 1-Math.sqrt(1-t*t)},Elastic:function(t){return 0===t||1===t?t:-Math.pow(2,8*(t-1))*Math.sin((80*(t-1)-7.5)*Math.PI/15)},Back:function(t){return t*t*(3*t-2)},Bounce:function(t){for(var e,i=4;((e=Math.pow(2,--i))-1)/11>t;);return 1/Math.pow(4,3-i)-7.5625*Math.pow((3*e-2)/22-t,2)}}),t.each(e,function(e,i){t.easing["easeIn"+e]=i,t.easing["easeOut"+e]=function(t){return 1-i(1-t)},t.easing["easeInOut"+e]=function(t){return.5>t?i(2*t)/2:1-i(-2*t+2)/2}})}(),t.effects,t.effects.define("highlight","show",function(e,i){var s=t(this),n={backgroundColor:s.css("backgroundColor")};"hide"===e.mode&&(n.opacity=0),t.effects.saveStyle(s),s.css({backgroundImage:"none",backgroundColor:e.color||"#ffff99"}).animate(n,{queue:!1,duration:e.duration,easing:e.easing,complete:i})})}); -------------------------------------------------------------------------------- /pfweb/static/js/main.js: -------------------------------------------------------------------------------- 1 | function save_rules_order() { 2 | // Store new order in array with old location as value 3 | var order = []; 4 | $('#rulestable > tbody > tr').each(function(index, row) { 5 | order.push(parseInt(row.id.slice(9))); 6 | }); 7 | 8 | // Create a form with a JSON array and submit 9 | form = $('
'); 10 | order_form = $(''); 11 | form.append(order_form).submit(); 12 | } 13 | 14 | function remove_rule(rule) { 15 | resp = confirm("Are you sure you wish to delete this rule?"); 16 | 17 | if(resp == false) { 18 | return 19 | } 20 | 21 | location.href = "/firewall/rules/remove/" + rule 22 | } 23 | 24 | function remove_table(table) { 25 | $("#table_" + table).prop("checked", true); 26 | 27 | $("#delete_tables_submit").trigger("click"); 28 | 29 | $("#table_" + table).prop("checked", false); 30 | } 31 | 32 | function add_address() { 33 | // Get next ID to use 34 | field_id = $('#address_fields').children().last().attr('id').split("-")[1]; 35 | new_id = (parseInt(field_id) + 1); 36 | // Create new dom for addr 37 | field = $('
' + 38 | '' + 39 | '
' + 40 | '' + 41 | '
' + 42 | '
' + 43 | '' + 47 | '
' + 48 | '
'); 49 | // Add to the container 50 | $("#address_fields").append(field); 51 | } 52 | 53 | function table_remove_addr(id) { 54 | // Only clear the field if only one address 55 | if($('#address_fields').children().length == 1) { 56 | $("#addr_container-" + id + " input").val(""); 57 | return 58 | } 59 | 60 | // Add Label to second field if removing the first address 61 | if(id == parseInt($('#address_fields').children().first().attr('id').split("-")[1])) { 62 | $('#address_fields').children().eq(1).children('label').text('Addresses'); 63 | } 64 | // Remove addr div 65 | $("#addr_container-" + id).remove(); 66 | } 67 | 68 | function toggle_fields() { 69 | // Modify protocol options based on address family 70 | if($('#af').val() == '*') { 71 | // Hide ICMP and translation 72 | $('#icmp_option').hide(); 73 | $('#translation_panel').hide(); 74 | $('#trans_type').val('none'); 75 | 76 | if($('#proto').val() == 'ICMP') { 77 | $('#proto').val('*'); 78 | } 79 | 80 | $.each(['src', 'dst'], function(i, val) { 81 | if($('#' + val + '_addr_type').val() == 'addrmask') { 82 | $('#' + val + '_addr_type').val('any'); 83 | addr_type(val, 'any'); 84 | } 85 | $('#form_' + val + '_addrmask_type').prop('disabled', true); 86 | }); 87 | } 88 | else { 89 | // Show Translation 90 | $('#translation_panel').show(); 91 | // Only allow NAT with 'out' direction 92 | if($('#direction').val() == 'out') { 93 | $('#trans_type_nat').prop('disabled', false); 94 | } 95 | else { 96 | if($('#trans_type').val() == 'NAT') { 97 | $('#trans_type').val('none'); 98 | } 99 | $('#trans_type_nat').prop('disabled', true); 100 | } 101 | 102 | // Show ICMP and addrmask for both IPv4 and IPv6 103 | $('#icmp_option').show(); 104 | $('#form_src_addrmask_type').prop('disabled', false); 105 | $('#form_dst_addrmask_type').prop('disabled', false); 106 | } 107 | 108 | // RDR can only be used with TCP or UDP 109 | if($('#proto').val() == "TCP" || $('#proto').val() == "UDP") { 110 | $('#trans_type_rdr').prop('disabled', false); 111 | } 112 | else { 113 | if($('#trans_type').val() == 'RDR') { 114 | $('#trans_type').val('none'); 115 | } 116 | $('#trans_type_rdr').prop('disabled', true); 117 | } 118 | 119 | if($('#proto').val() != "ICMP" || $('#af').val() == '*') { 120 | // Hide ICMP Type form when protocol isn't ICMP or the AF is Any 121 | $('#form_icmptype').hide(); 122 | 123 | // The ports must be hidden when ICMP is chosen even with AF is Any 124 | if($('#proto').val() == "ICMP") { 125 | $('#form_src_port').hide(); 126 | $('#form_dst_port').hide(); 127 | } 128 | else { 129 | // Only show ports when ICMP is not the chosen protocol 130 | $('#form_src_port').show(); 131 | $('#form_dst_port').show(); 132 | } 133 | 134 | return 135 | } 136 | 137 | // Make sure to hide ports when ICMP type is shown 138 | $('#form_src_port').hide(); 139 | $('#form_dst_port').hide(); 140 | 141 | // Show the correct ICMP options in the select for the AF chosen 142 | if($('#af').val() == 'IPv4') { 143 | $('#icmptype').show(); 144 | $('#icmp6type').hide(); 145 | } 146 | else if($('#af').val() == 'IPv6') { 147 | $('#icmp6type').show(); 148 | $('#icmptype').hide(); 149 | } 150 | 151 | // Show the whole ICMP type form 152 | $('#form_icmptype').show(); 153 | } 154 | 155 | function port_op(type, port_op) { 156 | /* Modify port from and to based on port op */ 157 | 158 | if(port_op.indexOf('Range') !== -1) { 159 | $("#" + type + "_port_from").prop('disabled', false); 160 | $("#" + type + "_port_to").prop('disabled', false); 161 | } 162 | else if(port_op == 'Any') { 163 | $("#" + type + "_port_from").prop('disabled', true); 164 | $("#" + type + "_port_to").prop('disabled', true); 165 | } 166 | else { 167 | $("#" + type + "_port_from").prop('disabled', false); 168 | $("#" + type + "_port_to").prop('disabled', true); 169 | } 170 | } 171 | 172 | function addr_type(type, value) { 173 | if(value == 'addrmask') { 174 | $("#" + type + "_addr_table").hide(); 175 | $("#" + type + "_addr_iface").hide(); 176 | $("#form_" + type + "_addrmask").show(); 177 | } 178 | else if(value == 'table') { 179 | $("#form_" + type + "_addrmask").hide(); 180 | $("#" + type + "_addr_iface").hide(); 181 | $("#" + type + "_addr_table").show(); 182 | } 183 | else if(value == 'dynif') { 184 | $("#form_" + type + "_addrmask").hide(); 185 | $("#" + type + "_addr_table").hide(); 186 | $("#" + type + "_addr_iface").show(); 187 | } 188 | if(value == 'any') { 189 | $("#" + type + "_addr_table").hide(); 190 | $("#form_" + type + "_addrmask").hide(); 191 | $("#" + type + "_addr_iface").hide(); 192 | } 193 | } 194 | 195 | function trans_form_type(value) { 196 | /* Show or hide fields when selecting different translation types */ 197 | if(value == 'NAT') { 198 | $("#form_trans_port").hide() 199 | $("#form_trans_addr_type_dynif").prop('disabled', false); 200 | $("#form_trans_staticport").show() 201 | } 202 | else if(value == 'RDR'){ 203 | $("#form_trans_staticport").hide() 204 | $("#form_trans_port").show() 205 | if($('#trans_addr_type').val() == 'dynif') { 206 | $("#trans_addr_type").val('addrmask'); 207 | addr_type('trans', 'addrmask'); 208 | } 209 | $("#form_trans_addr_type_dynif").prop('disabled', true); 210 | } 211 | } 212 | 213 | function load_edit_rules_page() { 214 | /* All actions needed when loading the edit rules page */ 215 | toggle_fields(); 216 | 217 | $.each(['src', 'dst'], function(i, val) { 218 | addr_type(val, $('#' + val + '_addr_type').val()); 219 | port_op(val, $('#' + val + '_port_op option:selected').text()); 220 | }); 221 | 222 | addr_type('trans', $('#trans_addr_type').val()); 223 | trans_form_type($('#trans_type').val()); 224 | } 225 | 226 | function remove_state(item) { 227 | var data = $(item).data('entry').split('|'); 228 | 229 | resp = confirm("Are you sure you wish to delete this state?\n" + data[0] + " -> " + data[1]); 230 | 231 | if(resp == false) { 232 | return 233 | } 234 | 235 | var data = $(item).data('entry').split('|'); 236 | 237 | $.ajax('/status/states', 238 | { 239 | type: 'post', 240 | data: { 241 | action: 'remove', 242 | src: data[0], 243 | dst: data[1] 244 | }, 245 | success: function() { 246 | $(item).parents('tr').remove(); 247 | }, 248 | error: function(e) { 249 | $('#modal_alert .modal-title').text("Bad Request"); 250 | $('#modal_alert .modal-body').text(e.responseJSON.message); 251 | $('#modal_alert').modal() 252 | } 253 | } 254 | ); 255 | } 256 | -------------------------------------------------------------------------------- /pfweb/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block head %} 5 | 6 | 7 | 8 | 9 | {% block title %}pfweb{% endblock %} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | {% endblock %} 21 | 22 | 23 | {% block navbar %} 24 | 25 | 72 | {% endblock %} 73 | {% block content %}{% endblock %} 74 | {% block footer %} 75 | 76 | 77 | 78 | 79 | {% endblock %} 80 | 81 | -------------------------------------------------------------------------------- /pfweb/templates/dash.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |
6 |
7 |
8 |

System Information

9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
Hostname{{ sys_info.hostname }}
Operating System{{ sys_info.os }}
Uptime{{ sys_info.uptime }}
Current Time{{ sys_info.current_time }}
30 |
31 |
32 | 33 |
34 |

PF Information

35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 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 | 75 | 76 |
TotalRate
Status{% if pf_info.enabled %}Enabled{% else %}Disabled{% endif %} for {{ pf_info.since }}
State Table Entries{{ pf_info.states }}
State Table Searches{{ pf_info.searches.total }}{{ pf_info.searches.rate }}/s
State Table Inserts{{ pf_info.inserts.total }}{{ pf_info.inserts.rate }}/s
State Table Removals{{ pf_info.removals.total }}{{ pf_info.removals.rate }}/s
Matches{{ pf_info.match.total }}{{ pf_info.match.rate }}/s
77 |
78 |
79 |
80 | 81 |
82 |
83 |

Interface Information

84 |
85 | 86 | 87 | {% for iface in if_info %} 88 | 89 | 90 | 91 | 92 | 96 | 97 | {% endfor %} 98 | 99 |
{{ iface.name }}{% if iface.status %}{% else %}{% endif %}{{ iface.media }} 93 | {% for ip in iface.ipv4 %}{{ ip }}
{% endfor %} 94 | {% for ip in iface.ipv6|sort(attribute=1) %}{% if ip[1] %}{{ ip[0] }}{% else %}{{ ip[0] }}{% endif%}
{% endfor %} 95 |
100 |
101 |
102 | 103 |
104 |

Interface Statistics

105 |
106 | 107 | {{ if_stats|safe }} 108 |
109 |
110 |
111 |
112 |
113 |
114 | {% endblock %} 115 | 116 | {% block footer %} 117 | {{ super() }} 118 | 119 | 125 | {% endblock %} 126 | -------------------------------------------------------------------------------- /pfweb/templates/edit.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 | 6 |
7 |
8 |

{% if rule.id or rule.id == 0 %}Edit{% else %}Add{% endif %} Firewall Rule

9 | 10 |
11 |
12 | 13 |
14 | 19 | Reject sets block-policy to return. Block drops the packet silently. 20 |
21 |
22 | 23 |
24 | 25 |
26 | 31 |
32 |
33 | 34 |
35 | 36 |
37 | 42 |
43 |
44 | 45 |
46 | 47 |
48 | 53 |
54 |
55 | 56 |
57 | 58 |
59 | 65 |
66 |
67 | 68 |
69 | 70 |
71 | 77 | 83 |
84 |
85 |
86 |
87 |
88 |

Source

89 | 90 |
91 | 92 |
93 | 94 |
95 | 96 | CIDR Format. eg '192.168.1.0/24'. Set to '*' or blank to match any source address 97 |
98 |
99 | 100 |
101 | 102 |
103 | 108 | Port Type 109 |
110 |
111 | 112 | From 113 |
114 |
115 | 116 | To 117 |
118 | 119 |
120 | Specify the source port or port range for this rule. The "To" field may be left empty if only filtering a single port. Both fields may be left empty if "Any" is chosen. 121 |
122 |
123 |
124 |
125 | 126 |
127 |

Destination

128 | 129 |
130 | 131 |
132 | 133 |
134 | 135 | CIDR Format. eg '192.168.1.0/24'. Set to '*' or blank to match any destination address 136 |
137 |
138 | 139 |
140 | 141 |
142 | 147 | Port Type 148 |
149 |
150 | 151 | From 152 |
153 |
154 | 155 | To 156 |
157 | 158 |
159 | Specify the destination port or port range for this rule. The "To" field may be left empty if only filtering a single port. Both fields may be left empty if "Any" is chosen. 160 |
161 |
162 |
163 |
164 | 165 |
166 |

Extra Options

167 | 168 |
169 | 170 |
171 | 172 |
173 | Log packets matched to this rule 174 |
175 |
176 | 177 |
178 | 179 |
180 | Stop evaluating rules when matched 181 |
182 |
183 | 184 |
185 | 186 |
187 | Enable stateful tracking 188 |
189 |
190 | 191 |
192 | 193 |
194 | 195 |
196 |
197 | 198 |
199 |
200 | 201 |
202 | 206 | 207 | Cancel 208 |
209 |
210 |
211 | {% endblock %} 212 | {% block footer %} 213 | {{ super() }} 214 | 215 | 219 | {% endblock %} -------------------------------------------------------------------------------- /pfweb/templates/edit_rule.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |
6 |
7 |
8 |

{% if rule.id or rule.id == 0 %}Edit{% else %}Add{% endif %} Firewall Rule

9 | 10 |
11 |
12 | 13 |
14 | 20 | Reject sets block-policy to return. Block drops the packet silently. 21 |
22 |
23 | 24 |
25 | 26 |
27 | 32 |
33 |
34 | 35 |
36 | 37 |
38 | 43 |
44 |
45 | 46 |
47 | 48 |
49 | 54 |
55 |
56 | 57 |
58 | 59 |
60 | 66 |
67 |
68 | 69 |
70 | 71 |
72 | 78 | 84 |
85 |
86 |
87 |
88 |
89 |

Source

90 | 91 |
92 | 93 |
94 | 95 |
96 | 102 |
103 |
104 |
105 | 106 | CIDR Format. eg '192.168.1.0/24' 107 |
108 | 113 | 118 |
119 |
120 | 121 |
122 | 123 |
124 | 129 | Port Type 130 |
131 |
132 | 133 | From 134 |
135 |
136 | 137 | To 138 |
139 | 140 |
141 | Specify the source port or port range for this rule. The "To" field may be left empty if only filtering a single port. Both fields may be left empty if "Any" is chosen. 142 |
143 |
144 |
145 |
146 | 147 |
148 |

Destination

149 | 150 |
151 | 152 |
153 | 154 |
155 | 161 |
162 |
163 |
164 | 165 | CIDR Format. eg '192.168.1.0/24' 166 |
167 | 172 | 177 |
178 |
179 | 180 |
181 | 182 |
183 | 188 | Port Type 189 |
190 |
191 | 192 | From 193 |
194 |
195 | 196 | To 197 |
198 | 199 |
200 | Specify the destination port or port range for this rule. The "To" field may be left empty if only filtering a single port. Both fields may be left empty if "Any" is chosen. 201 |
202 |
203 |
204 |
205 | 206 |
207 |
208 |

209 | 210 |

211 |
212 | 213 |
214 |
215 | 216 |
217 | 218 |
219 | 224 |
225 | 226 |
227 | 232 |
233 |
234 |
235 | 236 | CIDR Format. eg '192.168.1.0/24' 237 |
238 | 243 | 248 |
249 |
250 | 251 |
252 | 253 |
254 | Prevents modifying the source port on TCP and UDP packets. 255 |
256 |
257 | 258 |
259 | 260 |
261 | 262 | From 263 |
264 |
265 | 266 | To 267 |
268 | 269 |
270 | Specify the destination port or port range for this rule. The "To" field may be left empty if only translating to a single port. 271 |
272 |
273 | 274 |
275 |
276 | 277 |
278 | 279 |
280 |
281 |

282 | Extra Options 283 |

284 |
285 | 286 |
287 |
288 | 289 |
290 | 291 |
292 | Log packets matched to this rule 293 |
294 |
295 | 296 |
297 | 298 |
299 | Stop evaluating rules when matched 300 |
301 |
302 | 303 |
304 | 305 |
306 | Enable stateful tracking 307 |
308 |
309 | 310 |
311 | 312 |
313 | 314 |
315 |
316 | 317 |
318 |
319 | 320 |
321 |
322 | 323 |
324 | 328 | 329 | Cancel 330 |
331 |
332 |
333 | {% endblock %} 334 | {% block footer %} 335 | {{ super() }} 336 | 337 | 341 | {% endblock %} -------------------------------------------------------------------------------- /pfweb/templates/edit_table.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |
6 |
7 |

{% if table.name %}Edit{% else %}Add{% endif %} Table

8 | 9 |
10 |
11 | 12 |
13 | {% if table.name %} 14 | {{ table.name }} 15 | {% else %} 16 | 17 | {% endif %} 18 |
19 |
20 | 21 |
22 | 23 |
Enter each address in CIDR format eg 192.168.1.0/24 or fe80::ec3e:cdb0:6ed2:192f/64
24 |
25 | 26 |
27 | {% for addr in table.addrs %} 28 |
29 | 30 |
31 | 32 |
33 |
34 | 38 |
39 |
40 | {% else %} 41 |
42 | 43 |
44 | 45 |
46 |
47 | 51 |
52 |
53 | {% endfor %} 54 |
55 |
56 |
57 | 58 |
59 | 63 | 64 | Cancel 65 |
66 |
67 | 71 |
72 |
73 |
74 | {% endblock %} -------------------------------------------------------------------------------- /pfweb/templates/error.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Bad Request{% endblock %} 3 | {% block content %} 4 |
5 | 6 |

Your request could not be understood by the server.

7 | {% if msg %}

Error Message: {{ msg }}

{% endif %} 8 |
9 | {% endblock %} -------------------------------------------------------------------------------- /pfweb/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 | {% if message %} 6 | 10 | {% endif %} 11 | {% if not no_login %} 12 |
13 |

Login to pfweb

14 |
15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 |
23 |
24 | 25 |
26 | 27 |
28 |
29 |
30 |
31 |
32 | 33 |
34 |
35 |
36 |
37 |
38 | {% endif %} 39 |
40 | {% endblock %} -------------------------------------------------------------------------------- /pfweb/templates/pfinfo.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |
6 |

PF Information

7 |
8 |
 9 | {{ status.info }}
10 | 
11 | LIMITS:
12 | {% for limit, val in status.limits|dictsort %}{{ "{:<15} hard limit {:>7}".format(limit, val) }}
{% endfor %} 13 | TIMEOUTS: 14 | {% for timeout, val in status.timeouts|dictsort %}{{ "{:<16} {:>5}s".format(timeout, val) }}
{% endfor %} 15 | INTERFACES: 16 | {% for iface in status.ifaces %}{{ iface|string }}{% endfor %} 17 |
18 |
19 |
20 |
21 | 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /pfweb/templates/rules.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 | {% if message %} 6 | 10 | {% endif %} 11 | 12 |
13 |
14 |

Rules

15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | {% for rule in rules %} 34 | 35 | 36 | 47 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 63 | 64 | {% endfor %} 65 | 66 |
InterfaceProtocolSourcePortDestinationPortNAT / RDRDescriptionActions
37 | 38 | 39 | {% if rule.log %} 40 | 41 | {% endif %} 42 | 43 | {% if rule.quick %} 44 | 45 | {% endif %} 46 | 48 | 49 | Packets: {}
Bytes: {}
States: {}
State Creations: {}".format(rule.evaluations, rule.packets, rule.bytes, rule.states, rule.states_creations) }}" data-html="true" class="text-info cursor-help">{{ rule.iface }}
50 |
{{ "{} ".format(rule.af) if rule.af != "*" or rule.proto == '*' }}{{ rule.proto if rule.proto != '*' }}{% if rule.src_addr_type == 'table' %}{{ "<{}>".format(rule.src_addr) }}{% else %}{{ rule.src_addr }}{% endif %}{{ port_ops[rule.src_port_op][2].format(rule.src_port) }}{% if rule.dst_addr_type == 'table' %}{{ "<{}>".format(rule.dst_addr) }}{% else %}{{ rule.dst_addr }}{% endif %}{{ port_ops[rule.dst_port_op][2].format(rule.dst_port) }}{% if rule.trans_type %} {{ "<{}>".format(rule.trans_addr) if rule.trans_addr_type == 'table' else rule.trans_addr }}{% if rule.trans_type == 'RDR' and rule.trans_port[0] %} {{ port_ops[rule.trans_port_op][2].format(rule.trans_port) }}{% endif %}{% endif %}{{ rule.label }} 59 | 60 | 61 | 62 |
67 |
68 |
69 | 81 |
82 |
83 | 84 | {% endblock %} 85 | {% block footer %} 86 | {{ super() }} 87 | 88 | 89 | 90 | 197 | {% endblock %} 198 | -------------------------------------------------------------------------------- /pfweb/templates/states.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |
6 |

States

7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {% for state in states %} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {% endfor %} 32 | 33 |
IFProtoSource (Original SRC) Destination (Original DST)StatePacketsBytesExpires
{{ state.ifname }}{{ state.proto }}{{ state.src }} {{ state.dst }}{{ state.state }}{{ state.packets[1]|safe }}{{ state.bytes[1]|safe }}{{ state.expires[1] }}
34 |
35 |
36 |
37 | 38 | 50 | 51 | {% endblock %} 52 | 53 | {% block footer %} 54 | {{ super() }} 55 | 56 | 57 | 58 | 59 | 68 | {% endblock %} 69 | -------------------------------------------------------------------------------- /pfweb/templates/tables.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 | {% if remove_error|length > 0 %} 6 | 10 | {% endif %} 11 | 12 |
13 |
14 |

Tables

15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {% for table in tables|sort(attribute="name") %} 27 | 28 | 29 | 30 | 34 | 38 | 39 | {% endfor %} 40 | 41 |
NameAddressesActions
{{ table.name }} 31 | {% set comma = joiner(", ") %} 32 | {% for addr in table.addrs %}{{ comma() }}{{ addr }}{% endfor %} 33 | 35 | 36 | 37 |
42 |
43 |
44 | 52 |
53 |
54 | {% endblock %} 55 | {% block footer %} 56 | {{ super() }} 57 | 58 | 69 | {% endblock %} 70 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | pfweb 3 | ----- 4 | 5 | pfweb is a python web application to manage the OpenBSD Packet Filter (PF). It 6 | uses *py-pf* to interface with PF and Flask for the web framework. The look 7 | and feel is based on pfSense and a lot of the ideas are ripped off from them. 8 | 9 | pfweb is designed with few dependencies and strives to use only included Python 10 | modules. 11 | 12 | The source is on `GitHub `_ 13 | """ 14 | from setuptools import setup, find_packages 15 | 16 | requires = [ 17 | 'py-pf>=0.1.7', 18 | 'Flask', 19 | 'flask-login' 20 | ] 21 | 22 | setup(name='pfweb', 23 | version='0.1.0dev4', 24 | description='Simple web interface for the OpenBSD Packet Filter', 25 | long_description=__doc__, 26 | license='BSD', 27 | classifiers=[ 28 | 'Development Status :: 2 - Pre-Alpha', 29 | 'License :: OSI Approved :: BSD License', 30 | 'Natural Language :: English', 31 | 'Intended Audience :: System Administrators', 32 | 'Operating System :: POSIX :: BSD :: OpenBSD', 33 | 'Programming Language :: Python :: 2.7', 34 | 'Framework :: Flask', 35 | 'Topic :: System :: Networking :: Firewalls' 36 | ], 37 | author='Nathan Wheeler', 38 | author_email='nate.wheeler@gmail.com', 39 | url='https://github.com/nahun/pfweb', 40 | packages=find_packages(), 41 | install_requires=requires, 42 | include_package_data=True, 43 | zip_safe=False 44 | ) 45 | --------------------------------------------------------------------------------