├── dev-requirements.in ├── MANIFEST.in ├── experiments ├── test.db └── test_sqlalchemy.py ├── wsgi.py ├── dev-requirements.txt ├── wgadmin ├── templates │ ├── interfaces │ │ ├── edit │ │ │ ├── delete.html │ │ │ ├── info.html │ │ │ ├── addresses.html │ │ │ └── peers.html │ │ ├── base.html │ │ ├── edit.html │ │ ├── list.html │ │ ├── add_peer.html │ │ └── add.html │ ├── auth │ │ ├── login.html │ │ └── register.html │ ├── lib.html │ ├── base.html │ └── export_onetime_conf.html ├── utils.py ├── forms.py ├── static │ ├── style.css │ ├── wireguard.js │ └── qr.js ├── auth.py ├── config.py ├── __init__.py ├── interfaces.py └── models.py ├── requirements.txt ├── .editorconfig ├── setup.py ├── README.md ├── LICENSE ├── tests ├── test_gen_ip.py └── test_models.py └── .gitignore /dev-requirements.in: -------------------------------------------------------------------------------- 1 | -c requirements.txt 2 | Flask-Testing 3 | pip-tools 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft wgadmin/static 2 | graft wgadmin/templates 3 | global-exclude *.pyc 4 | -------------------------------------------------------------------------------- /experiments/test.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artizirk/wgadmin/HEAD/experiments/test.db -------------------------------------------------------------------------------- /wsgi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from wgadmin import create_app 3 | 4 | application = create_app() 5 | 6 | 7 | if __name__ == '__main__': 8 | application.run() 9 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile dev-requirements.in 6 | # 7 | click==7.0 # via flask, pip-tools 8 | flask-testing==0.7.1 9 | flask==1.1.1 # via flask-testing 10 | pip-tools==4.3.0 11 | six==1.13.0 # via pip-tools 12 | -------------------------------------------------------------------------------- /wgadmin/templates/interfaces/edit/delete.html: -------------------------------------------------------------------------------- 1 | {% extends 'interfaces/edit.html' %} 2 | 3 | {% block form %} 4 |

Delete interface {{ iface }}?

5 | 6 |
7 | 8 |
9 | {% endblock form %} 10 | -------------------------------------------------------------------------------- /wgadmin/templates/interfaces/base.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

5 | {% block title %} 6 | Interfaces - {% block subtitle %}{% endblock %} 7 | {% endblock %} 8 |

9 | 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile setup.py 6 | # 7 | click==7.0 # via flask 8 | flask-sqlalchemy==2.4.1 9 | flask-wtf==0.14.2 10 | flask==1.1.1 11 | itsdangerous==1.1.0 # via flask 12 | jinja2==2.10.3 # via flask 13 | markupsafe==1.1.1 # via jinja2 14 | sqlalchemy==1.3.12 # via flask-sqlalchemy 15 | werkzeug==0.16.0 # via flask 16 | wtforms==2.2.1 # via flask-wtf 17 | -------------------------------------------------------------------------------- /wgadmin/templates/auth/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

{% block title %}Log In{% endblock %}

5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 | 10 | 11 | 12 | 13 | 14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /wgadmin/templates/auth/register.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

{% block title %}Register{% endblock %}

5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 | 10 | 11 | 12 | 13 | 14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | # Matches multiple files with brace expansion notation 13 | # Set default charset 14 | [*.{js,py}] 15 | charset = utf-8 16 | 17 | # 4 space indentation 18 | [*.py] 19 | indent_style = space 20 | indent_size = 4 21 | 22 | [*.html] 23 | indent_style = space 24 | indent_size = 2 25 | 26 | -------------------------------------------------------------------------------- /wgadmin/templates/lib.html: -------------------------------------------------------------------------------- 1 | {% macro search_form_macro(form) %} 2 |
3 | {{ form.query.label }} {{ form.query }} 4 | 5 |
6 | {%- endmacro %} 7 | 8 | {% macro render_form_field(field) %} 9 | {% if field.flags.required %}*{% endif %}{{ field.label }} 10 | {{ field(**kwargs)|safe }} 11 | {% if field.errors %} 12 | 17 | {% endif %} 18 | {% endmacro %} 19 | -------------------------------------------------------------------------------- /wgadmin/templates/interfaces/edit/info.html: -------------------------------------------------------------------------------- 1 | {% extends 'interfaces/edit.html' %} 2 | {% from "lib.html" import render_form_field %} 3 | {% block form %} 4 |

Info

5 |
6 | {{ info_form.hidden_tag() }} 7 | {{ render_form_field(info_form.enabled) }} 8 | {{ render_form_field(info_form.host) }} 9 | {{ render_form_field(info_form.name) }} 10 | {{ render_form_field(info_form.description) }} 11 | {{ render_form_field(info_form.public_key) }} 12 | {{ render_form_field(info_form.persistent_keepalive) }} 13 | {{ render_form_field(info_form.linkable) }} 14 | {{ render_form_field(info_form.listen_port) }} 15 | {{ render_form_field(info_form.endpoint) }} 16 | 17 |
18 | 19 | {% endblock form %} 20 | -------------------------------------------------------------------------------- /wgadmin/templates/interfaces/edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'interfaces/base.html' %} 2 | 3 | {% block subtitle %}Edit {{ iface }}{% endblock %} 4 | 5 | {% block content %} 6 | 7 | 8 | Info | 9 | Addresses | 10 | Peers | 11 | Delete 12 | 13 |
14 | {% block form %}{% endblock %} 15 | 16 |
17 |

Download config

18 | One time file or QR code | 19 | wg-quick | 20 | systemd-networkd | 21 | NetworkManager 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | setup( 4 | name='wgadmin', 5 | version='0.0.0', 6 | author="Arti Zirk", 7 | author_email="arti@zirk.me", 8 | description="WireGuard VPN Administration interface", 9 | packages=find_packages(), 10 | include_package_data=True, 11 | zip_safe=False, 12 | python_requires='>=3.5', 13 | install_requires=[ 14 | 'Flask', 15 | 'Flask-WTF', 16 | 'Flask-SQLAlchemy' 17 | ], 18 | extras_require={ 19 | 'dev': ['Flask-Testing'] 20 | }, 21 | classifiers=[ 22 | 'License :: OSI Approved :: MIT License', 23 | 'Programming Language :: Python :: 3', 24 | 'Programming Language :: Python :: 3 :: Only', 25 | 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', 26 | 'Topic :: System :: Networking', 27 | 'Intended Audience :: System Administrators', 28 | 'Framework :: Flask' 29 | ] 30 | 31 | ) 32 | -------------------------------------------------------------------------------- /experiments/test_sqlalchemy.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.orm import sessionmaker 3 | from sqlalchemy.ext.declarative import declarative_base 4 | from sqlalchemy import Column, Integer, String 5 | 6 | engine = create_engine('sqlite:///./test.db', echo=True) 7 | 8 | Session = sessionmaker(bind=engine) 9 | Base = declarative_base() 10 | 11 | 12 | class User(Base): 13 | __tablename__ = 'users' 14 | 15 | id = Column(Integer, primary_key=True) 16 | name = Column(String) 17 | fullname = Column(String) 18 | nickname = Column(String) 19 | 20 | def __repr__(self): 21 | return "" % ( 22 | self.name, self.fullname, self.nickname) 23 | 24 | 25 | Base.metadata.create_all(engine) 26 | 27 | session = Session() 28 | ed_user = User(name="ed") 29 | print(str(ed_user.id)) 30 | session.add(ed_user) 31 | session.commit() 32 | print(str(ed_user.id)) 33 | session.close() 34 | 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wireguard admin interface 2 | 3 | Plan is to provide a central config server with web ui and a gateway agent. 4 | Not yet ready. 5 | 6 | [Screenshots](https://github.com/artizirk/wireguard-admin/wiki) 7 | 8 | ![Screenshot of main view](https://i.imgur.com/gK9f6Mw.png) 9 | 10 | # Development setup 11 | 12 | python3 -m venv venv 13 | source venv/bin/activate 14 | pip install -r requirements.txt -r dev-requirements.txt 15 | 16 | ## Initialize database 17 | 18 | source venv/bin/activate 19 | flask init-db 20 | 21 | 22 | ## Run dev server 23 | 24 | source venv/bin/activate 25 | export FLASK_ENV=development 26 | flask run 27 | 28 | ## Run unittests 29 | 30 | source venv/bin/activate 31 | python -m unittest discover tests 32 | 33 | ## Update requirements.txt 34 | 35 | source venv/bin/activate 36 | pip-compile --upgrade setup.py 37 | pip-compile --upgrade dev-requirements.in 38 | # Apply updates to current venv 39 | pip-sync dev-requirements.txt requirements.txt 40 | -------------------------------------------------------------------------------- /wgadmin/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% block title %}{% endblock %} - Wireguard Admin 7 | 8 | 9 | 10 |
11 |

Wireguard Admin

12 | 21 |
22 |
23 | 24 | 27 | {% for message in get_flashed_messages() %} 28 |
{{ message }}
29 | {% endfor %} 30 | 31 | {% block content %}{% endblock %} 32 | 33 | 34 | -------------------------------------------------------------------------------- /wgadmin/templates/interfaces/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'interfaces/base.html' %} 2 | 3 | {% block subtitle %}List{% endblock %} 4 | 5 | {% block content %} 6 | 7 | {% from 'lib.html' import search_form_macro %} 8 | {{ search_form_macro(search_form) }} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% for interface in ifaces %} 20 | 21 | 22 | 23 | 24 | 25 | 26 | 33 | 36 | 37 | {% endfor %} 38 | 39 |
NameOwnerPeers
{{ interface }}arti{{ interface.slaves.__len__() }}/{{ interface.masters.__len__() }}
27 | {% for addr in interface.address %} 28 | {{ addr.address }}, 29 | {% else %} 30 | No Addresses configured 31 | {% endfor %} 32 | 34 | Edit 35 |
40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /wgadmin/templates/interfaces/add_peer.html: -------------------------------------------------------------------------------- 1 | {% extends 'interfaces/base.html' %} 2 | 3 | {% block subtitle %}Add peer to {{ iface.host }}@{{ iface.name }}{% endblock %} 4 | 5 | {% block content %} 6 | 7 | 8 | {% from 'lib.html' import search_form_macro %} 9 | {{ search_form_macro(search_form) }} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% for iface in ifaces %} 20 | 21 | 22 | 28 | 29 | 30 | 37 | 38 | {% else %} 39 | 40 | 41 | 42 | {% endfor %} 43 | 44 |
NameAction
{{ iface }} 23 |
24 | 25 | 26 |
27 |
31 | {% for addr in iface.address %} 32 | {{ addr.address }}, 33 | {% else %} 34 | No Addresses configured 35 | {% endfor %} 36 |
No Interfaces available
45 | 46 | 47 | {% endblock %} 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Arti Zirk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /wgadmin/utils.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | from hashlib import sha256 3 | from ipaddress import IPv4Address, IPv6Address, IPv4Network, IPv6Network 4 | from ipaddress import ip_address, ip_network 5 | 6 | 7 | def gen_ip( 8 | pubkey: Union[bytes, str], 9 | subnet: Union[IPv4Network, IPv6Network] = ip_network('fe80::/64') 10 | ) -> Union[IPv4Address, IPv6Address]: 11 | """Generate wg-ip compatible addresses from WireGuard public key""" 12 | 13 | prefix_bytes = subnet.network_address.packed 14 | mask_bytes = subnet.netmask.packed 15 | if type(pubkey) != "bytes": 16 | pubkey = pubkey.encode('ascii') # only bytes can be sha256sumed 17 | suffix_bytes = sha256(pubkey+b'\n').digest() 18 | 19 | address = b'' 20 | for prefix, suffix, mask in zip(prefix_bytes, suffix_bytes, mask_bytes): 21 | address += ( 22 | (prefix & mask) | (suffix & (mask^255)) 23 | ).to_bytes(1, byteorder='big') # In network byte order 24 | 25 | return ip_address(address) 26 | 27 | 28 | if __name__ == '__main__': 29 | pubkey = "foo" 30 | subnet = ip_network("fd1a:6126:2887::/48") 31 | #subnet = ip_network("10.0.0.0/8") 32 | exp_result = ip_address("fd1a:6126:2887:f9b1:d61e:21e7:96d7:8dcc") 33 | real_result = gen_ip(pubkey, subnet) 34 | print(exp_result, real_result, exp_result == real_result) 35 | -------------------------------------------------------------------------------- /tests/test_gen_ip.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from ipaddress import ip_address, ip_network 3 | 4 | from wgadmin.utils import gen_ip 5 | 6 | 7 | # Test Cases from wg-ip test_gen_ip 8 | # https://github.com/chmduquesne/wg-ip/blob/1a0ac39a511d67a078f0e2bb17776fe090f46720/wg-ip#L462 9 | DATA = [ 10 | ("foo", "fd1a:6126:2887::/48", "fd1a:6126:2887:f9b1:d61e:21e7:96d7:8dcc"), 11 | ("bar", "fd1a:6126:2887::/48", "fd1a:6126:2887:6691:8c98:63af:ca94:2d0f"), 12 | ("foo", "fd1a:6126:2887::/49", "fd1a:6126:2887:79b1:d61e:21e7:96d7:8dcc"), 13 | ("bar", "fd1a:6126:2887::/49", "fd1a:6126:2887:6691:8c98:63af:ca94:2d0f"), 14 | ("foo", "2001:db8::/64", "2001:db8::d61e:21e7:96d7:8dcc"), 15 | ("bar", "2001:db8::/64", "2001:db8::8c98:63af:ca94:2d0f"), 16 | ("foo", "10.0.0.0/8", "10.187.157.128"), 17 | ("bar", "10.0.0.0/8", "10.134.94.149"), 18 | ("foo", "10.0.0.0/9", "10.59.157.128"), 19 | ("bar", "10.0.0.0/9", "10.6.94.149"), 20 | ("foo", "172.16.0.0/12", "172.27.157.128"), 21 | ("bar", "172.16.0.0/12", "172.22.94.149") 22 | ] 23 | 24 | 25 | class TestGenIp(TestCase): 26 | def test_gen_ip(self): 27 | for case in DATA: 28 | pubkey = case[0] 29 | network = ip_network(case[1]) 30 | expected_result = ip_address(case[2]) 31 | with self.subTest(): 32 | self.assertEqual(gen_ip(pubkey, network), expected_result) 33 | -------------------------------------------------------------------------------- /wgadmin/forms.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField, IntegerField, BooleanField 3 | from wtforms.validators import Optional, NumberRange, Length 4 | 5 | class InterfaceSearchForm(FlaskForm): 6 | class Meta: 7 | csrf = False 8 | query = StringField("Search") 9 | 10 | 11 | class InterfaceInfoForm(FlaskForm): 12 | public_key = StringField("Public Key") 13 | host = StringField("Hostname", 14 | validators=(Length(max=128),), 15 | render_kw={"placeholder": "my-pc"}) 16 | name = StringField("Interface Name", 17 | validators=(Length(max=16),), 18 | render_kw={"placeholder": "wg0"}) 19 | description = StringField("Description", 20 | validators=(Length(max=1024),)) 21 | listen_port = IntegerField("Listen Port", 22 | validators=(Optional(),NumberRange(0, (2**16)-1)), 23 | render_kw={"placeholder": "51820"}) 24 | persistent_keepalive = IntegerField("Persistent Keepalive", 25 | validators=(Optional(),NumberRange(0, (2**16)-1))) 26 | endpoint = StringField("Endpoint", 27 | validators=(Length(max=253),), 28 | render_kw={"placeholder": "example.com:51820"}) 29 | linkable = BooleanField("Linkable", default=False) 30 | enabled = BooleanField("Enabled", default=True) 31 | -------------------------------------------------------------------------------- /wgadmin/templates/interfaces/add.html: -------------------------------------------------------------------------------- 1 | {% extends 'interfaces/base.html' %} 2 | 3 | {% block subtitle %}Add{% endblock %} 4 | 5 | {% block content %} 6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | 39 | 40 | 41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /.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 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # pipenv 86 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 87 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 88 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 89 | # install all needed dependencies. 90 | #Pipfile.lock 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | .spyproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | .dmypy.json 120 | dmypy.json 121 | 122 | # Pyre type checker 123 | .pyre/ 124 | 125 | 126 | # Pycharm 127 | .idea/ 128 | -------------------------------------------------------------------------------- /tests/test_models.py: -------------------------------------------------------------------------------- 1 | import os 2 | from itertools import islice 3 | from flask_testing import TestCase 4 | 5 | from wgadmin import create_app, db, init_db 6 | from wgadmin.models import Interface, IpAddress, Peer 7 | 8 | INTERFACES = { 9 | "master": "gSIp9rl2sStEzurSbMFGTDPwz5mx5/xmkd9VA68+8yI=", 10 | "a": "onjhmO9zcd7hVJLhwNiKGl5qlz9qTXcGgmuFp4pvu0k=", 11 | "b": "gwU9CStMW6SIzPn3kZ3LVhEgEJjeMwORfyPD4xxX/kA=", 12 | "c": "n7DW48WJAUoWKHcBmTB7U+02eBzixnKfkg0h4yisRiI=", 13 | "d": "sd/Jg8QA2UHEox5uXj9IuxyrZ5yY+Fo7mpan2nmo0Ao=", 14 | } 15 | 16 | 17 | class TestPeers(TestCase): 18 | 19 | def create_app(self): 20 | self.app = create_app({ 21 | "TESTING": True 22 | }) 23 | return self.app 24 | 25 | def setUp(self) -> None: 26 | 27 | init_db() 28 | for name, key in INTERFACES.items(): 29 | i = Interface() 30 | i.name = name 31 | i.public_key = key 32 | db.session.add(i) 33 | db.session.commit() 34 | 35 | def tearDown(self) -> None: 36 | db.session.remove() 37 | #db.drop_all() 38 | 39 | def test_one_peer_half_linked(self): 40 | ifm = Interface.query.filter_by(name="master").one() 41 | ifa = Interface.query.filter_by(name="a").one() 42 | p = Peer() 43 | p.master = ifm 44 | p.slave = ifa 45 | db.session.add(p) 46 | db.session.commit() 47 | 48 | self.assertIn(p, ifm.slaves) 49 | self.assertNotIn(p, ifm.masters) 50 | 51 | self.assertIn(p, ifa.masters) 52 | self.assertNotIn(p, ifa.slaves) 53 | 54 | self.assertNotIn(p, ifm.fully_linked_peers) 55 | self.assertIn(p, ifm.outgoing_peers) 56 | self.assertNotIn(p, ifm.incoming_peers) 57 | 58 | self.assertIn(p, ifa.incoming_peers) 59 | self.assertNotIn(p, ifa.outgoing_peers) 60 | 61 | def test_many_peers_half_linked(self): 62 | ifm = Interface.query.filter_by(name="master").one() 63 | for i, (name, key) in enumerate(islice(INTERFACES.items(), 1, None), 1): 64 | with self.subTest(i=i): 65 | ifx = Interface.query.filter_by(name=name).one() 66 | p = Peer() 67 | p.master = ifm 68 | p.slave = ifx 69 | db.session.add(p) 70 | db.session.commit() 71 | 72 | self.assertIn(p, ifm.outgoing_peers.all()) 73 | self.assertIn(p, ifx.incoming_peers.all()) 74 | self.assertEqual(len(ifm.slaves), i) 75 | self.assertEqual(ifm.outgoing_peers.count(), i) 76 | self.assertEqual(ifx.incoming_peers.count(), 1) 77 | -------------------------------------------------------------------------------- /wgadmin/static/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | margin: 40px auto; 7 | max-width: 650px; 8 | line-height: 1.6; 9 | font-size: 18px; 10 | color: #444; 11 | padding: 0 10px; 12 | } 13 | 14 | h1, h2, h3 { 15 | line-height: 1.2 16 | } 17 | 18 | hr { 19 | display: block; 20 | height: 1px; 21 | border: 0; 22 | border-top: 1px solid #ccc; 23 | margin: 1em 0; 24 | padding: 0; 25 | } 26 | 27 | header { 28 | display: flex; 29 | align-items: flex-end; 30 | justify-content: space-between; 31 | flex-wrap: wrap; 32 | } 33 | 34 | header h1 { 35 | margin: 0; 36 | } 37 | 38 | .page-header { 39 | margin-bottom: 1.5em; 40 | } 41 | 42 | .page-header h1, .page-header h2, .page-header h3{ 43 | margin: 0; 44 | } 45 | 46 | table { 47 | table-layout: fixed; 48 | border-collapse: collapse; 49 | width: 100%; 50 | margin: 1em 0; 51 | } 52 | 53 | td, th { 54 | border: 1px solid #dddddd; 55 | text-align: left; 56 | padding: 8px; 57 | } 58 | 59 | .even tr:nth-child(even) { 60 | background-color: #f5f5f5; 61 | } 62 | 63 | .double tr:nth-child(4n+0) { 64 | background-color: #f5f5f5; 65 | } 66 | .double tr:nth-child(4n+3) { 67 | background-color: #f5f5f5; 68 | } 69 | 70 | .double th:first-child { 71 | width: 50%; 72 | } 73 | 74 | .flash { 75 | margin: 1em 0; 76 | padding: 1em; 77 | background: #f6e7db; 78 | border: 1px solid #000000; 79 | } 80 | 81 | .errors { 82 | margin: 0.1em; 83 | padding: 0.1em 0.5em; 84 | background: #ffb3b3; 85 | border: 1px solid #000000; 86 | flex: 100% 87 | } 88 | ul.errors { 89 | list-style-type: none; 90 | } 91 | 92 | /* Top-down Form 93 | Label [Input] 94 | -----Button----- 95 | */ 96 | 97 | form { 98 | display:flex; 99 | flex-direction: row; 100 | flex-wrap: wrap; 101 | justify-content: space-between; 102 | align-items: center; 103 | } 104 | 105 | form label { 106 | flex-basis: 30%; 107 | } 108 | 109 | form input { 110 | flex-basis: 70%; 111 | margin: 0.4em 0; 112 | height: 2em; 113 | } 114 | 115 | form input[type=submit], form input[type=button] { 116 | flex-basis: 100%; 117 | } 118 | 119 | form .basis_auto { 120 | flex-basis: auto !important; 121 | } 122 | 123 | form textarea { 124 | height: 30em; 125 | flex-basis: 100%; 126 | margin-bottom:0.4em; 127 | } 128 | 129 | /* Inline Form 130 | Label [Input] Button 131 | */ 132 | form.inline { 133 | flex-wrap: nowrap; 134 | align-content: stretch; 135 | align-items: center; 136 | } 137 | 138 | form.inline label, form.inline input { 139 | align-self: unset; 140 | flex: 1 1 auto; 141 | margin: 0 0.3em 1em; 142 | } 143 | 144 | form.inline label, 145 | form.inline input[type=submit], 146 | form.inline input[type=reset], 147 | form.inline input[type=button] { 148 | flex: 0 1 auto; 149 | } 150 | -------------------------------------------------------------------------------- /wgadmin/templates/interfaces/edit/addresses.html: -------------------------------------------------------------------------------- 1 | {% extends 'interfaces/edit.html' %} 2 | 3 | {% block form %} 4 |

Addresses

5 |

Fully linked peers can use those Address to connect to this peer

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% for addr in iface.address %} 15 | 16 | 17 | 25 | 26 | {% else %} 27 | 28 | 31 | 32 | {% endfor %} 33 | 34 |
AddressAction
{{ addr.address }} 18 |
19 | 20 | 21 | 22 | 23 |
24 |
29 | No Addresses configured 30 |
35 | 36 |

Add address to the interface

37 | 38 |
39 | 40 | 41 | 42 | 43 | 44 |
45 | 46 | 47 | 48 |
49 | 50 | 51 |
52 | 53 |

Additional routes

54 |

55 | List of additional routes that are available to fully linked peers via this peer 56 |

57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | {% for addr in iface.route %} 67 | 68 | 69 | 77 | 78 | {% else %} 79 | 80 | 83 | 84 | {% endfor %} 85 | 86 |
RouteAction
{{ addr.address }} 70 |
71 | 72 | 73 | 74 | 75 |
76 |
81 | No routes configured 82 |
87 | 88 |

Add routes to the interface

89 | 90 |
91 | 92 | 93 | 94 | 95 | 96 |
97 | 98 | 99 | 100 | {% endblock form %} 101 | -------------------------------------------------------------------------------- /wgadmin/auth.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | from flask import ( 4 | Blueprint, flash, g, redirect, render_template, request, session, url_for 5 | ) 6 | from werkzeug.security import check_password_hash, generate_password_hash 7 | 8 | from . import db 9 | from .models import User 10 | 11 | bp = Blueprint('auth', __name__, url_prefix='/auth') 12 | 13 | 14 | def login_required(view): 15 | """View decorator that redirects anonymous users to the login page.""" 16 | 17 | @functools.wraps(view) 18 | def wrapped_view(**kwargs): 19 | if g.user is None: 20 | return redirect(url_for("auth.login")) 21 | 22 | return view(**kwargs) 23 | 24 | return wrapped_view 25 | 26 | 27 | @bp.before_app_request 28 | def load_logged_in_user(): 29 | """If a user id is stored in the session, load the user object from 30 | the database into ``g.user``.""" 31 | user_id = session.get("user_id") 32 | g.user = User.query.get(user_id) if user_id is not None else None 33 | 34 | 35 | @bp.route("/register", methods=("GET", "POST")) 36 | def register(): 37 | """Register a new user. 38 | 39 | Validates that the username is not already taken. Hashes the 40 | password for security. 41 | """ 42 | if request.method == "POST": 43 | username = request.form["username"] 44 | password = request.form["password"] 45 | error = None 46 | 47 | if not username: 48 | error = "Username is required." 49 | elif not password: 50 | error = "Password is required." 51 | elif db.session.query( 52 | User.query.filter_by(username=username).exists() 53 | ).scalar(): 54 | error = f"User {username} is already registered." 55 | 56 | if error is None: 57 | # the name is available, create the user and go to the login page 58 | db.session.add(User(username=username, password=password)) 59 | db.session.commit() 60 | return redirect(url_for("auth.login")) 61 | 62 | flash(error) 63 | 64 | return render_template("auth/register.html") 65 | 66 | 67 | @bp.route("/login", methods=("GET", "POST")) 68 | def login(): 69 | """Log in a registered user by adding the user id to the session.""" 70 | if request.method == "POST": 71 | username = request.form["username"] 72 | password = request.form["password"] 73 | error = None 74 | user = User.query.filter_by(username=username).first() 75 | 76 | if user is None: 77 | error = "Incorrect username." 78 | elif not user.check_password(password): 79 | error = "Incorrect password." 80 | 81 | if error is None: 82 | # store the user id in a new session and return to the index 83 | session.clear() 84 | session["user_id"] = user.id 85 | return redirect(url_for("index")) 86 | 87 | flash(error) 88 | 89 | return render_template("auth/login.html") 90 | 91 | 92 | @bp.route("/logout") 93 | def logout(): 94 | """Clear the current session, including the stored user id.""" 95 | session.clear() 96 | return redirect(url_for("index")) 97 | -------------------------------------------------------------------------------- /wgadmin/config.py: -------------------------------------------------------------------------------- 1 | import ipaddress 2 | 3 | from sqlalchemy.exc import IntegrityError 4 | from flask import ( 5 | Blueprint, Response, flash, g, redirect, render_template, request, url_for 6 | ) 7 | from http import HTTPStatus 8 | 9 | from . import db 10 | from .models import Interface, IpAddress, Peer 11 | from .auth import login_required 12 | from .forms import InterfaceSearchForm 13 | from . import utils 14 | 15 | bp = Blueprint("config", __name__) 16 | 17 | 18 | def generate_wg_config(id, variant="quick", private_key_placeholder=True): 19 | iface = Interface.query.get_or_404(id) 20 | body = ["[Interface]"] 21 | if private_key_placeholder: 22 | body.append("\n# PrivateKey = REPLACE ME!!") 23 | body.append("\n# PublicKey = " + iface.public_key) 24 | if iface.listen_port: 25 | body.append("\nListenPort = " + str(iface.listen_port)) 26 | 27 | if variant == "quick": 28 | body.append("\nAddress = ") 29 | for addr in iface.address: 30 | body.append(str(addr.address)) 31 | body.append(', ') 32 | del body[-1] # Remove last , 33 | 34 | for peer in iface.fully_linked_peers: 35 | body.append("\n\n# " + str(peer.slave)) 36 | body.append("\n[Peer]") 37 | body.append("\nPublicKey = " + peer.slave.public_key) 38 | if peer.slave.persistent_keepalive: 39 | body.append("\nPersistentKeepalive = " + str(peer.slave.persistent_keepalive)) 40 | if peer.slave.endpoint: 41 | body.append("\nEndpoint = " + peer.slave.endpoint) 42 | body.append("\nAllowedIPs = ") 43 | for addr in peer.slave.allowed_ips: 44 | body.append(str(addr.address)) 45 | body.append(', ') 46 | del body[-1] # Remove last , 47 | body.append("\n") 48 | 49 | return Response(body, mimetype='text/plain') 50 | 51 | 52 | @bp.route("/export//wg-quick") 53 | def export_wg_quick(id): 54 | return generate_wg_config(id, "quick") 55 | 56 | 57 | @bp.route("/export//wg-sync") 58 | def export_wg_synx(id): 59 | return generate_wg_config(id, "sync", 60 | private_key_placeholder=False) 61 | 62 | 63 | @bp.route("/export//onetime", methods=("GET", "POST")) 64 | def export_onetime(id): 65 | iface = Interface.query.get_or_404(id) 66 | if request.method == "POST": 67 | iface.public_key = request.form.get("publicKey") 68 | db.session.add(iface) 69 | try: 70 | db.session.commit() 71 | return generate_wg_config(id, "quick", 72 | private_key_placeholder=False) 73 | except IntegrityError as e: 74 | db.session.rollback() 75 | if "UNIQUE" in str(e): 76 | msg = "Error: Public Key is not UNIQUE" 77 | return Response(msg, status=HTTPStatus.CONFLICT, 78 | mimetype='text/plain') 79 | else: 80 | msg = "Error: {}".format(e) 81 | return Response(msg, status=HTTPStatus.BAD_REQUEST, 82 | mimetype='text/plain') 83 | return render_template("export_onetime_conf.html", iface=iface) 84 | -------------------------------------------------------------------------------- /wgadmin/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import click 4 | from flask import Flask 5 | from flask.cli import with_appcontext 6 | from flask_sqlalchemy import SQLAlchemy 7 | 8 | 9 | db = SQLAlchemy() 10 | 11 | 12 | def create_app(test_config=None): 13 | # create and configure the app 14 | app = Flask(__name__, instance_relative_config=True) 15 | 16 | # some deploy systems set the database url in the environ 17 | db_url = os.environ.get("DATABASE_URL") 18 | 19 | if db_url is None: 20 | # default to a sqlite database in the instance folder 21 | db_url = "sqlite:///" + os.path.join(app.instance_path, "wgadmin.sqlite") 22 | # ensure the instance folder exists 23 | os.makedirs(app.instance_path, exist_ok=True) 24 | 25 | app.config.from_mapping( 26 | # default secret that should be overridden in environ or config 27 | SECRET_KEY=os.environ.get("SECRET_KEY", "dev"), 28 | SQLALCHEMY_DATABASE_URI=db_url, 29 | SQLALCHEMY_TRACK_MODIFICATIONS=False, 30 | SQLALCHEMY_ECHO=True, 31 | ) 32 | 33 | if test_config is None: 34 | # load the instance config, if it exists, when not testing 35 | app.config.from_pyfile('config.py', silent=True) 36 | else: 37 | # load the test config if passed in 38 | app.config.from_mapping(test_config) 39 | 40 | # ensure the instance folder exists 41 | try: 42 | os.makedirs(app.instance_path) 43 | except OSError: 44 | pass 45 | 46 | # initialize Flask-SQLAlchemy and the init-db command 47 | db.init_app(app) 48 | app.cli.add_command(init_db_command) 49 | app.cli.add_command(add_addr) 50 | app.cli.add_command(get_addr) 51 | 52 | 53 | from . import auth 54 | app.register_blueprint(auth.bp) 55 | 56 | from . import interfaces 57 | app.register_blueprint(interfaces.bp) 58 | app.add_url_rule('/', endpoint='index') 59 | 60 | from . import config 61 | app.register_blueprint(config.bp, url_prefix='/config') 62 | 63 | # a simple page that says hello 64 | @app.route('/hello') 65 | def hello(): 66 | return 'Hello, World!' 67 | 68 | return app 69 | 70 | 71 | def init_db(): 72 | db.drop_all() 73 | db.create_all() 74 | 75 | 76 | @click.command('init-db') 77 | @with_appcontext 78 | def init_db_command(): 79 | """Clear the existing data and create new tables.""" 80 | init_db() 81 | click.echo('Initialized the database.') 82 | 83 | 84 | @click.command('addr') 85 | @with_appcontext 86 | def add_addr(): 87 | """Test command""" 88 | from .models import IpAddress 89 | import ipaddress 90 | a = IpAddress() 91 | a.address = ipaddress.ip_interface('fe80::1/64') 92 | print(a.id, a.version, a.mask, a._address0, a._address1, a._address2, a._address3) 93 | print(a.address) 94 | db.session.add(a) 95 | db.session.commit() 96 | 97 | 98 | @click.command('paddr') 99 | @with_appcontext 100 | def get_addr(): 101 | from .models import IpAddress 102 | for a in IpAddress.query.all(): 103 | print(a.id, a.version, a.mask, a._address0, a._address1, a._address2, a._address3, a.address) 104 | -------------------------------------------------------------------------------- /wgadmin/templates/export_onetime_conf.html: -------------------------------------------------------------------------------- 1 | {% extends 'interfaces/base.html' %} 2 | 3 | {% block subtitle %}{{ iface }}{% endblock %} 4 | 5 | {% block content %} 6 |

Generate config

7 |

This form will generate a one time WireGuard key pair that will override 8 | currently stored public key that will be used to generate a 9 | fully working WireGuard wg-quick config file.

10 | 11 |

Select output type

12 | 13 | 14 | 15 |

16 |

17 | 18 | 19 | 20 | 104 | 105 | {% endblock %} 106 | -------------------------------------------------------------------------------- /wgadmin/templates/interfaces/edit/peers.html: -------------------------------------------------------------------------------- 1 | {% extends 'interfaces/edit.html' %} 2 | 3 | {% block form %} 4 |

Fully linked peers

5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% for peer in iface.fully_linked_peers %} 15 | 16 | 17 | 18 | 19 | 26 | 36 | 37 | {% else %} 38 | 39 | 40 | 41 | {% endfor %} 42 | 43 |
HostAction
{{ peer.slave }}
20 | {% for addr in peer.slave.allowed_ips %} 21 | {{ addr.address }}, 22 | {% else %} 23 | No Addresses configured 24 | {% endfor %} 25 | 27 |
28 | 29 |
30 |
31 | 32 | 33 | 34 |
35 |
No peers configured
44 |
45 | 46 |
47 | 48 | 49 |

Half linked peers

50 |

Outgoing links

51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | {% for peer in iface.outgoing_peers %} 60 | 61 | 62 | 63 | 64 | 71 | 81 | 82 | {% else %} 83 | 84 | 85 | 86 | {% endfor %} 87 | 88 |
HostAction
{{ peer.slave }}
65 | {% for addr in peer.slave.allowed_ips %} 66 | {{ addr.address }}, 67 | {% else %} 68 | No Addresses configured 69 | {% endfor %} 70 | 72 |
73 | 74 |
75 |
76 | 77 | 78 | 79 |
80 |
No peers configured
89 | 90 |

Incoming links

91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | {% for peer in iface.incoming_peers %} 100 | 101 | 102 | 103 | 104 | 111 | 120 | 121 | {% else %} 122 | 123 | 124 | 125 | {% endfor %} 126 | 127 |
HostAction
{{ peer.master }}
105 | {% for addr in peer.master.allowed_ips %} 106 | {{ addr.address }}, 107 | {% else %} 108 | No Addresses configured 109 | {% endfor %} 110 | 112 |
113 | 114 |
115 |
116 | 117 | 118 |
119 |
No peers configured
128 | 129 | {% endblock form %} 130 | -------------------------------------------------------------------------------- /wgadmin/static/wireguard.js: -------------------------------------------------------------------------------- 1 | /*! SPDX-License-Identifier: GPL-2.0 2 | * 3 | * Copyright (C) 2015-2019 Jason A. Donenfeld . All Rights Reserved. 4 | */ 5 | 6 | (function() { 7 | function gf(init) { 8 | var r = new Float64Array(16); 9 | if (init) { 10 | for (var i = 0; i < init.length; ++i) 11 | r[i] = init[i]; 12 | } 13 | return r; 14 | } 15 | 16 | function pack(o, n) { 17 | var b, m = gf(), t = gf(); 18 | for (var i = 0; i < 16; ++i) 19 | t[i] = n[i]; 20 | carry(t); 21 | carry(t); 22 | carry(t); 23 | for (var j = 0; j < 2; ++j) { 24 | m[0] = t[0] - 0xffed; 25 | for (var i = 1; i < 15; ++i) { 26 | m[i] = t[i] - 0xffff - ((m[i - 1] >> 16) & 1); 27 | m[i - 1] &= 0xffff; 28 | } 29 | m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1); 30 | b = (m[15] >> 16) & 1; 31 | m[14] &= 0xffff; 32 | cswap(t, m, 1 - b); 33 | } 34 | for (var i = 0; i < 16; ++i) { 35 | o[2 * i] = t[i] & 0xff; 36 | o[2 * i + 1] = t[i] >> 8; 37 | } 38 | } 39 | 40 | function carry(o) { 41 | var c; 42 | for (var i = 0; i < 16; ++i) { 43 | o[(i + 1) % 16] += (i < 15 ? 1 : 38) * Math.floor(o[i] / 65536); 44 | o[i] &= 0xffff; 45 | } 46 | } 47 | 48 | function cswap(p, q, b) { 49 | var t, c = ~(b - 1); 50 | for (var i = 0; i < 16; ++i) { 51 | t = c & (p[i] ^ q[i]); 52 | p[i] ^= t; 53 | q[i] ^= t; 54 | } 55 | } 56 | 57 | function add(o, a, b) { 58 | for (var i = 0; i < 16; ++i) 59 | o[i] = (a[i] + b[i]) | 0; 60 | } 61 | 62 | function subtract(o, a, b) { 63 | for (var i = 0; i < 16; ++i) 64 | o[i] = (a[i] - b[i]) | 0; 65 | } 66 | 67 | function multmod(o, a, b) { 68 | var t = new Float64Array(31); 69 | for (var i = 0; i < 16; ++i) { 70 | for (var j = 0; j < 16; ++j) 71 | t[i + j] += a[i] * b[j]; 72 | } 73 | for (var i = 0; i < 15; ++i) 74 | t[i] += 38 * t[i + 16]; 75 | for (var i = 0; i < 16; ++i) 76 | o[i] = t[i]; 77 | carry(o); 78 | carry(o); 79 | } 80 | 81 | function invert(o, i) { 82 | var c = gf(); 83 | for (var a = 0; a < 16; ++a) 84 | c[a] = i[a]; 85 | for (var a = 253; a >= 0; --a) { 86 | multmod(c, c, c); 87 | if (a !== 2 && a !== 4) 88 | multmod(c, c, i); 89 | } 90 | for (var a = 0; a < 16; ++a) 91 | o[a] = c[a]; 92 | } 93 | 94 | function clamp(z) { 95 | z[31] = (z[31] & 127) | 64; 96 | z[0] &= 248; 97 | } 98 | 99 | function generatePublicKey(privateKey) { 100 | var r, z = new Uint8Array(32); 101 | var a = gf([1]), 102 | b = gf([9]), 103 | c = gf(), 104 | d = gf([1]), 105 | e = gf(), 106 | f = gf(), 107 | _121665 = gf([0xdb41, 1]), 108 | _9 = gf([9]); 109 | for (var i = 0; i < 32; ++i) 110 | z[i] = privateKey[i]; 111 | clamp(z); 112 | for (var i = 254; i >= 0; --i) { 113 | r = (z[i >>> 3] >>> (i & 7)) & 1; 114 | cswap(a, b, r); 115 | cswap(c, d, r); 116 | add(e, a, c); 117 | subtract(a, a, c); 118 | add(c, b, d); 119 | subtract(b, b, d); 120 | multmod(d, e, e); 121 | multmod(f, a, a); 122 | multmod(a, c, a); 123 | multmod(c, b, e); 124 | add(e, a, c); 125 | subtract(a, a, c); 126 | multmod(b, a, a); 127 | subtract(c, d, f); 128 | multmod(a, c, _121665); 129 | add(a, a, d); 130 | multmod(c, c, a); 131 | multmod(a, d, f); 132 | multmod(d, b, _9); 133 | multmod(b, e, e); 134 | cswap(a, b, r); 135 | cswap(c, d, r); 136 | } 137 | invert(c, c); 138 | multmod(a, a, c); 139 | pack(z, a); 140 | return z; 141 | } 142 | 143 | function generatePresharedKey() { 144 | var privateKey = new Uint8Array(32); 145 | window.crypto.getRandomValues(privateKey); 146 | return privateKey; 147 | } 148 | 149 | function generatePrivateKey() { 150 | var privateKey = generatePresharedKey(); 151 | clamp(privateKey); 152 | return privateKey; 153 | } 154 | 155 | function encodeBase64(dest, src) { 156 | var input = Uint8Array.from([(src[0] >> 2) & 63, ((src[0] << 4) | (src[1] >> 4)) & 63, ((src[1] << 2) | (src[2] >> 6)) & 63, src[2] & 63]); 157 | for (var i = 0; i < 4; ++i) 158 | dest[i] = input[i] + 65 + 159 | (((25 - input[i]) >> 8) & 6) - 160 | (((51 - input[i]) >> 8) & 75) - 161 | (((61 - input[i]) >> 8) & 15) + 162 | (((62 - input[i]) >> 8) & 3); 163 | } 164 | 165 | function keyToBase64(key) { 166 | var i, base64 = new Uint8Array(44); 167 | for (i = 0; i < 32 / 3; ++i) 168 | encodeBase64(base64.subarray(i * 4), key.subarray(i * 3)); 169 | encodeBase64(base64.subarray(i * 4), Uint8Array.from([key[i * 3 + 0], key[i * 3 + 1], 0])); 170 | base64[43] = 61; 171 | return String.fromCharCode.apply(null, base64); 172 | } 173 | 174 | window.wireguard = { 175 | generateKeypair: function() { 176 | var privateKey = generatePrivateKey(); 177 | var publicKey = generatePublicKey(privateKey); 178 | return { 179 | publicKey: keyToBase64(publicKey), 180 | privateKey: keyToBase64(privateKey) 181 | }; 182 | } 183 | }; 184 | })(); 185 | -------------------------------------------------------------------------------- /wgadmin/interfaces.py: -------------------------------------------------------------------------------- 1 | import ipaddress 2 | 3 | from sqlalchemy.exc import IntegrityError 4 | from flask import ( 5 | Blueprint, flash, g, redirect, render_template, request, url_for 6 | ) 7 | 8 | from . import db 9 | from .models import Interface, IpAddress, Peer 10 | from .auth import login_required 11 | from . import forms 12 | from . import utils 13 | 14 | bp = Blueprint("interfaces", __name__) 15 | 16 | 17 | def add_ipv6_link_local_address(iface): 18 | ip = IpAddress() 19 | subnet = ipaddress.ip_network("fe80::/64") 20 | address = utils.gen_ip(iface.public_key, subnet) 21 | interface_addr = ipaddress.ip_interface((address, 128)) 22 | ip.address = interface_addr 23 | iface.address.append(ip) 24 | db.session.add(ip) 25 | return ip 26 | 27 | 28 | @bp.route("/") 29 | def list(): 30 | search_form = forms.InterfaceSearchForm(request.args) 31 | if search_form.query.data: 32 | ifaces = Interface.query.filter(db.or_( 33 | Interface.name.like('%' + search_form.query.data + '%'), 34 | Interface.host.like('%' + search_form.query.data + '%'), 35 | )).all() 36 | else: 37 | ifaces = Interface.query.all() 38 | return render_template("interfaces/list.html", 39 | search_form=search_form, ifaces=ifaces) 40 | 41 | 42 | @bp.route("/add", methods=('GET', 'POST')) 43 | def add(): 44 | if request.method == 'POST': 45 | interface = Interface() 46 | interface.host = request.form["host"] 47 | interface.name = request.form["name"] 48 | interface.description = request.form["description"] 49 | interface.public_key = request.form["publicKey"] 50 | 51 | if "generateIp" in request.form: 52 | add_ipv6_link_local_address(interface) 53 | db.session.add(interface) 54 | try: 55 | db.session.commit() 56 | flash("New WireGuard interface added") 57 | return redirect(url_for("interfaces.edit", id=interface.id)) 58 | except IntegrityError as e: 59 | db.session.rollback() 60 | if "UNIQUE" in str(e): 61 | flash("Error: Public Key is not UNIQUE") 62 | else: 63 | flash("Error: {}".format(e)) 64 | return render_template("interfaces/add.html", form=request.form) 65 | return render_template("interfaces/add.html", form={}) 66 | 67 | 68 | @bp.route("/edit/", methods=("GET", "POST")) 69 | def edit(id): 70 | iface = Interface.query.get_or_404(id) 71 | info_form = forms.InterfaceInfoForm(obj=iface) 72 | if info_form.validate_on_submit(): 73 | info_form.populate_obj(iface) 74 | db.session.commit() 75 | flash("Interface updated") 76 | return redirect(url_for("interfaces.edit", id=id)) 77 | return render_template("interfaces/edit/info.html", 78 | iface=iface, info_form=info_form) 79 | 80 | 81 | @bp.route("/edit//addresses", methods=("GET", "POST")) 82 | def addresses(id): 83 | iface = Interface.query.get_or_404(id) 84 | if request.method == "POST": 85 | if request.form["action"] in {"addAddress", "addRoute"}: 86 | ip = IpAddress() 87 | try: 88 | ip.address = ipaddress.ip_interface(request.form["address"]) 89 | except ValueError as e: 90 | flash("Error adding IP address: {}".format(e)) 91 | else: 92 | if request.form["action"] == "addRoute": 93 | ip.route_only = True 94 | iface.route.append(ip) 95 | else: 96 | ip.route_only = False 97 | iface.address.append(ip) 98 | db.session.add(ip) 99 | db.session.commit() 100 | flash("IP Address added") 101 | elif request.form["action"] == "generateLinkLocalAddress": 102 | add_ipv6_link_local_address(iface) 103 | db.session.commit() 104 | flash("Link Local IP address added") 105 | elif request.form["action"] in {"deleteAddress", "deleteRoute"}: 106 | ip = IpAddress.query.get_or_404(request.form["id"]) 107 | db.session.delete(ip) 108 | db.session.commit() 109 | flash("IP Address deleted") 110 | else: 111 | flash("Invalid action") 112 | return redirect(url_for("interfaces.addresses", id=id)) 113 | return render_template("interfaces/edit/addresses.html", iface=iface) 114 | 115 | 116 | @bp.route("/edit//peers", methods=("GET", "POST")) 117 | def peers(id): 118 | iface = Interface.query.get_or_404(id) 119 | if request.method == "POST" and request.form["action"] == "deletePeer": 120 | peer_id = request.form['peer'] 121 | peer = Peer.query.get_or_404(peer_id) 122 | db.session.delete(peer) 123 | db.session.commit() 124 | return redirect(url_for("interfaces.peers", id=id)) 125 | return render_template("interfaces/edit/peers.html", iface=iface) 126 | 127 | 128 | @bp.route("/edit//delete", methods=("GET", "POST")) 129 | def delete(id): 130 | iface = Interface.query.get_or_404(id) 131 | if request.method == "POST": 132 | db.session.delete(iface) 133 | db.session.commit() 134 | flash("Interface {}@{} deleted".format(iface.host, iface.name)) 135 | return redirect(url_for("interfaces.list")) 136 | return render_template("interfaces/edit/delete.html", iface=iface) 137 | 138 | 139 | @bp.route("/edit//add_peer", methods=("GET", "POST")) 140 | def add_peer(id): 141 | iface = Interface.query.get_or_404(id) 142 | if request.method == "POST": 143 | peer_id = request.form["peer"] 144 | peer_iface = Interface.query.filter_by(id=peer_id).first_or_404() 145 | peer = Peer() 146 | peer.master_id = iface.id 147 | peer.slave_id = peer_iface.id 148 | db.session.add(peer) 149 | db.session.add(iface) 150 | db.session.commit() 151 | flash("Peer {}@{} added".format(peer_iface.host, peer_iface.name)) 152 | return redirect(url_for("interfaces.peers", id=id)) 153 | 154 | search_form = forms.InterfaceSearchForm(request.args) 155 | if search_form.query.data: 156 | ifaces = iface.linkable_interfaces.filter(db.or_( 157 | Interface.name.like('%' + search_form.query.data + '%'), 158 | Interface.host.like('%' + search_form.query.data + '%'), 159 | )).all() 160 | else: 161 | ifaces = iface.linkable_interfaces 162 | return render_template("interfaces/add_peer.html", 163 | iface=iface, ifaces=ifaces, search_form=search_form) 164 | -------------------------------------------------------------------------------- /wgadmin/models.py: -------------------------------------------------------------------------------- 1 | import ipaddress 2 | 3 | from sqlalchemy.ext.hybrid import hybrid_property 4 | from werkzeug.security import check_password_hash 5 | from werkzeug.security import generate_password_hash 6 | 7 | from . import db 8 | 9 | 10 | class Peer(db.Model): 11 | id = db.Column(db.Integer, primary_key=True) 12 | master_id = db.Column(db.Integer, db.ForeignKey('interface.id'), nullable=False) 13 | slave_id = db.Column(db.Integer, db.ForeignKey('interface.id'), nullable=False) 14 | 15 | __table_args__ = (db.UniqueConstraint('master_id', 'slave_id'),) 16 | 17 | def __repr__(self): 18 | return ' {}>'.format(self.id, self.master, self.slave) 19 | 20 | 21 | class Interface(db.Model): 22 | id = db.Column(db.Integer, primary_key=True) 23 | enabled = db.Column(db.Boolean, default=True) 24 | linkable = db.Column(db.Boolean, default=False) 25 | host = db.Column(db.String(128)) 26 | public_key = db.Column(db.String(256), unique=True, nullable=False) 27 | listen_port = db.Column(db.Integer) 28 | persistent_keepalive = db.Column(db.Integer) 29 | name = db.Column(db.String(16), nullable=False) 30 | endpoint = db.Column(db.String(253)) 31 | description = db.Column(db.String(1024)) 32 | address = db.relationship("IpAddress", 33 | primaryjoin="and_(IpAddress.interface_id==Interface.id,\ 34 | IpAddress.route_only==False)") 35 | route = db.relationship("IpAddress", cascade="delete", 36 | primaryjoin="and_(IpAddress.interface_id==Interface.id,\ 37 | IpAddress.route_only==True)") 38 | allowed_ips = db.relationship("IpAddress", cascade="delete") 39 | masters = db.relationship("Peer", # List of interfaces that im slave to 40 | primaryjoin=Peer.slave_id==id, 41 | backref="slave", 42 | cascade="delete") 43 | slaves = db.relationship("Peer", # List of interfaces im master of 44 | primaryjoin=Peer.master_id==id, 45 | backref="master", 46 | cascade="delete") 47 | 48 | @property 49 | def fully_linked_peers(self): 50 | # TODO: SLOW AF 51 | a_peer = db.aliased(Peer) 52 | b_peer = db.aliased(Peer) 53 | return db.object_session(self).query(a_peer).\ 54 | filter(a_peer.slave_id.in_( 55 | db.object_session(self).query(b_peer.master_id).\ 56 | filter(b_peer.slave_id==self.id).subquery()), 57 | a_peer.master_id==self.id 58 | ) 59 | 60 | @property 61 | def outgoing_peers(self): 62 | """Not fully linked peers waiting on a backlink""" 63 | a_peer = db.aliased(Peer) 64 | b_peer = db.aliased(Peer) 65 | f_peer = db.aliased(Peer) 66 | qa = db.object_session(self).query(a_peer.master_id).\ 67 | filter(a_peer.slave_id == self.id) 68 | qb = db.object_session(self).query(b_peer.slave_id).\ 69 | filter(b_peer.master_id == self.id) 70 | qfa = qb.except_(qa) 71 | # print("++++",qfa.all()) 72 | qfb = qa.except_(qb) 73 | # print("++++",qfb.all()) 74 | # return db.object_session(self).query(f_peer).\ 75 | # filter(f_peer.master_id==qfa) 76 | return db.object_session(self).query(f_peer).\ 77 | filter(f_peer.slave_id.in_(qfa), f_peer.master_id == self.id) 78 | 79 | @property 80 | def incoming_peers(self): 81 | """Not fully linked peers that want to link with me""" 82 | a_peer = db.aliased(Peer) 83 | b_peer = db.aliased(Peer) 84 | f_peer = db.aliased(Peer) 85 | qa = db.object_session(self).query(a_peer.master_id).\ 86 | filter(a_peer.slave_id == self.id) 87 | qb = db.object_session(self).query(b_peer.slave_id).\ 88 | filter(b_peer.master_id == self.id) 89 | qfa = qb.except_(qa) 90 | # print("++++",qfa.all()) 91 | qfb = qa.except_(qb) 92 | # print("++++",qfb.all()) 93 | # return db.object_session(self).query(f_peer).\ 94 | # filter(f_peer.master_id==qfa) 95 | return db.object_session(self).query(f_peer).\ 96 | filter(f_peer.master_id.in_(qfb), f_peer.slave_id == self.id) 97 | 98 | @property 99 | def linkable_interfaces(self): 100 | return db.object_session(self).query(Interface).\ 101 | filter(Interface.id != self.id, 102 | db.not_( 103 | Interface.id.in_( 104 | db.object_session(self).query(Peer.slave_id). 105 | filter(Peer.master_id == self.id) 106 | ) 107 | ), 108 | Interface.enabled == True, 109 | db.or_( 110 | Interface.linkable == True, 111 | self.linkable == True 112 | ) 113 | ) 114 | 115 | def __str__(self): 116 | if self.host: 117 | return '{}@{}'.format(self.name, self.host) 118 | else: 119 | return self.name 120 | 121 | def __repr__(self): 122 | return ''.format(self.id, self.name, self.host) 123 | 124 | 125 | class IpAddress(db.Model): 126 | id = db.Column(db.Integer, primary_key=True) 127 | interface_id = db.Column(db.Integer, db.ForeignKey('interface.id')) 128 | interface = db.relationship(Interface, back_populates="address") 129 | 130 | route_only = db.Column(db.Boolean, default=False) 131 | 132 | version = db.Column(db.Integer, nullable=False) # 4 or 6 133 | mask = db.Column(db.Integer, nullable=False) # ip address mask 134 | _address0 = db.Column(db.Integer) # ipv6 32bit msb 135 | _address1 = db.Column(db.Integer) # ipv6 32bit 136 | _address2 = db.Column(db.Integer) # ipv6 32bit 137 | _address3 = db.Column(db.Integer, nullable=False) # ipv6/ipv4 32bit 138 | 139 | @hybrid_property 140 | def address(self): 141 | if self.version == 4: 142 | return ipaddress.IPv4Interface((self._address3, self.mask)) 143 | else: 144 | addr = b''.join(( 145 | self._address0.to_bytes(4, byteorder='big'), 146 | self._address1.to_bytes(4, byteorder='big'), 147 | self._address2.to_bytes(4, byteorder='big'), 148 | self._address3.to_bytes(4, byteorder='big'), 149 | )) 150 | return ipaddress.IPv6Interface((addr, self.mask)) 151 | 152 | @address.setter 153 | def address(self, value): 154 | if value.version == 4: 155 | self.version = 4 156 | self.mask = value.network.prefixlen 157 | self._address3 = int.from_bytes(value.packed, byteorder='big') 158 | elif value.version == 6: 159 | self.version = 6 160 | self.mask = value.network.prefixlen 161 | self._address0 = int.from_bytes(value.packed[0:4], byteorder='big') 162 | self._address1 = int.from_bytes(value.packed[4:8], byteorder='big') 163 | self._address2 = int.from_bytes(value.packed[8:12], byteorder='big') 164 | self._address3 = int.from_bytes(value.packed[12:16], byteorder='big') 165 | else: 166 | raise ValueError("Only IPv4Network or IPv6Network is supported") 167 | 168 | def __repr__(self): 169 | return ''.format(self.id, self.address) 170 | 171 | 172 | class User(db.Model): 173 | id = db.Column(db.Integer, primary_key=True) 174 | username = db.Column(db.String, unique=True, nullable=False) 175 | _password = db.Column("password", db.String, nullable=False) 176 | 177 | @hybrid_property 178 | def password(self): 179 | return self._password 180 | 181 | @password.setter 182 | def password(self, value): 183 | """Store the password as a hash for security.""" 184 | self._password = generate_password_hash(value) 185 | 186 | def check_password(self, value): 187 | return check_password_hash(self.password, value) 188 | 189 | def __repr__(self): 190 | return ''.format(self.id, self.username) 191 | -------------------------------------------------------------------------------- /wgadmin/static/qr.js: -------------------------------------------------------------------------------- 1 | /* qr.js -- QR code generator in Javascript (revision 2011-01-19) 2 | * Written by Kang Seonghoon . 3 | * 4 | * This source code is in the public domain; if your jurisdiction does not 5 | * recognize the public domain the terms of Creative Commons CC0 license 6 | * apply. In the other words, you can always do what you want. 7 | */ 8 | (function(root, name, definition) { 9 | if (typeof define === 'function' && define.amd) { 10 | define([], definition); 11 | } else if (typeof module === 'object' && module.exports) { 12 | module.exports = definition(); 13 | } else { 14 | root[name] = definition(); 15 | } 16 | })(this, 'QRCode', function() { 17 | /* Quick overview: QR code composed of 2D array of modules (a rectangular 18 | * area that conveys one bit of information); some modules are fixed to help 19 | * the recognition of the code, and remaining data modules are further divided 20 | * into 8-bit code words which are augumented by Reed-Solomon error correcting 21 | * codes (ECC). There could be multiple ECCs, in the case the code is so large 22 | * that it is helpful to split the raw data into several chunks. 23 | * 24 | * The number of modules is determined by the code's "version", ranging from 1 25 | * (21x21) to 40 (177x177). How many ECC bits are used is determined by the 26 | * ECC level (L/M/Q/H). The number and size (and thus the order of generator 27 | * polynomial) of ECCs depend to the version and ECC level. 28 | */ 29 | 30 | // per-version information (cf. JIS X 0510:2004 pp. 30--36, 71) 31 | // 32 | // [0]: the degree of generator polynomial by ECC levels 33 | // [1]: # of code blocks by ECC levels 34 | // [2]: left-top positions of alignment patterns 35 | // 36 | // the number in this table (in particular, [0]) does not exactly match with 37 | // the numbers in the specficiation. see augumenteccs below for the reason. 38 | var VERSIONS = [ 39 | null, 40 | [[10, 7,17,13], [ 1, 1, 1, 1], []], 41 | [[16,10,28,22], [ 1, 1, 1, 1], [4,16]], 42 | [[26,15,22,18], [ 1, 1, 2, 2], [4,20]], 43 | [[18,20,16,26], [ 2, 1, 4, 2], [4,24]], 44 | [[24,26,22,18], [ 2, 1, 4, 4], [4,28]], 45 | [[16,18,28,24], [ 4, 2, 4, 4], [4,32]], 46 | [[18,20,26,18], [ 4, 2, 5, 6], [4,20,36]], 47 | [[22,24,26,22], [ 4, 2, 6, 6], [4,22,40]], 48 | [[22,30,24,20], [ 5, 2, 8, 8], [4,24,44]], 49 | [[26,18,28,24], [ 5, 4, 8, 8], [4,26,48]], 50 | [[30,20,24,28], [ 5, 4,11, 8], [4,28,52]], 51 | [[22,24,28,26], [ 8, 4,11,10], [4,30,56]], 52 | [[22,26,22,24], [ 9, 4,16,12], [4,32,60]], 53 | [[24,30,24,20], [ 9, 4,16,16], [4,24,44,64]], 54 | [[24,22,24,30], [10, 6,18,12], [4,24,46,68]], 55 | [[28,24,30,24], [10, 6,16,17], [4,24,48,72]], 56 | [[28,28,28,28], [11, 6,19,16], [4,28,52,76]], 57 | [[26,30,28,28], [13, 6,21,18], [4,28,54,80]], 58 | [[26,28,26,26], [14, 7,25,21], [4,28,56,84]], 59 | [[26,28,28,30], [16, 8,25,20], [4,32,60,88]], 60 | [[26,28,30,28], [17, 8,25,23], [4,26,48,70,92]], 61 | [[28,28,24,30], [17, 9,34,23], [4,24,48,72,96]], 62 | [[28,30,30,30], [18, 9,30,25], [4,28,52,76,100]], 63 | [[28,30,30,30], [20,10,32,27], [4,26,52,78,104]], 64 | [[28,26,30,30], [21,12,35,29], [4,30,56,82,108]], 65 | [[28,28,30,28], [23,12,37,34], [4,28,56,84,112]], 66 | [[28,30,30,30], [25,12,40,34], [4,32,60,88,116]], 67 | [[28,30,30,30], [26,13,42,35], [4,24,48,72,96,120]], 68 | [[28,30,30,30], [28,14,45,38], [4,28,52,76,100,124]], 69 | [[28,30,30,30], [29,15,48,40], [4,24,50,76,102,128]], 70 | [[28,30,30,30], [31,16,51,43], [4,28,54,80,106,132]], 71 | [[28,30,30,30], [33,17,54,45], [4,32,58,84,110,136]], 72 | [[28,30,30,30], [35,18,57,48], [4,28,56,84,112,140]], 73 | [[28,30,30,30], [37,19,60,51], [4,32,60,88,116,144]], 74 | [[28,30,30,30], [38,19,63,53], [4,28,52,76,100,124,148]], 75 | [[28,30,30,30], [40,20,66,56], [4,22,48,74,100,126,152]], 76 | [[28,30,30,30], [43,21,70,59], [4,26,52,78,104,130,156]], 77 | [[28,30,30,30], [45,22,74,62], [4,30,56,82,108,134,160]], 78 | [[28,30,30,30], [47,24,77,65], [4,24,52,80,108,136,164]], 79 | [[28,30,30,30], [49,25,81,68], [4,28,56,84,112,140,168]]]; 80 | 81 | // mode constants (cf. Table 2 in JIS X 0510:2004 p. 16) 82 | var MODE_TERMINATOR = 0; 83 | var MODE_NUMERIC = 1, MODE_ALPHANUMERIC = 2, MODE_OCTET = 4, MODE_KANJI = 8; 84 | 85 | // validation regexps 86 | var NUMERIC_REGEXP = /^\d*$/; 87 | var ALPHANUMERIC_REGEXP = /^[A-Za-z0-9 $%*+\-./:]*$/; 88 | var ALPHANUMERIC_OUT_REGEXP = /^[A-Z0-9 $%*+\-./:]*$/; 89 | 90 | // ECC levels (cf. Table 22 in JIS X 0510:2004 p. 45) 91 | var ECCLEVEL_L = 1, ECCLEVEL_M = 0, ECCLEVEL_Q = 3, ECCLEVEL_H = 2; 92 | 93 | // GF(2^8)-to-integer mapping with a reducing polynomial x^8+x^4+x^3+x^2+1 94 | // invariant: GF256_MAP[GF256_INVMAP[i]] == i for all i in [1,256) 95 | var GF256_MAP = [], GF256_INVMAP = [-1]; 96 | for (var i = 0, v = 1; i < 255; ++i) { 97 | GF256_MAP.push(v); 98 | GF256_INVMAP[v] = i; 99 | v = (v * 2) ^ (v >= 128 ? 0x11d : 0); 100 | } 101 | 102 | // generator polynomials up to degree 30 103 | // (should match with polynomials in JIS X 0510:2004 Appendix A) 104 | // 105 | // generator polynomial of degree K is product of (x-\alpha^0), (x-\alpha^1), 106 | // ..., (x-\alpha^(K-1)). by convention, we omit the K-th coefficient (always 1) 107 | // from the result; also other coefficients are written in terms of the exponent 108 | // to \alpha to avoid the redundant calculation. (see also calculateecc below.) 109 | var GF256_GENPOLY = [[]]; 110 | for (var i = 0; i < 30; ++i) { 111 | var prevpoly = GF256_GENPOLY[i], poly = []; 112 | for (var j = 0; j <= i; ++j) { 113 | var a = (j < i ? GF256_MAP[prevpoly[j]] : 0); 114 | var b = GF256_MAP[(i + (prevpoly[j-1] || 0)) % 255]; 115 | poly.push(GF256_INVMAP[a ^ b]); 116 | } 117 | GF256_GENPOLY.push(poly); 118 | } 119 | 120 | // alphanumeric character mapping (cf. Table 5 in JIS X 0510:2004 p. 19) 121 | var ALPHANUMERIC_MAP = {}; 122 | for (var i = 0; i < 45; ++i) { 123 | ALPHANUMERIC_MAP['0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'.charAt(i)] = i; 124 | } 125 | 126 | // mask functions in terms of row # and column # 127 | // (cf. Table 20 in JIS X 0510:2004 p. 42) 128 | var MASKFUNCS = [ 129 | function(i,j) { return (i+j) % 2 == 0; }, 130 | function(i,j) { return i % 2 == 0; }, 131 | function(i,j) { return j % 3 == 0; }, 132 | function(i,j) { return (i+j) % 3 == 0; }, 133 | function(i,j) { return (((i/2)|0) + ((j/3)|0)) % 2 == 0; }, 134 | function(i,j) { return (i*j) % 2 + (i*j) % 3 == 0; }, 135 | function(i,j) { return ((i*j) % 2 + (i*j) % 3) % 2 == 0; }, 136 | function(i,j) { return ((i+j) % 2 + (i*j) % 3) % 2 == 0; }]; 137 | 138 | // returns true when the version information has to be embeded. 139 | var needsverinfo = function(ver) { return ver > 6; }; 140 | 141 | // returns the size of entire QR code for given version. 142 | var getsizebyver = function(ver) { return 4 * ver + 17; }; 143 | 144 | // returns the number of bits available for code words in this version. 145 | var nfullbits = function(ver) { 146 | /* 147 | * |<--------------- n --------------->| 148 | * | |<----- n-17 ---->| | 149 | * +-------+ ///+-------+ ---- 150 | * | | ///| | ^ 151 | * | 9x9 | @@@@@ ///| 9x8 | | 152 | * | | # # # @5x5@ # # # | | | 153 | * +-------+ @@@@@ +-------+ | 154 | * # ---| 155 | * ^ | 156 | * # | 157 | * @@@@@ @@@@@ @@@@@ | n 158 | * @5x5@ @5x5@ @5x5@ n-17 159 | * @@@@@ @@@@@ @@@@@ | | 160 | * # | | 161 | * ////// v | 162 | * //////# ---| 163 | * +-------+ @@@@@ @@@@@ | 164 | * | | @5x5@ @5x5@ | 165 | * | 8x9 | @@@@@ @@@@@ | 166 | * | | v 167 | * +-------+ ---- 168 | * 169 | * when the entire code has n^2 modules and there are m^2-3 alignment 170 | * patterns, we have: 171 | * - 225 (= 9x9 + 9x8 + 8x9) modules for finder patterns and 172 | * format information; 173 | * - 2n-34 (= 2(n-17)) modules for timing patterns; 174 | * - 36 (= 3x6 + 6x3) modules for version information, if any; 175 | * - 25m^2-75 (= (m^2-3)(5x5)) modules for alignment patterns 176 | * if any, but 10m-20 (= 2(m-2)x5) of them overlaps with 177 | * timing patterns. 178 | */ 179 | var v = VERSIONS[ver]; 180 | var nbits = 16*ver*ver + 128*ver + 64; // finder, timing and format info. 181 | if (needsverinfo(ver)) nbits -= 36; // version information 182 | if (v[2].length) { // alignment patterns 183 | nbits -= 25 * v[2].length * v[2].length - 10 * v[2].length - 55; 184 | } 185 | return nbits; 186 | }; 187 | 188 | // returns the number of bits available for data portions (i.e. excludes ECC 189 | // bits but includes mode and length bits) in this version and ECC level. 190 | var ndatabits = function(ver, ecclevel) { 191 | var nbits = nfullbits(ver) & ~7; // no sub-octet code words 192 | var v = VERSIONS[ver]; 193 | nbits -= 8 * v[0][ecclevel] * v[1][ecclevel]; // ecc bits 194 | return nbits; 195 | } 196 | 197 | // returns the number of bits required for the length of data. 198 | // (cf. Table 3 in JIS X 0510:2004 p. 16) 199 | var ndatalenbits = function(ver, mode) { 200 | switch (mode) { 201 | case MODE_NUMERIC: return (ver < 10 ? 10 : ver < 27 ? 12 : 14); 202 | case MODE_ALPHANUMERIC: return (ver < 10 ? 9 : ver < 27 ? 11 : 13); 203 | case MODE_OCTET: return (ver < 10 ? 8 : 16); 204 | case MODE_KANJI: return (ver < 10 ? 8 : ver < 27 ? 10 : 12); 205 | } 206 | }; 207 | 208 | // returns the maximum length of data possible in given configuration. 209 | var getmaxdatalen = function(ver, mode, ecclevel) { 210 | var nbits = ndatabits(ver, ecclevel) - 4 - ndatalenbits(ver, mode); // 4 for mode bits 211 | switch (mode) { 212 | case MODE_NUMERIC: 213 | return ((nbits/10) | 0) * 3 + (nbits%10 < 4 ? 0 : nbits%10 < 7 ? 1 : 2); 214 | case MODE_ALPHANUMERIC: 215 | return ((nbits/11) | 0) * 2 + (nbits%11 < 6 ? 0 : 1); 216 | case MODE_OCTET: 217 | return (nbits/8) | 0; 218 | case MODE_KANJI: 219 | return (nbits/13) | 0; 220 | } 221 | }; 222 | 223 | // checks if the given data can be encoded in given mode, and returns 224 | // the converted data for the further processing if possible. otherwise 225 | // returns null. 226 | // 227 | // this function does not check the length of data; it is a duty of 228 | // encode function below (as it depends on the version and ECC level too). 229 | var validatedata = function(mode, data) { 230 | switch (mode) { 231 | case MODE_NUMERIC: 232 | if (!data.match(NUMERIC_REGEXP)) return null; 233 | return data; 234 | 235 | case MODE_ALPHANUMERIC: 236 | if (!data.match(ALPHANUMERIC_REGEXP)) return null; 237 | return data.toUpperCase(); 238 | 239 | case MODE_OCTET: 240 | if (typeof data === 'string') { // encode as utf-8 string 241 | var newdata = []; 242 | for (var i = 0; i < data.length; ++i) { 243 | var ch = data.charCodeAt(i); 244 | if (ch < 0x80) { 245 | newdata.push(ch); 246 | } else if (ch < 0x800) { 247 | newdata.push(0xc0 | (ch >> 6), 248 | 0x80 | (ch & 0x3f)); 249 | } else if (ch < 0x10000) { 250 | newdata.push(0xe0 | (ch >> 12), 251 | 0x80 | ((ch >> 6) & 0x3f), 252 | 0x80 | (ch & 0x3f)); 253 | } else { 254 | newdata.push(0xf0 | (ch >> 18), 255 | 0x80 | ((ch >> 12) & 0x3f), 256 | 0x80 | ((ch >> 6) & 0x3f), 257 | 0x80 | (ch & 0x3f)); 258 | } 259 | } 260 | return newdata; 261 | } else { 262 | return data; 263 | } 264 | } 265 | }; 266 | 267 | // returns the code words (sans ECC bits) for given data and configurations. 268 | // requires data to be preprocessed by validatedata. no length check is 269 | // performed, and everything has to be checked before calling this function. 270 | var encode = function(ver, mode, data, maxbuflen) { 271 | var buf = []; 272 | var bits = 0, remaining = 8; 273 | var datalen = data.length; 274 | 275 | // this function is intentionally no-op when n=0. 276 | var pack = function(x, n) { 277 | if (n >= remaining) { 278 | buf.push(bits | (x >> (n -= remaining))); 279 | while (n >= 8) buf.push((x >> (n -= 8)) & 255); 280 | bits = 0; 281 | remaining = 8; 282 | } 283 | if (n > 0) bits |= (x & ((1 << n) - 1)) << (remaining -= n); 284 | }; 285 | 286 | var nlenbits = ndatalenbits(ver, mode); 287 | pack(mode, 4); 288 | pack(datalen, nlenbits); 289 | 290 | switch (mode) { 291 | case MODE_NUMERIC: 292 | for (var i = 2; i < datalen; i += 3) { 293 | pack(parseInt(data.substring(i-2,i+1), 10), 10); 294 | } 295 | pack(parseInt(data.substring(i-2), 10), [0,4,7][datalen%3]); 296 | break; 297 | 298 | case MODE_ALPHANUMERIC: 299 | for (var i = 1; i < datalen; i += 2) { 300 | pack(ALPHANUMERIC_MAP[data.charAt(i-1)] * 45 + 301 | ALPHANUMERIC_MAP[data.charAt(i)], 11); 302 | } 303 | if (datalen % 2 == 1) { 304 | pack(ALPHANUMERIC_MAP[data.charAt(i-1)], 6); 305 | } 306 | break; 307 | 308 | case MODE_OCTET: 309 | for (var i = 0; i < datalen; ++i) { 310 | pack(data[i], 8); 311 | } 312 | break; 313 | }; 314 | 315 | // final bits. it is possible that adding terminator causes the buffer 316 | // to overflow, but then the buffer truncated to the maximum size will 317 | // be valid as the truncated terminator mode bits and padding is 318 | // identical in appearance (cf. JIS X 0510:2004 sec 8.4.8). 319 | pack(MODE_TERMINATOR, 4); 320 | if (remaining < 8) buf.push(bits); 321 | 322 | // the padding to fill up the remaining space. we should not add any 323 | // words when the overflow already occurred. 324 | while (buf.length + 1 < maxbuflen) buf.push(0xec, 0x11); 325 | if (buf.length < maxbuflen) buf.push(0xec); 326 | return buf; 327 | }; 328 | 329 | // calculates ECC code words for given code words and generator polynomial. 330 | // 331 | // this is quite similar to CRC calculation as both Reed-Solomon and CRC use 332 | // the certain kind of cyclic codes, which is effectively the division of 333 | // zero-augumented polynomial by the generator polynomial. the only difference 334 | // is that Reed-Solomon uses GF(2^8), instead of CRC's GF(2), and Reed-Solomon 335 | // uses the different generator polynomial than CRC's. 336 | var calculateecc = function(poly, genpoly) { 337 | var modulus = poly.slice(0); 338 | var polylen = poly.length, genpolylen = genpoly.length; 339 | for (var i = 0; i < genpolylen; ++i) modulus.push(0); 340 | for (var i = 0; i < polylen; ) { 341 | var quotient = GF256_INVMAP[modulus[i++]]; 342 | if (quotient >= 0) { 343 | for (var j = 0; j < genpolylen; ++j) { 344 | modulus[i+j] ^= GF256_MAP[(quotient + genpoly[j]) % 255]; 345 | } 346 | } 347 | } 348 | return modulus.slice(polylen); 349 | }; 350 | 351 | // auguments ECC code words to given code words. the resulting words are 352 | // ready to be encoded in the matrix. 353 | // 354 | // the much of actual augumenting procedure follows JIS X 0510:2004 sec 8.7. 355 | // the code is simplified using the fact that the size of each code & ECC 356 | // blocks is almost same; for example, when we have 4 blocks and 46 data words 357 | // the number of code words in those blocks are 11, 11, 12, 12 respectively. 358 | var augumenteccs = function(poly, nblocks, genpoly) { 359 | var subsizes = []; 360 | var subsize = (poly.length / nblocks) | 0, subsize0 = 0; 361 | var pivot = nblocks - poly.length % nblocks; 362 | for (var i = 0; i < pivot; ++i) { 363 | subsizes.push(subsize0); 364 | subsize0 += subsize; 365 | } 366 | for (var i = pivot; i < nblocks; ++i) { 367 | subsizes.push(subsize0); 368 | subsize0 += subsize+1; 369 | } 370 | subsizes.push(subsize0); 371 | 372 | var eccs = []; 373 | for (var i = 0; i < nblocks; ++i) { 374 | eccs.push(calculateecc(poly.slice(subsizes[i], subsizes[i+1]), genpoly)); 375 | } 376 | 377 | var result = []; 378 | var nitemsperblock = (poly.length / nblocks) | 0; 379 | for (var i = 0; i < nitemsperblock; ++i) { 380 | for (var j = 0; j < nblocks; ++j) { 381 | result.push(poly[subsizes[j] + i]); 382 | } 383 | } 384 | for (var j = pivot; j < nblocks; ++j) { 385 | result.push(poly[subsizes[j+1] - 1]); 386 | } 387 | for (var i = 0; i < genpoly.length; ++i) { 388 | for (var j = 0; j < nblocks; ++j) { 389 | result.push(eccs[j][i]); 390 | } 391 | } 392 | return result; 393 | }; 394 | 395 | // auguments BCH(p+q,q) code to the polynomial over GF(2), given the proper 396 | // genpoly. the both input and output are in binary numbers, and unlike 397 | // calculateecc genpoly should include the 1 bit for the highest degree. 398 | // 399 | // actual polynomials used for this procedure are as follows: 400 | // - p=10, q=5, genpoly=x^10+x^8+x^5+x^4+x^2+x+1 (JIS X 0510:2004 Appendix C) 401 | // - p=18, q=6, genpoly=x^12+x^11+x^10+x^9+x^8+x^5+x^2+1 (ibid. Appendix D) 402 | var augumentbch = function(poly, p, genpoly, q) { 403 | var modulus = poly << q; 404 | for (var i = p - 1; i >= 0; --i) { 405 | if ((modulus >> (q+i)) & 1) modulus ^= genpoly << i; 406 | } 407 | return (poly << q) | modulus; 408 | }; 409 | 410 | // creates the base matrix for given version. it returns two matrices, one of 411 | // them is the actual one and the another represents the "reserved" portion 412 | // (e.g. finder and timing patterns) of the matrix. 413 | // 414 | // some entries in the matrix may be undefined, rather than 0 or 1. this is 415 | // intentional (no initialization needed!), and putdata below will fill 416 | // the remaining ones. 417 | var makebasematrix = function(ver) { 418 | var v = VERSIONS[ver], n = getsizebyver(ver); 419 | var matrix = [], reserved = []; 420 | for (var i = 0; i < n; ++i) { 421 | matrix.push([]); 422 | reserved.push([]); 423 | } 424 | 425 | var blit = function(y, x, h, w, bits) { 426 | for (var i = 0; i < h; ++i) { 427 | for (var j = 0; j < w; ++j) { 428 | matrix[y+i][x+j] = (bits[i] >> j) & 1; 429 | reserved[y+i][x+j] = 1; 430 | } 431 | } 432 | }; 433 | 434 | // finder patterns and a part of timing patterns 435 | // will also mark the format information area (not yet written) as reserved. 436 | blit(0, 0, 9, 9, [0x7f, 0x41, 0x5d, 0x5d, 0x5d, 0x41, 0x17f, 0x00, 0x40]); 437 | blit(n-8, 0, 8, 9, [0x100, 0x7f, 0x41, 0x5d, 0x5d, 0x5d, 0x41, 0x7f]); 438 | blit(0, n-8, 9, 8, [0xfe, 0x82, 0xba, 0xba, 0xba, 0x82, 0xfe, 0x00, 0x00]); 439 | 440 | // the rest of timing patterns 441 | for (var i = 9; i < n-8; ++i) { 442 | matrix[6][i] = matrix[i][6] = ~i & 1; 443 | reserved[6][i] = reserved[i][6] = 1; 444 | } 445 | 446 | // alignment patterns 447 | var aligns = v[2], m = aligns.length; 448 | for (var i = 0; i < m; ++i) { 449 | var minj = (i==0 || i==m-1 ? 1 : 0), maxj = (i==0 ? m-1 : m); 450 | for (var j = minj; j < maxj; ++j) { 451 | blit(aligns[i], aligns[j], 5, 5, [0x1f, 0x11, 0x15, 0x11, 0x1f]); 452 | } 453 | } 454 | 455 | // version information 456 | if (needsverinfo(ver)) { 457 | var code = augumentbch(ver, 6, 0x1f25, 12); 458 | var k = 0; 459 | for (var i = 0; i < 6; ++i) { 460 | for (var j = 0; j < 3; ++j) { 461 | matrix[i][(n-11)+j] = matrix[(n-11)+j][i] = (code >> k++) & 1; 462 | reserved[i][(n-11)+j] = reserved[(n-11)+j][i] = 1; 463 | } 464 | } 465 | } 466 | 467 | return {matrix: matrix, reserved: reserved}; 468 | }; 469 | 470 | // fills the data portion (i.e. unmarked in reserved) of the matrix with given 471 | // code words. the size of code words should be no more than available bits, 472 | // and remaining bits are padded to 0 (cf. JIS X 0510:2004 sec 8.7.3). 473 | var putdata = function(matrix, reserved, buf) { 474 | var n = matrix.length; 475 | var k = 0, dir = -1; 476 | for (var i = n-1; i >= 0; i -= 2) { 477 | if (i == 6) --i; // skip the entire timing pattern column 478 | var jj = (dir < 0 ? n-1 : 0); 479 | for (var j = 0; j < n; ++j) { 480 | for (var ii = i; ii > i-2; --ii) { 481 | if (!reserved[jj][ii]) { 482 | // may overflow, but (undefined >> x) 483 | // is 0 so it will auto-pad to zero. 484 | matrix[jj][ii] = (buf[k >> 3] >> (~k&7)) & 1; 485 | ++k; 486 | } 487 | } 488 | jj += dir; 489 | } 490 | dir = -dir; 491 | } 492 | return matrix; 493 | }; 494 | 495 | // XOR-masks the data portion of the matrix. repeating the call with the same 496 | // arguments will revert the prior call (convenient in the matrix evaluation). 497 | var maskdata = function(matrix, reserved, mask) { 498 | var maskf = MASKFUNCS[mask]; 499 | var n = matrix.length; 500 | for (var i = 0; i < n; ++i) { 501 | for (var j = 0; j < n; ++j) { 502 | if (!reserved[i][j]) matrix[i][j] ^= maskf(i,j); 503 | } 504 | } 505 | return matrix; 506 | } 507 | 508 | // puts the format information. 509 | var putformatinfo = function(matrix, reserved, ecclevel, mask) { 510 | var n = matrix.length; 511 | var code = augumentbch((ecclevel << 3) | mask, 5, 0x537, 10) ^ 0x5412; 512 | for (var i = 0; i < 15; ++i) { 513 | var r = [0,1,2,3,4,5,7,8,n-7,n-6,n-5,n-4,n-3,n-2,n-1][i]; 514 | var c = [n-1,n-2,n-3,n-4,n-5,n-6,n-7,n-8,7,5,4,3,2,1,0][i]; 515 | matrix[r][8] = matrix[8][c] = (code >> i) & 1; 516 | // we don't have to mark those bits reserved; always done 517 | // in makebasematrix above. 518 | } 519 | return matrix; 520 | }; 521 | 522 | // evaluates the resulting matrix and returns the score (lower is better). 523 | // (cf. JIS X 0510:2004 sec 8.8.2) 524 | // 525 | // the evaluation procedure tries to avoid the problematic patterns naturally 526 | // occuring from the original matrix. for example, it penaltizes the patterns 527 | // which just look like the finder pattern which will confuse the decoder. 528 | // we choose the mask which results in the lowest score among 8 possible ones. 529 | // 530 | // note: zxing seems to use the same procedure and in many cases its choice 531 | // agrees to ours, but sometimes it does not. practically it doesn't matter. 532 | var evaluatematrix = function(matrix) { 533 | // N1+(k-5) points for each consecutive row of k same-colored modules, 534 | // where k >= 5. no overlapping row counts. 535 | var PENALTY_CONSECUTIVE = 3; 536 | // N2 points for each 2x2 block of same-colored modules. 537 | // overlapping block does count. 538 | var PENALTY_TWOBYTWO = 3; 539 | // N3 points for each pattern with >4W:1B:1W:3B:1W:1B or 540 | // 1B:1W:3B:1W:1B:>4W, or their multiples (e.g. highly unlikely, 541 | // but 13W:3B:3W:9B:3W:3B counts). 542 | var PENALTY_FINDERLIKE = 40; 543 | // N4*k points for every (5*k)% deviation from 50% black density. 544 | // i.e. k=1 for 55~60% and 40~45%, k=2 for 60~65% and 35~40%, etc. 545 | var PENALTY_DENSITY = 10; 546 | 547 | var evaluategroup = function(groups) { // assumes [W,B,W,B,W,...,B,W] 548 | var score = 0; 549 | for (var i = 0; i < groups.length; ++i) { 550 | if (groups[i] >= 5) score += PENALTY_CONSECUTIVE + (groups[i]-5); 551 | } 552 | for (var i = 5; i < groups.length; i += 2) { 553 | var p = groups[i]; 554 | if (groups[i-1] == p && groups[i-2] == 3*p && groups[i-3] == p && 555 | groups[i-4] == p && (groups[i-5] >= 4*p || groups[i+1] >= 4*p)) { 556 | // this part differs from zxing... 557 | score += PENALTY_FINDERLIKE; 558 | } 559 | } 560 | return score; 561 | }; 562 | 563 | var n = matrix.length; 564 | var score = 0, nblacks = 0; 565 | for (var i = 0; i < n; ++i) { 566 | var row = matrix[i]; 567 | var groups; 568 | 569 | // evaluate the current row 570 | groups = [0]; // the first empty group of white 571 | for (var j = 0; j < n; ) { 572 | var k; 573 | for (k = 0; j < n && row[j]; ++k) ++j; 574 | groups.push(k); 575 | for (k = 0; j < n && !row[j]; ++k) ++j; 576 | groups.push(k); 577 | } 578 | score += evaluategroup(groups); 579 | 580 | // evaluate the current column 581 | groups = [0]; 582 | for (var j = 0; j < n; ) { 583 | var k; 584 | for (k = 0; j < n && matrix[j][i]; ++k) ++j; 585 | groups.push(k); 586 | for (k = 0; j < n && !matrix[j][i]; ++k) ++j; 587 | groups.push(k); 588 | } 589 | score += evaluategroup(groups); 590 | 591 | // check the 2x2 box and calculate the density 592 | var nextrow = matrix[i+1] || []; 593 | nblacks += row[0]; 594 | for (var j = 1; j < n; ++j) { 595 | var p = row[j]; 596 | nblacks += p; 597 | // at least comparison with next row should be strict... 598 | if (row[j-1] == p && nextrow[j] === p && nextrow[j-1] === p) { 599 | score += PENALTY_TWOBYTWO; 600 | } 601 | } 602 | } 603 | 604 | score += PENALTY_DENSITY * ((Math.abs(nblacks / n / n - 0.5) / 0.05) | 0); 605 | return score; 606 | }; 607 | 608 | // returns the fully encoded QR code matrix which contains given data. 609 | // it also chooses the best mask automatically when mask is -1. 610 | var generate = function(data, ver, mode, ecclevel, mask) { 611 | var v = VERSIONS[ver]; 612 | var buf = encode(ver, mode, data, ndatabits(ver, ecclevel) >> 3); 613 | buf = augumenteccs(buf, v[1][ecclevel], GF256_GENPOLY[v[0][ecclevel]]); 614 | 615 | var result = makebasematrix(ver); 616 | var matrix = result.matrix, reserved = result.reserved; 617 | putdata(matrix, reserved, buf); 618 | 619 | if (mask < 0) { 620 | // find the best mask 621 | maskdata(matrix, reserved, 0); 622 | putformatinfo(matrix, reserved, ecclevel, 0); 623 | var bestmask = 0, bestscore = evaluatematrix(matrix); 624 | maskdata(matrix, reserved, 0); 625 | for (mask = 1; mask < 8; ++mask) { 626 | maskdata(matrix, reserved, mask); 627 | putformatinfo(matrix, reserved, ecclevel, mask); 628 | var score = evaluatematrix(matrix); 629 | if (bestscore > score) { 630 | bestscore = score; 631 | bestmask = mask; 632 | } 633 | maskdata(matrix, reserved, mask); 634 | } 635 | mask = bestmask; 636 | } 637 | 638 | maskdata(matrix, reserved, mask); 639 | putformatinfo(matrix, reserved, ecclevel, mask); 640 | return matrix; 641 | }; 642 | 643 | // the public interface is trivial; the options available are as follows: 644 | // 645 | // - version: an integer in [1,40]. when omitted (or -1) the smallest possible 646 | // version is chosen. 647 | // - mode: one of 'numeric', 'alphanumeric', 'octet'. when omitted the smallest 648 | // possible mode is chosen. 649 | // - ecclevel: one of 'L', 'M', 'Q', 'H'. defaults to 'L'. 650 | // - mask: an integer in [0,7]. when omitted (or -1) the best mask is chosen. 651 | // 652 | // for generate{HTML,PNG}: 653 | // 654 | // - modulesize: a number. this is a size of each modules in pixels, and 655 | // defaults to 5px. 656 | // - margin: a number. this is a size of margin in *modules*, and defaults to 657 | // 4 (white modules). the specficiation mandates the margin no less than 4 658 | // modules, so it is better not to alter this value unless you know what 659 | // you're doing. 660 | var QRCode = { 661 | 'generate': function(data, options) { 662 | var MODES = {'numeric': MODE_NUMERIC, 'alphanumeric': MODE_ALPHANUMERIC, 663 | 'octet': MODE_OCTET}; 664 | var ECCLEVELS = {'L': ECCLEVEL_L, 'M': ECCLEVEL_M, 'Q': ECCLEVEL_Q, 665 | 'H': ECCLEVEL_H}; 666 | 667 | options = options || {}; 668 | var ver = options.version || -1; 669 | var ecclevel = ECCLEVELS[(options.ecclevel || 'L').toUpperCase()]; 670 | var mode = options.mode ? MODES[options.mode.toLowerCase()] : -1; 671 | var mask = 'mask' in options ? options.mask : -1; 672 | 673 | if (mode < 0) { 674 | if (typeof data === 'string') { 675 | if (data.match(NUMERIC_REGEXP)) { 676 | mode = MODE_NUMERIC; 677 | } else if (data.match(ALPHANUMERIC_OUT_REGEXP)) { 678 | // while encode supports case-insensitive 679 | // encoding, we restrict the data to be 680 | // uppercased when auto-selecting the mode. 681 | mode = MODE_ALPHANUMERIC; 682 | } else { 683 | mode = MODE_OCTET; 684 | } 685 | } else { 686 | mode = MODE_OCTET; 687 | } 688 | } else if (!(mode == MODE_NUMERIC || mode == MODE_ALPHANUMERIC || 689 | mode == MODE_OCTET)) { 690 | throw 'invalid or unsupported mode'; 691 | } 692 | 693 | data = validatedata(mode, data); 694 | if (data === null) throw 'invalid data format'; 695 | 696 | if (ecclevel < 0 || ecclevel > 3) throw 'invalid ECC level'; 697 | 698 | if (ver < 0) { 699 | for (ver = 1; ver <= 40; ++ver) { 700 | if (data.length <= getmaxdatalen(ver, mode, ecclevel)) break; 701 | } 702 | if (ver > 40) throw 'too large data'; 703 | } else if (ver < 1 || ver > 40) { 704 | throw 'invalid version'; 705 | } 706 | 707 | if (mask != -1 && (mask < 0 || mask > 8)) throw 'invalid mask'; 708 | 709 | return generate(data, ver, mode, ecclevel, mask); 710 | }, 711 | 712 | 'generateHTML': function(data, options) { 713 | options = options || {}; 714 | var matrix = QRCode['generate'](data, options); 715 | var modsize = Math.max(options.modulesize || 5, 0.5); 716 | var margin = Math.max(options.margin !== null ? options.margin : 4, 0.0); 717 | 718 | var e = document.createElement('div'); 719 | var n = matrix.length; 720 | var html = ['']; 722 | for (var i = 0; i < n; ++i) { 723 | html.push(''); 724 | for (var j = 0; j < n; ++j) { 725 | html.push(''); 727 | } 728 | html.push(''); 729 | } 730 | e.className = 'qrcode'; 731 | e.innerHTML = html.join('') + '
'; 732 | return e; 733 | }, 734 | 735 | 'generateSVG': function(data, options) { 736 | options = options || {}; 737 | var matrix = QRCode['generate'](data, options); 738 | var n = matrix.length; 739 | var modsize = Math.max(options.modulesize || 5, 0.5); 740 | var margin = Math.max(options.margin? options.margin : 4, 0.0); 741 | var size = modsize * (n + 2 * margin); 742 | 743 | var common = ' class= "fg"'+' width="'+modsize+'" height="'+modsize+'"/>'; 744 | 745 | var e = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 746 | e.setAttribute('viewBox', '0 0 '+size+' '+size); 747 | e.setAttribute('style', 'shape-rendering:crispEdges'); 748 | 749 | var svg = [ 750 | '', 751 | '', 753 | ]; 754 | 755 | var yo = margin * modsize; 756 | for (var y = 0; y < n; ++y) { 757 | var xo = margin * modsize; 758 | for (var x = 0; x < n; ++x) { 759 | if (matrix[y][x]) 760 | svg.push('