├── 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 |
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 |
10 | List
11 | Add
12 |
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 |
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 |
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 |
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 |
13 | {% for error in field.errors %}
14 | {{ error }}
15 | {% endfor %}
16 |
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 |
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 | 
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 |
13 | {% if g.user %}
14 | {{ g.user['username'] }}
15 | Log Out
16 | {% else %}
17 | Register
18 | Log In
19 | {% endif %}
20 |
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 | Name
14 | Owner
15 | Peers
16 |
17 |
18 |
19 | {% for interface in ifaces %}
20 |
21 | {{ interface }}
22 | arti
23 | {{ interface.slaves.__len__() }}/{{ interface.masters.__len__() }}
24 |
25 |
26 |
27 | {% for addr in interface.address %}
28 | {{ addr.address }},
29 | {% else %}
30 | No Addresses configured
31 | {% endfor %}
32 |
33 |
34 | Edit
35 |
36 |
37 | {% endfor %}
38 |
39 |
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 | Name
15 | Action
16 |
17 |
18 |
19 | {% for iface in ifaces %}
20 |
21 | {{ iface }}
22 |
23 |
27 |
28 |
29 |
30 |
31 | {% for addr in iface.address %}
32 | {{ addr.address }},
33 | {% else %}
34 | No Addresses configured
35 | {% endfor %}
36 |
37 |
38 | {% else %}
39 |
40 | No Interfaces available
41 |
42 | {% endfor %}
43 |
44 |
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 |
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 | Address
10 | Action
11 |
12 |
13 |
14 | {% for addr in iface.address %}
15 |
16 | {{ addr.address }}
17 |
18 |
24 |
25 |
26 | {% else %}
27 |
28 |
29 | No Addresses configured
30 |
31 |
32 | {% endfor %}
33 |
34 |
35 |
36 | Add address to the interface
37 |
38 |
45 |
46 |
47 |
48 |
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 | Route
62 | Action
63 |
64 |
65 |
66 | {% for addr in iface.route %}
67 |
68 | {{ addr.address }}
69 |
70 |
76 |
77 |
78 | {% else %}
79 |
80 |
81 | No routes configured
82 |
83 |
84 | {% endfor %}
85 |
86 |
87 |
88 | Add routes to the interface
89 |
90 |
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 | Host
10 | Action
11 |
12 |
13 |
14 | {% for peer in iface.fully_linked_peers %}
15 |
16 | {{ peer.slave }}
17 |
18 |
19 |
20 | {% for addr in peer.slave.allowed_ips %}
21 | {{ addr.address }},
22 | {% else %}
23 | No Addresses configured
24 | {% endfor %}
25 |
26 |
27 |
30 |
35 |
36 |
37 | {% else %}
38 |
39 | No peers configured
40 |
41 | {% endfor %}
42 |
43 |
44 |
47 |
48 |
49 | Half linked peers
50 | Outgoing links
51 |
52 |
53 |
54 | Host
55 | Action
56 |
57 |
58 |
59 | {% for peer in iface.outgoing_peers %}
60 |
61 | {{ peer.slave }}
62 |
63 |
64 |
65 | {% for addr in peer.slave.allowed_ips %}
66 | {{ addr.address }},
67 | {% else %}
68 | No Addresses configured
69 | {% endfor %}
70 |
71 |
72 |
75 |
80 |
81 |
82 | {% else %}
83 |
84 | No peers configured
85 |
86 | {% endfor %}
87 |
88 |
89 |
90 | Incoming links
91 |
92 |
93 |
94 | Host
95 | Action
96 |
97 |
98 |
99 | {% for peer in iface.incoming_peers %}
100 |
101 | {{ peer.master }}
102 |
103 |
104 |
105 | {% for addr in peer.master.allowed_ips %}
106 | {{ addr.address }},
107 | {% else %}
108 | No Addresses configured
109 | {% endfor %}
110 |
111 |
112 |
115 |
119 |
120 |
121 | {% else %}
122 |
123 | No peers configured
124 |
125 | {% endfor %}
126 |
127 |
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('