diff --git a/.gitignore b/.gitignore index 0eb1867..6b179f5 100755 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ # testing /frontend/coverage +/backend/db/ /backend/db/firegex.db /backend/db/firegex.db-journal /backend/modules/cppqueue diff --git a/Dockerfile b/Dockerfile index fba2f7c..3e3d418 100755 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM python:slim-bullseye RUN apt-get update && apt-get -y install \ - build-essential git iptables libpcre2-dev\ + build-essential git python3-nftables libpcre2-dev\ libnetfilter-queue-dev libtins-dev\ libnfnetlink-dev libmnl-dev diff --git a/backend/app.py b/backend/app.py index 1a4f2f0..de52210 100644 --- a/backend/app.py +++ b/backend/app.py @@ -9,10 +9,9 @@ from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from jose import JWTError, jwt from passlib.context import CryptContext from fastapi_socketio import SocketManager -from ipaddress import ip_interface from modules import SQLite, FirewallManager from modules.firewall import STATUS -from utils import refactor_name, gen_service_id +from utils import ip_parse, refactor_name, gen_service_id ON_DOCKER = len(sys.argv) > 1 and sys.argv[1] == "DOCKER" DEBUG = len(sys.argv) > 1 and sys.argv[1] == "DEBUG" @@ -54,8 +53,10 @@ async def startup_event(): @app.on_event("shutdown") async def shutdown_event(): + db.backup() await firewall.close() db.disconnect() + db.restore() def create_access_token(data: dict): to_encode = data.copy() @@ -350,11 +351,8 @@ class ServiceAddResponse(BaseModel): @app.post('/api/services/add', response_model=ServiceAddResponse) async def add_new_service(form: ServiceAddForm, auth: bool = Depends(is_loggined)): """Add a new service""" - ipv6 = None try: - ip_int = ip_interface(form.ip_int) - ipv6 = ip_int.version == 6 - form.ip_int = str(ip_int) + form.ip_int = ip_parse(form.ip_int) except ValueError: return {"status":"Invalid address"} if form.proto not in ["tcp", "udp"]: @@ -363,7 +361,7 @@ async def add_new_service(form: ServiceAddForm, auth: bool = Depends(is_loggined try: srv_id = gen_service_id(db) db.query("INSERT INTO services (service_id ,name, port, ipv6, status, proto, ip_int) VALUES (?, ?, ?, ?, ?, ?, ?)", - srv_id, refactor_name(form.name), form.port, ipv6, STATUS.STOP, form.proto, form.ip_int) + srv_id, refactor_name(form.name), form.port, True, STATUS.STOP, form.proto, form.ip_int) except sqlite3.IntegrityError: return {'status': 'This type of service already exists'} await firewall.reload() diff --git a/backend/modules/firegex.py b/backend/modules/firegex.py index 22d6162..cb0e778 100644 --- a/backend/modules/firegex.py +++ b/backend/modules/firegex.py @@ -1,113 +1,140 @@ from typing import Dict, List, Set -from ipaddress import ip_interface -from modules.iptables import IPTables +from utils import ip_parse, ip_family from modules.sqlite import Service import re, os, asyncio -import traceback +import traceback, nftables from modules.sqlite import Regex -class FilterTypes: - INPUT = "FIREGEX-INPUT" - OUTPUT = "FIREGEX-OUTPUT" - QUEUE_BASE_NUM = 1000 class FiregexFilter(): - def __init__(self, proto:str, port:int, ip_int:str, queue=None, target=None, id=None): - self.target = target + def __init__(self, proto:str, port:int, ip_int:str, queue=None, target:str=None, id=None): + self.nftables = nftables.Nftables() self.id = int(id) if id else None self.queue = queue + self.target = target self.proto = proto self.port = int(port) self.ip_int = str(ip_int) def __eq__(self, o: object) -> bool: if isinstance(o, FiregexFilter): - return self.port == o.port and self.proto == o.proto and ip_interface(self.ip_int) == ip_interface(o.ip_int) + return self.port == o.port and self.proto == o.proto and ip_parse(self.ip_int) == ip_parse(o.ip_int) return False - - def ipv6(self): - return ip_interface(self.ip_int).version == 6 - def ipv4(self): - return ip_interface(self.ip_int).version == 4 +class FiregexTables: -class FiregexTables(IPTables): + def __init__(self): + self.table_name = "firegex" + self.nft = nftables.Nftables() + + def raw_cmd(self, *cmds): + return self.nft.json_cmd({"nftables": list(cmds)}) - def __init__(self, ipv6=False): - super().__init__(ipv6, "mangle") - self.create_chain(FilterTypes.INPUT) - self.add_chain_to_input(FilterTypes.INPUT) - self.create_chain(FilterTypes.OUTPUT) - self.add_chain_to_output(FilterTypes.OUTPUT) - - def target_in_chain(self, chain, target): - for filter in self.list()[chain]: - if filter.target == target: - return True - return False - - def add_chain_to_input(self, chain): - if not self.target_in_chain("PREROUTING", str(chain)): - self.insert_rule("PREROUTING", str(chain)) - - def add_chain_to_output(self, chain): - if not self.target_in_chain("POSTROUTING", str(chain)): - self.insert_rule("POSTROUTING", str(chain)) + def cmd(self, *cmds): + code, out, err = self.raw_cmd(*cmds) - def add_output(self, queue_range, proto = None, port = None, ip_int = None): + if code == 0: return out + else: raise Exception(err) + + def init(self): + code, out, err = self.raw_cmd({"create":{"table":{"name":self.table_name,"family":"inet"}}}) + if code == 0: + self.cmd( + {"create":{"chain":{ + "family":"inet", + "table":self.table_name, + "name":"input", + "type":"filter", + "hook":"prerouting", + "prio":-150, + "policy":"accept" + }}}, + {"create":{"chain":{ + "family":"inet", + "table":self.table_name, + "name":"output", + "type":"filter", + "hook":"postrouting", + "prio":-150, + "policy":"accept" + }}} + ) + self.reset() + + def reset(self): + self.cmd({"flush":{"table":{"name":"firegex","family":"inet"}}}) + + def list(self): + return self.cmd({"list": {"ruleset": None}})["nftables"] + + def add_output(self, queue_range, proto, port, ip_int): init, end = queue_range if init > end: init, end = end, init - self.append_rule(FilterTypes.OUTPUT,"NFQUEUE", - * (["-p", str(proto)] if proto else []), - * (["-s", str(ip_int)] if ip_int else []), - * (["--sport", str(port)] if port else []), - * (["--queue-num", f"{init}"] if init == end else ["--queue-balance", f"{init}:{end}"]), - "--queue-bypass" - ) + ip_int = ip_parse(ip_int) + ip_addr = str(ip_int).split("/")[0] + ip_addr_cidr = int(str(ip_int).split("/")[1]) + self.cmd({ "insert":{ "rule": { + "family": "inet", + "table": self.table_name, + "chain": "output", + "expr": [ + {'match': {'left': {'payload': {'protocol': ip_family(ip_int), 'field': 'saddr'}}, 'op': '==', 'right': {"prefix": {"addr": ip_addr, "len": ip_addr_cidr}}}}, #ip_int + {'match': {'left': {'meta': {'key': 'l4proto'}}, 'op': '==', 'right': str(proto)}}, + {'match': {"left": { "payload": {"protocol": str(proto), "field": "sport"}}, "op": "==", "right": int(port)}}, + {"queue": {"num": str(init) if init == end else f"{init}-{end}", "flags": ["bypass"]}} + ] + }}}) def add_input(self, queue_range, proto = None, port = None, ip_int = None): init, end = queue_range if init > end: init, end = end, init - self.append_rule(FilterTypes.INPUT, "NFQUEUE", - * (["-p", str(proto)] if proto else []), - * (["-d", str(ip_int)] if ip_int else []), - * (["--dport", str(port)] if port else []), - * (["--queue-num", f"{init}"] if init == end else ["--queue-balance", f"{init}:{end}"]), - "--queue-bypass" - ) + ip_int = ip_parse(ip_int) + ip_addr = str(ip_int).split("/")[0] + ip_addr_cidr = int(str(ip_int).split("/")[1]) + self.cmd({"insert":{"rule":{ + "family": "inet", + "table": self.table_name, + "chain": "input", + "expr": [ + {'match': {'left': {'payload': {'protocol': ip_family(ip_int), 'field': 'daddr'}}, 'op': '==', 'right': {"prefix": {"addr": ip_addr, "len": ip_addr_cidr}}}}, #ip_int + {'match': {"left": { "payload": {"protocol": str(proto), "field": "dport"}}, "op": "==", "right": int(port)}}, + {"queue": {"num": str(init) if init == end else f"{init}-{end}", "flags": ["bypass"]}} + ] + }}}) def get(self) -> List[FiregexFilter]: res = [] - iptables_filters = self.list() - for filter_type in [FilterTypes.INPUT, FilterTypes.OUTPUT]: - for filter in iptables_filters[filter_type]: - port = filter.sport() if filter_type == FilterTypes.OUTPUT else filter.dport() - queue = filter.nfqueue() - if queue and port: - res.append(FiregexFilter( - target=filter_type, - id=filter.id, - queue=queue, - proto=filter.prot, - port=port, - ip_int=filter.source if filter_type == FilterTypes.OUTPUT else filter.destination - )) + for filter in [ele["rule"] for ele in self.list() if "rule" in ele and ele["rule"]["table"] == self.table_name]: + queue_str = str(filter["expr"][2]["queue"]["num"]).split("-") + queue = None + if len(queue_str) == 1: queue = int(queue_str[0]), int(queue_str[0]) + else: queue = int(queue_str[0]), int(queue_str[1]) + ip_int = None + if isinstance(filter["expr"][0]["match"]["right"],str): + ip_int = str(ip_parse(filter["expr"][0]["match"]["right"])) + else: + ip_int = f'{filter["expr"][0]["match"]["right"]["prefix"]["addr"]}/{filter["expr"][0]["match"]["right"]["prefix"]["len"]}' + res.append(FiregexFilter( + target=filter["chain"], + id=int(filter["handle"]), + queue=queue, + proto=filter["expr"][1]["match"]["left"]["payload"]["protocol"], + port=filter["expr"][1]["match"]["right"], + ip_int=ip_int + )) return res async def add(self, filter:FiregexFilter): if filter in self.get(): return None - return await FiregexInterceptor.start( iptables=self, filter=filter, n_queues=int(os.getenv("N_THREADS_NFQUEUE","1"))) - - def delete_all(self): - for filter_type in [FilterTypes.INPUT, FilterTypes.OUTPUT]: - self.flush_chain(filter_type) + return await FiregexInterceptor.start( filter=filter, n_queues=int(os.getenv("N_THREADS_NFQUEUE","1"))) def delete_by_srv(self, srv:Service): for filter in self.get(): - if filter.port == srv.port and filter.proto == srv.proto and ip_interface(filter.ip_int) == ip_interface(srv.ip_int): - self.delete_rule(filter.target, filter.id) + if filter.port == srv.port and filter.proto == srv.proto and ip_parse(filter.ip_int) == ip_parse(srv.ip_int): + print("DELETE CMD", {"delete":{"rule": {"handle": filter.id, "table": self.table_name, "chain": filter.target, "family": "inet"}}}) + self.cmd({"delete":{"rule": {"handle": filter.id, "table": self.table_name, "chain": filter.target, "family": "inet"}}}) class RegexFilter: @@ -159,7 +186,6 @@ class FiregexInterceptor: def __init__(self): self.filter:FiregexFilter - self.ipv6:bool self.filter_map_lock:asyncio.Lock self.filter_map: Dict[str, RegexFilter] self.regex_filters: Set[RegexFilter] @@ -167,21 +193,18 @@ class FiregexInterceptor: self.process:asyncio.subprocess.Process self.n_queues:int self.update_task: asyncio.Task - self.iptables:FiregexTables @classmethod - async def start(cls, iptables: FiregexTables, filter: FiregexFilter, n_queues:int = 1): + async def start(cls, filter: FiregexFilter, n_queues:int = 1): self = cls() self.filter = filter self.n_queues = n_queues - self.iptables = iptables - self.ipv6 = self.filter.ipv6() self.filter_map_lock = asyncio.Lock() self.update_config_lock = asyncio.Lock() input_range, output_range = await self._start_binary() self.update_task = asyncio.create_task(self.update_blocked()) - self.iptables.add_input(queue_range=input_range, proto=self.filter.proto, port=self.filter.port, ip_int=self.filter.ip_int) - self.iptables.add_output(queue_range=output_range, proto=self.filter.proto, port=self.filter.port, ip_int=self.filter.ip_int) + FiregexTables().add_input(queue_range=input_range, proto=self.filter.proto, port=self.filter.port, ip_int=self.filter.ip_int) + FiregexTables().add_output(queue_range=output_range, proto=self.filter.proto, port=self.filter.port, ip_int=self.filter.ip_int) return self async def _start_binary(self): @@ -221,7 +244,8 @@ class FiregexInterceptor: async def stop(self): self.update_task.cancel() - self.process.kill() + if self.process and self.process.returncode is None: + self.process.kill() async def _update_config(self, filters_codes): async with self.update_config_lock: diff --git a/backend/modules/firewall.py b/backend/modules/firewall.py index e12706b..213e42b 100644 --- a/backend/modules/firewall.py +++ b/backend/modules/firewall.py @@ -24,6 +24,7 @@ class FirewallManager: del self.proxy_table[srv_id] async def init(self): + FiregexTables().init() await self.reload() async def reload(self): @@ -47,7 +48,6 @@ class ServiceManager: def __init__(self, srv: Service, db): self.srv = srv self.db = db - self.firegextable = FiregexTables(self.srv.ipv6) self.status = STATUS.STOP self.filters: Dict[int, FiregexFilter] = {} self.lock = asyncio.Lock() @@ -93,13 +93,13 @@ class ServiceManager: async def start(self): if not self.interceptor: - self.firegextable.delete_by_srv(self.srv) - self.interceptor = await self.firegextable.add(FiregexFilter(self.srv.proto,self.srv.port, self.srv.ip_int)) + FiregexTables().delete_by_srv(self.srv) + self.interceptor = await FiregexTables().add(FiregexFilter(self.srv.proto,self.srv.port, self.srv.ip_int)) await self._update_filters_from_db() self._set_status(STATUS.ACTIVE) async def stop(self): - self.firegextable.delete_by_srv(self.srv) + FiregexTables().delete_by_srv(self.srv) if self.interceptor: await self.interceptor.stop() self.interceptor = None diff --git a/backend/modules/iptables.py b/backend/modules/iptables.py deleted file mode 100644 index 0eb7f76..0000000 --- a/backend/modules/iptables.py +++ /dev/null @@ -1,85 +0,0 @@ -import os, re -from subprocess import PIPE, Popen -from typing import Dict, List, Tuple, Union - -class Rule(): - def __init__(self, id, target, prot, opt, source, destination, details): - self.id = id - self.target = target - self.prot = prot - self.opt = opt - self.source = source - self.destination = destination - self.details = details - - def __repr__(self) -> str: - return f"Rule {self.id} : {self.target}, {self.prot}, {self.opt}, {self.source}, {self.destination}, {self.details}" - - def dport(self) -> Union[int, None]: - port = re.findall(r"dpt:([0-9]+)", self.details) - return int(port[0]) if port else None - - def sport(self) -> Union[int, None]: - port = re.findall(r"spt:([0-9]+)", self.details) - return int(port[0]) if port else None - - def nfqueue(self) -> Union[Tuple[int,int], None]: - balanced = re.findall(r"NFQUEUE balance ([0-9]+):([0-9]+)", self.details) - numbered = re.findall(r"NFQUEUE num ([0-9]+)", self.details) - queue_num = None - if balanced: queue_num = (int(balanced[0][0]), int(balanced[0][1])) - if numbered: queue_num = (int(numbered[0]), int(numbered[0])) - return queue_num - -class IPTables: - - def __init__(self, ipv6=False, table="filter"): - self.ipv6 = ipv6 - self.table = table - - def command(self, params) -> Tuple[bytes, bytes]: - params = ["-t", self.table] + params - if os.geteuid() != 0: - exit("You need to have root privileges to run this script.\nPlease try again, this time using 'sudo'. Exiting.") - return Popen(["ip6tables"]+params if self.ipv6 else ["iptables"]+params, stdout=PIPE, stderr=PIPE).communicate() - - def list(self) -> Dict[str, List[Rule]]: - stdout, strerr = self.command(["-L", "--line-number", "-n"]) - lines = stdout.decode().split("\n") - res: Dict[str, List[Rule]] = {} - chain_name = "" - for line in lines: - if line.startswith("Chain"): - chain_name = line.split()[1] - res[chain_name] = [] - elif line and line.split()[0].isnumeric(): - parsed = re.findall(r"([^ ]*)[ ]{,10}([^ ]*)[ ]{,5}([^ ]*)[ ]{,5}([^ ]*)[ ]{,5}([^ ]*)[ ]+([^ ]*)[ ]+(.*)", line) - if len(parsed) > 0: - parsed = parsed[0] - res[chain_name].append(Rule( - id=parsed[0].strip(), - target=parsed[1].strip(), - prot=parsed[2].strip(), - opt=parsed[3].strip(), - source=parsed[4].strip(), - destination=parsed[5].strip(), - details=" ".join(parsed[6:]).strip() if len(parsed) >= 7 else "" - )) - return res - - def delete_rule(self, chain, id) -> None: - self.command(["-D", str(chain), str(id)]) - - def create_chain(self, name) -> None: - self.command(["-N", str(name)]) - - def flush_chain(self, name) -> None: - self.command(["-F", str(name)]) - - def insert_rule(self, chain, rule, *args, rulenum=1) -> None: - self.command(["-I", str(chain), str(rulenum), "-j", str(rule), *args]) - - def append_rule(self, chain, rule, *args) -> None: - self.command(["-A", str(chain), "-j", str(rule), *args]) - - diff --git a/backend/modules/sqlite.py b/backend/modules/sqlite.py index 85e9e86..329fb14 100644 --- a/backend/modules/sqlite.py +++ b/backend/modules/sqlite.py @@ -8,6 +8,7 @@ class SQLite(): self.conn: Union[None, sqlite3.Connection] = None self.cur = None self.db_name = db_name + self.__backup = None self.schema = { 'services': { 'service_id': 'VARCHAR(100) PRIMARY KEY', @@ -49,6 +50,17 @@ class SQLite(): return d self.conn.row_factory = dict_factory + def backup(self): + if self.conn: + with open(self.db_name, "rb") as f: + self.__backup = f.read() + + def restore(self): + if self.__backup: + with open(self.db_name, "wb") as f: + f.write(self.__backup) + self.__backup = None + def disconnect(self) -> None: if self.conn: self.conn.close() @@ -101,18 +113,17 @@ class SQLite(): class Service: - def __init__(self, id: str, status: str, port: int, name: str, ipv6: bool, proto: str, ip_int: str): + def __init__(self, id: str, status: str, port: int, name: str, proto: str, ip_int: str): self.id = id self.status = status self.port = port self.name = name - self.ipv6 = ipv6 self.proto = proto self.ip_int = ip_int @classmethod def from_dict(cls, var: dict): - return cls(id=var["service_id"], status=var["status"], port=var["port"], name=var["name"], ipv6=var["ipv6"], proto=var["proto"], ip_int=var["ip_int"]) + return cls(id=var["service_id"], status=var["status"], port=var["port"], name=var["name"], proto=var["proto"], ip_int=var["ip_int"]) class Regex: diff --git a/backend/nfqueue/nfqueue.cpp b/backend/nfqueue/nfqueue.cpp index ab508d0..788a8a0 100644 --- a/backend/nfqueue/nfqueue.cpp +++ b/backend/nfqueue/nfqueue.cpp @@ -12,11 +12,11 @@ void config_updater (){ while (true){ getline(cin, line); if (cin.eof()){ - cerr << "[fatal] [upfdater] cin.eof()" << endl; + cerr << "[fatal] [updater] cin.eof()" << endl; exit(EXIT_FAILURE); } if (cin.bad()){ - cerr << "[fatal] [upfdater] cin.bad()" << endl; + cerr << "[fatal] [updater] cin.bad()" << endl; exit(EXIT_FAILURE); } cerr << "[info] [updater] Updating configuration with line " << line << endl; diff --git a/backend/requirements.txt b/backend/requirements.txt index cb24aeb..6ce2631 100755 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -4,3 +4,4 @@ uvicorn[standard] passlib[bcrypt] python-jose[cryptography] fastapi-socketio +git+https://salsa.debian.org/pkg-netfilter-team/pkg-nftables#egg=nftables&subdirectory=py diff --git a/backend/utils.py b/backend/utils.py index 43959f5..1dda9ba 100755 --- a/backend/utils.py +++ b/backend/utils.py @@ -1,3 +1,4 @@ +from ipaddress import ip_interface import os, socket, secrets LOCALHOST_IP = socket.gethostbyname(os.getenv("LOCALHOST_IP","127.0.0.1")) @@ -12,4 +13,10 @@ def gen_service_id(db): res = secrets.token_hex(8) if len(db.query('SELECT 1 FROM services WHERE service_id = ?;', res)) == 0: break - return res \ No newline at end of file + return res + +def ip_parse(ip:str): + return str(ip_interface(ip).network) + +def ip_family(ip:str): + return "ip6" if ip_interface(ip).version == 6 else "ip" \ No newline at end of file diff --git a/frontend/build/asset-manifest.json b/frontend/build/asset-manifest.json index 8218ef3..80f40d4 100644 --- a/frontend/build/asset-manifest.json +++ b/frontend/build/asset-manifest.json @@ -1,13 +1,13 @@ { "files": { "main.css": "/static/css/main.08225a85.css", - "main.js": "/static/js/main.0e7d88b5.js", + "main.js": "/static/js/main.70ebb0b2.js", "index.html": "/index.html", "main.08225a85.css.map": "/static/css/main.08225a85.css.map", - "main.0e7d88b5.js.map": "/static/js/main.0e7d88b5.js.map" + "main.70ebb0b2.js.map": "/static/js/main.70ebb0b2.js.map" }, "entrypoints": [ "static/css/main.08225a85.css", - "static/js/main.0e7d88b5.js" + "static/js/main.70ebb0b2.js" ] } \ No newline at end of file diff --git a/frontend/build/index.html b/frontend/build/index.html index 1571b81..cfcc7e3 100644 --- a/frontend/build/index.html +++ b/frontend/build/index.html @@ -1 +1 @@ -
a||125d?(a.sortIndex=c,f(t,a),null===h(r)&&a===h(t)&&(B?(E(L),L=-1):B=!0,K(H,c-d))):(a.sortIndex=e,f(r,a),A||z||(A=!0,I(J)));return a};\nexports.unstable_shouldYield=M;exports.unstable_wrapCallback=function(a){var b=y;return function(){var c=y;y=b;try{return a.apply(this,arguments)}finally{y=c}}};\n","'use strict';\n\nif (process.env.NODE_ENV === 'production') {\n module.exports = require('./cjs/scheduler.production.min.js');\n} else {\n module.exports = require('./cjs/scheduler.development.js');\n}\n","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = function(module) {\n\tvar getter = module && module.__esModule ?\n\t\tfunction() { return module['default']; } :\n\t\tfunction() { return module; };\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = function(exports, definition) {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }","// define __esModule on exports\n__webpack_require__.r = function(exports) {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","export default function _arrayLikeToArray(arr, len) {\n if (len == null || len > arr.length) len = arr.length;\n\n for (var i = 0, arr2 = new Array(len); i < len; i++) {\n arr2[i] = arr[i];\n }\n\n return arr2;\n}","import arrayLikeToArray from \"./arrayLikeToArray.js\";\nexport default function _unsupportedIterableToArray(o, minLen) {\n if (!o) return;\n if (typeof o === \"string\") return arrayLikeToArray(o, minLen);\n var n = Object.prototype.toString.call(o).slice(8, -1);\n if (n === \"Object\" && o.constructor) n = o.constructor.name;\n if (n === \"Map\" || n === \"Set\") return Array.from(o);\n if (n === \"Arguments\" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return arrayLikeToArray(o, minLen);\n}","import arrayWithHoles from \"./arrayWithHoles.js\";\nimport iterableToArrayLimit from \"./iterableToArrayLimit.js\";\nimport unsupportedIterableToArray from \"./unsupportedIterableToArray.js\";\nimport nonIterableRest from \"./nonIterableRest.js\";\nexport default function _slicedToArray(arr, i) {\n return arrayWithHoles(arr) || iterableToArrayLimit(arr, i) || unsupportedIterableToArray(arr, i) || nonIterableRest();\n}","export default function _arrayWithHoles(arr) {\n if (Array.isArray(arr)) return arr;\n}","export default function _iterableToArrayLimit(arr, i) {\n var _i = arr == null ? null : typeof Symbol !== \"undefined\" && arr[Symbol.iterator] || arr[\"@@iterator\"];\n\n if (_i == null) return;\n var _arr = [];\n var _n = true;\n var _d = false;\n\n var _s, _e;\n\n try {\n for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) {\n _arr.push(_s.value);\n\n if (i && _arr.length === i) break;\n }\n } catch (err) {\n _d = true;\n _e = err;\n } finally {\n try {\n if (!_n && _i[\"return\"] != null) _i[\"return\"]();\n } finally {\n if (_d) throw _e;\n }\n }\n\n return _arr;\n}","export default function _nonIterableRest() {\n throw new TypeError(\"Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\");\n}","export default function _extends() {\n _extends = Object.assign ? Object.assign.bind() : function (target) {\n for (var i = 1; i < arguments.length; i++) {\n var source = arguments[i];\n\n for (var key in source) {\n if (Object.prototype.hasOwnProperty.call(source, key)) {\n target[key] = source[key];\n }\n }\n }\n\n return target;\n };\n return _extends.apply(this, arguments);\n}","import * as React from \"react\";\nimport type { History, Location } from \"history\";\nimport { Action as NavigationType } from \"history\";\n\nimport type { RouteMatch } from \"./router\";\n\n/**\n * A Navigator is a \"location changer\"; it's how you get to different locations.\n *\n * Every history instance conforms to the Navigator interface, but the\n * distinction is useful primarily when it comes to the low-level