aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src
diff options
context:
space:
mode:
authorEgor Tensin <egor@tensin.name>2026-06-06 22:18:37 +0200
committerEgor Tensin <egor@tensin.name>2026-06-06 22:25:32 +0200
commit07a784f0d654fcd552069e85b314ea7c29cb5dd6 (patch)
tree9c12dec00b68aaa33dbf8429f34f9812493c965a /src
parentbump dependencies (diff)
downloadwireguard-config-07a784f0d654fcd552069e85b314ea7c29cb5dd6.tar.gz
wireguard-config-07a784f0d654fcd552069e85b314ea7c29cb5dd6.zip
move Jekyll source files to src/
Diffstat (limited to 'src')
l---------src/LICENSE.txt1
-rw-r--r--src/assets/css/pre_overlay.css11
-rw-r--r--src/assets/js/bundle.js2064
-rw-r--r--src/assets/js/main.js1117
-rw-r--r--src/index.html187
5 files changed, 3380 insertions, 0 deletions
diff --git a/src/LICENSE.txt b/src/LICENSE.txt
new file mode 120000
index 0000000..4ab4373
--- /dev/null
+++ b/src/LICENSE.txt
@@ -0,0 +1 @@
+../LICENSE.txt \ No newline at end of file
diff --git a/src/assets/css/pre_overlay.css b/src/assets/css/pre_overlay.css
new file mode 100644
index 0000000..efa291a
--- /dev/null
+++ b/src/assets/css/pre_overlay.css
@@ -0,0 +1,11 @@
+.pre_container {
+ position: relative;
+}
+.pre_buttons {
+ position: absolute;
+ top: 5px;
+ right: 5px;
+}
+.pre_buttons button:focus {
+ outline: none;
+}
diff --git a/src/assets/js/bundle.js b/src/assets/js/bundle.js
new file mode 100644
index 0000000..da8f679
--- /dev/null
+++ b/src/assets/js/bundle.js
@@ -0,0 +1,2064 @@
+require=(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.AddressError = void 0;
+class AddressError extends Error {
+ constructor(message, parseMessage) {
+ super(message);
+ this.name = 'AddressError';
+ this.parseMessage = parseMessage;
+ }
+}
+exports.AddressError = AddressError;
+
+},{}],2:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.isInSubnet = isInSubnet;
+exports.isCorrect = isCorrect;
+exports.prefixLengthFromMask = prefixLengthFromMask;
+exports.numberToPaddedHex = numberToPaddedHex;
+exports.stringToPaddedHex = stringToPaddedHex;
+exports.testBit = testBit;
+const address_error_1 = require("./address-error");
+function isInSubnet(address) {
+ if (this.subnetMask < address.subnetMask) {
+ return false;
+ }
+ if (this.mask(address.subnetMask) === address.mask()) {
+ return true;
+ }
+ return false;
+}
+function isCorrect(defaultBits) {
+ return function () {
+ if (this.addressMinusSuffix !== this.correctForm()) {
+ return false;
+ }
+ if (this.subnetMask === defaultBits && !this.parsedSubnet) {
+ return true;
+ }
+ return this.parsedSubnet === String(this.subnetMask);
+ };
+}
+/**
+ * Returns the prefix length (number of leading 1 bits) of a contiguous
+ * subnet mask. Throws `AddressError` if the mask is non-contiguous (e.g.
+ * `255.0.255.0`).
+ */
+function prefixLengthFromMask(value, totalBits) {
+ const binary = value.toString(2).padStart(totalBits, '0');
+ if (binary.length > totalBits) {
+ throw new address_error_1.AddressError('Invalid subnet mask.');
+ }
+ const firstZero = binary.indexOf('0');
+ if (firstZero === -1) {
+ return totalBits;
+ }
+ if (binary.slice(firstZero).includes('1')) {
+ throw new address_error_1.AddressError('Invalid subnet mask.');
+ }
+ return firstZero;
+}
+function numberToPaddedHex(number) {
+ return number.toString(16).padStart(2, '0');
+}
+function stringToPaddedHex(numberString) {
+ return numberToPaddedHex(parseInt(numberString, 10));
+}
+/**
+ * @param binaryValue Binary representation of a value (e.g. `10`)
+ * @param position Byte position, where 0 is the least significant bit
+ */
+function testBit(binaryValue, position) {
+ const { length } = binaryValue;
+ if (position > length) {
+ return false;
+ }
+ const positionInString = length - position;
+ return binaryValue.substring(positionInString, positionInString + 1) === '1';
+}
+
+},{"./address-error":1}],3:[function(require,module,exports){
+"use strict";
+/* eslint-disable no-param-reassign */
+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ var desc = Object.getOwnPropertyDescriptor(m, k);
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
+ desc = { enumerable: true, get: function() { return m[k]; } };
+ }
+ Object.defineProperty(o, k2, desc);
+}) : (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ o[k2] = m[k];
+}));
+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
+}) : function(o, v) {
+ o["default"] = v;
+});
+var __importStar = (this && this.__importStar) || function (mod) {
+ if (mod && mod.__esModule) return mod;
+ var result = {};
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
+ __setModuleDefault(result, mod);
+ return result;
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.Address4 = void 0;
+const common = __importStar(require("./common"));
+const constants = __importStar(require("./v4/constants"));
+const address_error_1 = require("./address-error");
+const isCorrect4 = common.isCorrect(constants.BITS);
+/**
+ * Represents an IPv4 address
+ * @param {string} address - An IPv4 address string
+ */
+class Address4 {
+ constructor(address) {
+ this.groups = constants.GROUPS;
+ this.parsedAddress = [];
+ this.parsedSubnet = '';
+ this.subnet = '/32';
+ this.subnetMask = 32;
+ this.v4 = true;
+ /**
+ * Returns true if the address is correct, false otherwise
+ * @returns {Boolean}
+ */
+ this.isCorrect = isCorrect4;
+ /**
+ * Returns true if the given address is in the subnet of the current address
+ * @returns {boolean}
+ */
+ this.isInSubnet = common.isInSubnet;
+ this.address = address;
+ const subnet = constants.RE_SUBNET_STRING.exec(address);
+ if (subnet) {
+ this.parsedSubnet = subnet[0].replace('/', '');
+ this.subnetMask = parseInt(this.parsedSubnet, 10);
+ this.subnet = `/${this.subnetMask}`;
+ if (this.subnetMask < 0 || this.subnetMask > constants.BITS) {
+ throw new address_error_1.AddressError('Invalid subnet mask.');
+ }
+ address = address.replace(constants.RE_SUBNET_STRING, '');
+ }
+ this.addressMinusSuffix = address;
+ this.parsedAddress = this.parse(address);
+ }
+ /**
+ * Returns true if the given string is a valid IPv4 address (with optional
+ * CIDR subnet), false otherwise. Host bits in the subnet portion are
+ * allowed (e.g. `192.168.1.5/24` is valid); for strict network-address
+ * validation compare `correctForm()` to `startAddress().correctForm()`,
+ * or use `networkForm()`.
+ */
+ static isValid(address) {
+ try {
+ // eslint-disable-next-line no-new
+ new Address4(address);
+ return true;
+ }
+ catch (e) {
+ return false;
+ }
+ }
+ /**
+ * Parses an IPv4 address string into its four octet groups and stores the
+ * result on `this.parsedAddress`. Called automatically by the constructor;
+ * you typically don't need to call it directly. Throws `AddressError` if
+ * the input is not a valid IPv4 address.
+ */
+ parse(address) {
+ const groups = address.split('.');
+ if (!address.match(constants.RE_ADDRESS)) {
+ throw new address_error_1.AddressError('Invalid IPv4 address.');
+ }
+ return groups;
+ }
+ /**
+ * Returns the address in correct form: octets joined with `.` and any
+ * leading zeros stripped (e.g. `192.168.1.1`). For IPv4 this matches the
+ * canonical dotted-decimal representation.
+ */
+ correctForm() {
+ return this.parsedAddress.map((part) => parseInt(part, 10)).join('.');
+ }
+ /**
+ * Construct an `Address4` from an address and a dotted-decimal subnet
+ * mask given as separate strings (e.g. as returned by Node's
+ * `os.networkInterfaces()`). Throws `AddressError` if the mask is
+ * non-contiguous (e.g. `255.0.255.0`).
+ * @example
+ * var address = Address4.fromAddressAndMask('192.168.1.1', '255.255.255.0');
+ * address.subnetMask; // 24
+ */
+ static fromAddressAndMask(address, mask) {
+ const bits = common.prefixLengthFromMask(new Address4(mask).bigInt(), constants.BITS);
+ return new Address4(`${address}/${bits}`);
+ }
+ /**
+ * Construct an `Address4` from an address and a Cisco-style wildcard mask
+ * given as separate strings (e.g. `0.0.0.255` for a `/24`). The wildcard
+ * mask is the bitwise inverse of the subnet mask. Throws `AddressError`
+ * if the mask is non-contiguous (e.g. `0.255.0.255`).
+ * @example
+ * var address = Address4.fromAddressAndWildcardMask('10.0.0.1', '0.0.0.255');
+ * address.subnetMask; // 24
+ */
+ static fromAddressAndWildcardMask(address, wildcardMask) {
+ const wildcard = new Address4(wildcardMask).bigInt();
+ const allOnes = (BigInt(1) << BigInt(constants.BITS)) - BigInt(1);
+ // eslint-disable-next-line no-bitwise
+ const mask = wildcard ^ allOnes;
+ const bits = common.prefixLengthFromMask(mask, constants.BITS);
+ return new Address4(`${address}/${bits}`);
+ }
+ /**
+ * Construct an `Address4` from a wildcard pattern with trailing `*`
+ * octets. The number of trailing wildcards determines the prefix
+ * length: each `*` represents 8 bits.
+ *
+ * Only trailing whole-octet wildcards are supported. Partial-octet
+ * wildcards (e.g. `192.168.0.1*`) and interior wildcards (e.g.
+ * `192.*.0.1`) throw `AddressError`.
+ * @example
+ * Address4.fromWildcard('192.168.0.*').subnet; // '/24'
+ * Address4.fromWildcard('192.168.*.*').subnet; // '/16'
+ * Address4.fromWildcard('*.*.*.*').subnet; // '/0'
+ */
+ static fromWildcard(input) {
+ const groups = input.split('.');
+ if (groups.length !== constants.GROUPS) {
+ throw new address_error_1.AddressError('Wildcard pattern must have 4 octets');
+ }
+ let firstWildcard = -1;
+ for (let i = 0; i < groups.length; i++) {
+ if (groups[i] === '*') {
+ if (firstWildcard === -1) {
+ firstWildcard = i;
+ }
+ }
+ else if (firstWildcard !== -1) {
+ throw new address_error_1.AddressError('Wildcard `*` must only appear in trailing octets (e.g. `192.168.0.*`)');
+ }
+ }
+ const trailing = firstWildcard === -1 ? 0 : groups.length - firstWildcard;
+ const replaced = groups.map((g) => (g === '*' ? '0' : g));
+ const subnetBits = constants.BITS - trailing * 8;
+ return new Address4(`${replaced.join('.')}/${subnetBits}`);
+ }
+ /**
+ * Converts a hex string to an IPv4 address object. Accepts 8 hex digits
+ * with optional `:` separators (e.g. `'7f000001'` or `'7f:00:00:01'`).
+ * Throws `AddressError` for any other length or for non-hex characters.
+ * @param {string} hex - a hex string to convert
+ * @returns {Address4}
+ */
+ static fromHex(hex) {
+ const stripped = hex.replace(/:/g, '');
+ if (!/^[0-9a-fA-F]{8}$/.test(stripped)) {
+ throw new address_error_1.AddressError('IPv4 hex must be exactly 8 hex digits');
+ }
+ const groups = [];
+ for (let i = 0; i < 8; i += 2) {
+ groups.push(parseInt(stripped.slice(i, i + 2), 16));
+ }
+ return new Address4(groups.join('.'));
+ }
+ /**
+ * Converts an integer into a IPv4 address object. The integer must be a
+ * non-negative safe integer in the range `[0, 2**32 - 1]`; otherwise
+ * `AddressError` is thrown.
+ * @param {integer} integer - a number to convert
+ * @returns {Address4}
+ */
+ static fromInteger(integer) {
+ if (!Number.isInteger(integer) || integer < 0 || integer > 0xffffffff) {
+ throw new address_error_1.AddressError('IPv4 integer must be in the range 0 to 2**32 - 1');
+ }
+ return Address4.fromHex(integer.toString(16).padStart(8, '0'));
+ }
+ /**
+ * Return an address from in-addr.arpa form
+ * @param {string} arpaFormAddress - an 'in-addr.arpa' form ipv4 address
+ * @returns {Adress4}
+ * @example
+ * var address = Address4.fromArpa(42.2.0.192.in-addr.arpa.)
+ * address.correctForm(); // '192.0.2.42'
+ */
+ static fromArpa(arpaFormAddress) {
+ // remove ending ".in-addr.arpa." or just "."
+ const leader = arpaFormAddress.replace(/(\.in-addr\.arpa)?\.$/, '');
+ const address = leader.split('.').reverse().join('.');
+ return new Address4(address);
+ }
+ /**
+ * Converts an IPv4 address object to a hex string
+ * @returns {String}
+ */
+ toHex() {
+ return this.parsedAddress.map((part) => common.stringToPaddedHex(part)).join(':');
+ }
+ /**
+ * Converts an IPv4 address object to an array of bytes.
+ *
+ * To get a Node.js `Buffer`, wrap the result: `Buffer.from(address.toArray())`.
+ * @returns {Array}
+ */
+ toArray() {
+ return this.parsedAddress.map((part) => parseInt(part, 10));
+ }
+ /**
+ * Converts an IPv4 address object to an IPv6 address group
+ * @returns {String}
+ */
+ toGroup6() {
+ const output = [];
+ let i;
+ for (i = 0; i < constants.GROUPS; i += 2) {
+ output.push(`${common.stringToPaddedHex(this.parsedAddress[i])}${common.stringToPaddedHex(this.parsedAddress[i + 1])}`);
+ }
+ return output.join(':');
+ }
+ /**
+ * Returns the address as a `bigint`
+ * @returns {bigint}
+ */
+ bigInt() {
+ return BigInt(`0x${this.parsedAddress.map((n) => common.stringToPaddedHex(n)).join('')}`);
+ }
+ /**
+ * Helper function getting start address.
+ * @returns {bigint}
+ */
+ _startAddress() {
+ return BigInt(`0b${this.mask() + '0'.repeat(constants.BITS - this.subnetMask)}`);
+ }
+ /**
+ * The first address in the range given by this address' subnet.
+ * Often referred to as the Network Address.
+ * @returns {Address4}
+ */
+ startAddress() {
+ return Address4.fromBigInt(this._startAddress());
+ }
+ /**
+ * The first host address in the range given by this address's subnet ie
+ * the first address after the Network Address
+ * @returns {Address4}
+ */
+ startAddressExclusive() {
+ const adjust = BigInt('1');
+ return Address4.fromBigInt(this._startAddress() + adjust);
+ }
+ /**
+ * Helper function getting end address.
+ * @returns {bigint}
+ */
+ _endAddress() {
+ return BigInt(`0b${this.mask() + '1'.repeat(constants.BITS - this.subnetMask)}`);
+ }
+ /**
+ * The last address in the range given by this address' subnet
+ * Often referred to as the Broadcast
+ * @returns {Address4}
+ */
+ endAddress() {
+ return Address4.fromBigInt(this._endAddress());
+ }
+ /**
+ * The last host address in the range given by this address's subnet ie
+ * the last address prior to the Broadcast Address
+ * @returns {Address4}
+ */
+ endAddressExclusive() {
+ const adjust = BigInt('1');
+ return Address4.fromBigInt(this._endAddress() - adjust);
+ }
+ /**
+ * The dotted-decimal form of the subnet mask, e.g. `255.255.240.0` for
+ * a `/20`. Returns an `Address4`; call `.correctForm()` for the string.
+ * @returns {Address4}
+ */
+ subnetMaskAddress() {
+ return Address4.fromBigInt(BigInt(`0b${'1'.repeat(this.subnetMask)}${'0'.repeat(constants.BITS - this.subnetMask)}`));
+ }
+ /**
+ * The Cisco-style wildcard mask, e.g. `0.0.0.255` for a `/24`. This is
+ * the bitwise inverse of `subnetMaskAddress()`. Returns an `Address4`;
+ * call `.correctForm()` for the string.
+ * @returns {Address4}
+ */
+ wildcardMask() {
+ return Address4.fromBigInt(BigInt(`0b${'0'.repeat(this.subnetMask)}${'1'.repeat(constants.BITS - this.subnetMask)}`));
+ }
+ /**
+ * The network address in CIDR string form, e.g. `192.168.1.0/24` for
+ * `192.168.1.5/24`. For an address with no explicit subnet the prefix is
+ * `/32`, e.g. `networkForm()` on `192.168.1.5` returns `192.168.1.5/32`.
+ * @returns {string}
+ */
+ networkForm() {
+ return `${this.startAddress().correctForm()}/${this.subnetMask}`;
+ }
+ /**
+ * Converts a BigInt to a v4 address object. The value must be in the
+ * range `[0, 2**32 - 1]`; otherwise `AddressError` is thrown.
+ * @param {bigint} bigInt - a BigInt to convert
+ * @returns {Address4}
+ */
+ static fromBigInt(bigInt) {
+ if (bigInt < 0n || bigInt > 0xffffffffn) {
+ throw new address_error_1.AddressError('IPv4 BigInt must be in the range 0 to 2**32 - 1');
+ }
+ return Address4.fromHex(bigInt.toString(16).padStart(8, '0'));
+ }
+ /**
+ * Convert a byte array to an Address4 object.
+ *
+ * To convert from a Node.js `Buffer`, spread it: `Address4.fromByteArray([...buf])`.
+ * @param {Array<number>} bytes - an array of 4 bytes (0-255)
+ * @returns {Address4}
+ */
+ static fromByteArray(bytes) {
+ if (bytes.length !== 4) {
+ throw new address_error_1.AddressError('IPv4 addresses require exactly 4 bytes');
+ }
+ // Validate that all bytes are within valid range (0-255)
+ for (let i = 0; i < bytes.length; i++) {
+ if (!Number.isInteger(bytes[i]) || bytes[i] < 0 || bytes[i] > 255) {
+ throw new address_error_1.AddressError('All bytes must be integers between 0 and 255');
+ }
+ }
+ return this.fromUnsignedByteArray(bytes);
+ }
+ /**
+ * Convert an unsigned byte array to an Address4 object
+ * @param {Array<number>} bytes - an array of 4 unsigned bytes (0-255)
+ * @returns {Address4}
+ */
+ static fromUnsignedByteArray(bytes) {
+ if (bytes.length !== 4) {
+ throw new address_error_1.AddressError('IPv4 addresses require exactly 4 bytes');
+ }
+ const address = bytes.join('.');
+ return new Address4(address);
+ }
+ /**
+ * Returns the first n bits of the address, defaulting to the
+ * subnet mask
+ * @returns {String}
+ */
+ mask(mask) {
+ if (mask === undefined) {
+ mask = this.subnetMask;
+ }
+ return this.getBitsBase2(0, mask);
+ }
+ /**
+ * Returns the bits in the given range as a base-2 string
+ * @returns {string}
+ */
+ getBitsBase2(start, end) {
+ return this.binaryZeroPad().slice(start, end);
+ }
+ /**
+ * Return the reversed ip6.arpa form of the address
+ * @param {Object} options
+ * @param {boolean} options.omitSuffix - omit the "in-addr.arpa" suffix
+ * @returns {String}
+ */
+ reverseForm(options) {
+ if (!options) {
+ options = {};
+ }
+ const reversed = this.correctForm().split('.').reverse().join('.');
+ if (options.omitSuffix) {
+ return reversed;
+ }
+ return `${reversed}.in-addr.arpa.`;
+ }
+ /**
+ * Returns true if the given address is a multicast address
+ * @returns {boolean}
+ */
+ isMulticast() {
+ return this.isInSubnet(MULTICAST_V4);
+ }
+ /**
+ * Returns true if the address is in one of the [RFC 1918](https://datatracker.ietf.org/doc/html/rfc1918) private address ranges (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`).
+ * @returns {boolean}
+ */
+ isPrivate() {
+ return PRIVATE_V4.some((subnet) => this.isInSubnet(subnet));
+ }
+ /**
+ * Returns true if the address is in the loopback range `127.0.0.0/8` ([RFC 1122](https://datatracker.ietf.org/doc/html/rfc1122)).
+ * @returns {boolean}
+ */
+ isLoopback() {
+ return this.isInSubnet(LOOPBACK_V4);
+ }
+ /**
+ * Returns true if the address is in the link-local range `169.254.0.0/16` ([RFC 3927](https://datatracker.ietf.org/doc/html/rfc3927)).
+ * @returns {boolean}
+ */
+ isLinkLocal() {
+ return this.isInSubnet(LINK_LOCAL_V4);
+ }
+ /**
+ * Returns true if the address is the unspecified address `0.0.0.0`.
+ * @returns {boolean}
+ */
+ isUnspecified() {
+ return this.isInSubnet(UNSPECIFIED_V4);
+ }
+ /**
+ * Returns true if the address is the limited broadcast address `255.255.255.255` ([RFC 919](https://datatracker.ietf.org/doc/html/rfc919)).
+ * @returns {boolean}
+ */
+ isBroadcast() {
+ return this.isInSubnet(BROADCAST_V4);
+ }
+ /**
+ * Returns true if the address is in the carrier-grade NAT range `100.64.0.0/10` ([RFC 6598](https://datatracker.ietf.org/doc/html/rfc6598)).
+ * @returns {boolean}
+ */
+ isCGNAT() {
+ return this.isInSubnet(CGNAT_V4);
+ }
+ /**
+ * Returns a zero-padded base-2 string representation of the address
+ * @returns {string}
+ */
+ binaryZeroPad() {
+ if (this._binaryZeroPad === undefined) {
+ this._binaryZeroPad = this.bigInt().toString(2).padStart(constants.BITS, '0');
+ }
+ return this._binaryZeroPad;
+ }
+ /**
+ * Groups an IPv4 address for inclusion at the end of an IPv6 address
+ * @returns {String}
+ */
+ groupForV6() {
+ const segments = this.parsedAddress;
+ return this.address.replace(constants.RE_ADDRESS, `<span class="hover-group group-v4 group-6">${segments
+ .slice(0, 2)
+ .join('.')}</span>.<span class="hover-group group-v4 group-7">${segments
+ .slice(2, 4)
+ .join('.')}</span>`);
+ }
+}
+exports.Address4 = Address4;
+const MULTICAST_V4 = new Address4('224.0.0.0/4');
+const PRIVATE_V4 = [
+ new Address4('10.0.0.0/8'),
+ new Address4('172.16.0.0/12'),
+ new Address4('192.168.0.0/16'),
+];
+const LOOPBACK_V4 = new Address4('127.0.0.0/8');
+const LINK_LOCAL_V4 = new Address4('169.254.0.0/16');
+const UNSPECIFIED_V4 = new Address4('0.0.0.0/32');
+const BROADCAST_V4 = new Address4('255.255.255.255/32');
+const CGNAT_V4 = new Address4('100.64.0.0/10');
+
+},{"./address-error":1,"./common":2,"./v4/constants":5}],4:[function(require,module,exports){
+"use strict";
+/* eslint-disable prefer-destructuring */
+/* eslint-disable no-param-reassign */
+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ var desc = Object.getOwnPropertyDescriptor(m, k);
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
+ desc = { enumerable: true, get: function() { return m[k]; } };
+ }
+ Object.defineProperty(o, k2, desc);
+}) : (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ o[k2] = m[k];
+}));
+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
+}) : function(o, v) {
+ o["default"] = v;
+});
+var __importStar = (this && this.__importStar) || function (mod) {
+ if (mod && mod.__esModule) return mod;
+ var result = {};
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
+ __setModuleDefault(result, mod);
+ return result;
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.Address6 = void 0;
+const common = __importStar(require("./common"));
+const constants4 = __importStar(require("./v4/constants"));
+const constants6 = __importStar(require("./v6/constants"));
+const helpers = __importStar(require("./v6/helpers"));
+const ipv4_1 = require("./ipv4");
+const regular_expressions_1 = require("./v6/regular-expressions");
+const address_error_1 = require("./address-error");
+const common_1 = require("./common");
+const isCorrect6 = common.isCorrect(constants6.BITS);
+function assert(condition) {
+ if (!condition) {
+ throw new Error('Assertion failed.');
+ }
+}
+function addCommas(number) {
+ const r = /(\d+)(\d{3})/;
+ while (r.test(number)) {
+ number = number.replace(r, '$1,$2');
+ }
+ return number;
+}
+function spanLeadingZeroes4(n) {
+ n = n.replace(/^(0{1,})([1-9]+)$/, '<span class="parse-error">$1</span>$2');
+ n = n.replace(/^(0{1,})(0)$/, '<span class="parse-error">$1</span>$2');
+ return n;
+}
+/*
+ * A helper function to compact an array
+ */
+function compact(address, slice) {
+ const s1 = [];
+ const s2 = [];
+ let i;
+ for (i = 0; i < address.length; i++) {
+ if (i < slice[0]) {
+ s1.push(address[i]);
+ }
+ else if (i > slice[1]) {
+ s2.push(address[i]);
+ }
+ }
+ return s1.concat(['compact']).concat(s2);
+}
+function paddedHex(octet) {
+ return parseInt(octet, 16).toString(16).padStart(4, '0');
+}
+function unsignByte(b) {
+ // eslint-disable-next-line no-bitwise
+ return b & 0xff;
+}
+/**
+ * Represents an IPv6 address
+ * @param {string} address - An IPv6 address string
+ * @param {number} [groups=8] - How many octets to parse
+ * @example
+ * var address = new Address6('2001::/32');
+ */
+class Address6 {
+ constructor(address, optionalGroups) {
+ this.addressMinusSuffix = '';
+ this.parsedSubnet = '';
+ this.subnet = '/128';
+ this.subnetMask = 128;
+ this.v4 = false;
+ this.zone = '';
+ // #region Attributes
+ /**
+ * Returns true if the given address is in the subnet of the current address
+ * @returns {boolean}
+ */
+ this.isInSubnet = common.isInSubnet;
+ /**
+ * Returns true if the address is correct, false otherwise
+ * @returns {boolean}
+ */
+ this.isCorrect = isCorrect6;
+ if (optionalGroups === undefined) {
+ this.groups = constants6.GROUPS;
+ }
+ else {
+ this.groups = optionalGroups;
+ }
+ this.address = address;
+ const subnet = constants6.RE_SUBNET_STRING.exec(address);
+ if (subnet) {
+ this.parsedSubnet = subnet[0].replace('/', '');
+ this.subnetMask = parseInt(this.parsedSubnet, 10);
+ this.subnet = `/${this.subnetMask}`;
+ if (Number.isNaN(this.subnetMask) ||
+ this.subnetMask < 0 ||
+ this.subnetMask > constants6.BITS) {
+ throw new address_error_1.AddressError('Invalid subnet mask.');
+ }
+ address = address.replace(constants6.RE_SUBNET_STRING, '');
+ }
+ else if (/\//.test(address)) {
+ throw new address_error_1.AddressError('Invalid subnet mask.');
+ }
+ const zone = constants6.RE_ZONE_STRING.exec(address);
+ if (zone) {
+ this.zone = zone[0];
+ address = address.replace(constants6.RE_ZONE_STRING, '');
+ }
+ this.addressMinusSuffix = address;
+ this.parsedAddress = this.parse(this.addressMinusSuffix);
+ }
+ /**
+ * Returns true if the given string is a valid IPv6 address (with optional
+ * CIDR subnet and zone identifier), false otherwise. Host bits in the
+ * subnet portion are allowed (e.g. `2001:db8::1/32` is valid); for strict
+ * network-address validation compare `correctForm()` to
+ * `startAddress().correctForm()`, or use `networkForm()`.
+ */
+ static isValid(address) {
+ try {
+ // eslint-disable-next-line no-new
+ new Address6(address);
+ return true;
+ }
+ catch (e) {
+ return false;
+ }
+ }
+ /**
+ * Convert a BigInt to a v6 address object. The value must be in the
+ * range `[0, 2**128 - 1]`; otherwise `AddressError` is thrown.
+ * @param {bigint} bigInt - a BigInt to convert
+ * @returns {Address6}
+ * @example
+ * var bigInt = BigInt('1000000000000');
+ * var address = Address6.fromBigInt(bigInt);
+ * address.correctForm(); // '::e8:d4a5:1000'
+ */
+ static fromBigInt(bigInt) {
+ if (bigInt < 0n || bigInt > (1n << BigInt(constants6.BITS)) - 1n) {
+ throw new address_error_1.AddressError('IPv6 BigInt must be in the range 0 to 2**128 - 1');
+ }
+ const hex = bigInt.toString(16).padStart(32, '0');
+ const groups = [];
+ for (let i = 0; i < constants6.GROUPS; i++) {
+ groups.push(hex.slice(i * 4, (i + 1) * 4));
+ }
+ return new Address6(groups.join(':'));
+ }
+ /**
+ * Parse a URL (with optional bracketed host and port) into an address and
+ * port. Returns either `{ address, port }` on success or
+ * `{ error, address: null, port: null }` if the URL could not be parsed.
+ * Ports are returned as numbers (or `null` if absent or out of range).
+ * @example
+ * var addressAndPort = Address6.fromURL('http://[ffff::]:8080/foo/');
+ * addressAndPort.address.correctForm(); // 'ffff::'
+ * addressAndPort.port; // 8080
+ */
+ static fromURL(url) {
+ let host;
+ let port = null;
+ let result;
+ // If we have brackets parse them and find a port
+ if (url.indexOf('[') !== -1 && url.indexOf(']:') !== -1) {
+ result = constants6.RE_URL_WITH_PORT.exec(url);
+ if (result === null) {
+ return {
+ error: 'failed to parse address with port',
+ address: null,
+ port: null,
+ };
+ }
+ host = result[1];
+ port = result[2];
+ // If there's a URL extract the address
+ }
+ else if (url.indexOf('/') !== -1) {
+ // Remove the protocol prefix
+ url = url.replace(/^[a-z0-9]+:\/\//, '');
+ // Parse the address
+ result = constants6.RE_URL.exec(url);
+ if (result === null) {
+ return {
+ error: 'failed to parse address from URL',
+ address: null,
+ port: null,
+ };
+ }
+ host = result[1];
+ // Otherwise just assign the URL to the host and let the library parse it
+ }
+ else {
+ host = url;
+ }
+ // If there's a port convert it to an integer
+ if (port) {
+ port = parseInt(port, 10);
+ // squelch out of range ports
+ if (port < 0 || port > 65536) {
+ port = null;
+ }
+ }
+ else {
+ // Standardize `undefined` to `null`
+ port = null;
+ }
+ return {
+ address: new Address6(host),
+ port,
+ };
+ }
+ /**
+ * Construct an `Address6` from an address and a hex subnet mask given as
+ * separate strings (e.g. as returned by Node's `os.networkInterfaces()`).
+ * Throws `AddressError` if the mask is non-contiguous (e.g.
+ * `ffff::ffff`).
+ * @example
+ * var address = Address6.fromAddressAndMask('fe80::1', 'ffff:ffff:ffff:ffff::');
+ * address.subnetMask; // 64
+ */
+ static fromAddressAndMask(address, mask) {
+ const bits = common.prefixLengthFromMask(new Address6(mask).bigInt(), constants6.BITS);
+ return new Address6(`${address}/${bits}`);
+ }
+ /**
+ * Construct an `Address6` from an address and a Cisco-style wildcard mask
+ * given as separate strings (e.g. `::ffff:ffff:ffff:ffff` for a `/64`).
+ * The wildcard mask is the bitwise inverse of the subnet mask. Throws
+ * `AddressError` if the mask is non-contiguous.
+ * @example
+ * var address = Address6.fromAddressAndWildcardMask('fe80::1', '::ffff:ffff:ffff:ffff');
+ * address.subnetMask; // 64
+ */
+ static fromAddressAndWildcardMask(address, wildcardMask) {
+ const wildcard = new Address6(wildcardMask).bigInt();
+ const allOnes = (BigInt(1) << BigInt(constants6.BITS)) - BigInt(1);
+ // eslint-disable-next-line no-bitwise
+ const mask = wildcard ^ allOnes;
+ const bits = common.prefixLengthFromMask(mask, constants6.BITS);
+ return new Address6(`${address}/${bits}`);
+ }
+ /**
+ * Construct an `Address6` from a wildcard pattern with trailing `*`
+ * groups. The number of trailing wildcards determines the prefix
+ * length: each `*` represents 16 bits. `::` is expanded to zero groups
+ * (not wildcards) before evaluating trailing wildcards.
+ *
+ * Only trailing whole-group wildcards are supported. Partial-group
+ * wildcards (e.g. `2001:db8::0*`) and interior wildcards (e.g.
+ * `*::1`) throw `AddressError`.
+ * @example
+ * Address6.fromWildcard('2001:db8:*:*:*:*:*:*').subnet; // '/32'
+ * Address6.fromWildcard('2001:db8::*').subnet; // '/112'
+ * Address6.fromWildcard('*:*:*:*:*:*:*:*').subnet; // '/0'
+ */
+ static fromWildcard(input) {
+ if (input.includes('%') || input.includes('/')) {
+ throw new address_error_1.AddressError('Wildcard pattern must not include a zone or CIDR suffix');
+ }
+ const halves = input.split('::');
+ if (halves.length > 2) {
+ throw new address_error_1.AddressError("Wildcard pattern cannot contain more than one '::'");
+ }
+ let groups;
+ if (halves.length === 2) {
+ const left = halves[0] === '' ? [] : halves[0].split(':');
+ const right = halves[1] === '' ? [] : halves[1].split(':');
+ const remaining = constants6.GROUPS - left.length - right.length;
+ if (remaining < 1) {
+ throw new address_error_1.AddressError("Wildcard pattern with '::' has too many groups");
+ }
+ groups = [...left, ...new Array(remaining).fill('0'), ...right];
+ }
+ else {
+ groups = input.split(':');
+ }
+ if (groups.length !== constants6.GROUPS) {
+ throw new address_error_1.AddressError('Wildcard pattern must have 8 groups');
+ }
+ let firstWildcard = -1;
+ for (let i = 0; i < groups.length; i++) {
+ if (groups[i] === '*') {
+ if (firstWildcard === -1) {
+ firstWildcard = i;
+ }
+ }
+ else if (firstWildcard !== -1) {
+ throw new address_error_1.AddressError('Wildcard `*` must only appear in trailing groups (e.g. `2001:db8:*:*:*:*:*:*`)');
+ }
+ }
+ const trailing = firstWildcard === -1 ? 0 : groups.length - firstWildcard;
+ const replaced = groups.map((g) => (g === '*' ? '0' : g));
+ const subnetBits = constants6.BITS - trailing * 16;
+ return new Address6(`${replaced.join(':')}/${subnetBits}`);
+ }
+ /**
+ * Create an IPv6-mapped address given an IPv4 address
+ * @param {string} address - An IPv4 address string
+ * @returns {Address6}
+ * @example
+ * var address = Address6.fromAddress4('192.168.0.1');
+ * address.correctForm(); // '::ffff:c0a8:1'
+ * address.to4in6(); // '::ffff:192.168.0.1'
+ */
+ static fromAddress4(address) {
+ const address4 = new ipv4_1.Address4(address);
+ const mask6 = constants6.BITS - (constants4.BITS - address4.subnetMask);
+ return new Address6(`::ffff:${address4.correctForm()}/${mask6}`);
+ }
+ /**
+ * Return an address from ip6.arpa form
+ * @param {string} arpaFormAddress - an 'ip6.arpa' form address
+ * @returns {Adress6}
+ * @example
+ * var address = Address6.fromArpa(e.f.f.f.3.c.2.6.f.f.f.e.6.6.8.e.1.0.6.7.9.4.e.c.0.0.0.0.1.0.0.2.ip6.arpa.)
+ * address.correctForm(); // '2001:0:ce49:7601:e866:efff:62c3:fffe'
+ */
+ static fromArpa(arpaFormAddress) {
+ // remove ending ".ip6.arpa." or just "."
+ let address = arpaFormAddress.replace(/(\.ip6\.arpa)?\.$/, '');
+ const semicolonAmount = 7;
+ // correct ip6.arpa form with ending removed will be 63 characters
+ if (address.length !== 63) {
+ throw new address_error_1.AddressError("Invalid 'ip6.arpa' form.");
+ }
+ const parts = address.split('.').reverse();
+ for (let i = semicolonAmount; i > 0; i--) {
+ const insertIndex = i * 4;
+ parts.splice(insertIndex, 0, ':');
+ }
+ address = parts.join('');
+ return new Address6(address);
+ }
+ /**
+ * Return the Microsoft UNC transcription of the address
+ * @returns {String} the Microsoft UNC transcription of the address
+ */
+ microsoftTranscription() {
+ return `${this.correctForm().replace(/:/g, '-')}.ipv6-literal.net`;
+ }
+ /**
+ * Return the first n bits of the address, defaulting to the subnet mask
+ * @param {number} [mask=subnet] - the number of bits to mask
+ * @returns {String} the first n bits of the address as a string
+ */
+ mask(mask = this.subnetMask) {
+ return this.getBitsBase2(0, mask);
+ }
+ /**
+ * Return the number of possible subnets of a given size in the address
+ * @param {number} [subnetSize=128] - the subnet size
+ * @returns {String}
+ */
+ // TODO: probably useful to have a numeric version of this too
+ possibleSubnets(subnetSize = 128) {
+ const availableBits = constants6.BITS - this.subnetMask;
+ const subnetBits = Math.abs(subnetSize - constants6.BITS);
+ const subnetPowers = availableBits - subnetBits;
+ if (subnetPowers < 0) {
+ return '0';
+ }
+ return addCommas((BigInt('2') ** BigInt(subnetPowers)).toString(10));
+ }
+ /**
+ * Helper function getting start address.
+ * @returns {bigint}
+ */
+ _startAddress() {
+ return BigInt(`0b${this.mask() + '0'.repeat(constants6.BITS - this.subnetMask)}`);
+ }
+ /**
+ * The first address in the range given by this address' subnet
+ * Often referred to as the Network Address.
+ * @returns {Address6}
+ */
+ startAddress() {
+ return Address6.fromBigInt(this._startAddress());
+ }
+ /**
+ * The first host address in the range given by this address's subnet ie
+ * the first address after the Network Address
+ * @returns {Address6}
+ */
+ startAddressExclusive() {
+ const adjust = BigInt('1');
+ return Address6.fromBigInt(this._startAddress() + adjust);
+ }
+ /**
+ * Helper function getting end address.
+ * @returns {bigint}
+ */
+ _endAddress() {
+ return BigInt(`0b${this.mask() + '1'.repeat(constants6.BITS - this.subnetMask)}`);
+ }
+ /**
+ * The last address in the range given by this address' subnet
+ * Often referred to as the Broadcast
+ * @returns {Address6}
+ */
+ endAddress() {
+ return Address6.fromBigInt(this._endAddress());
+ }
+ /**
+ * The last host address in the range given by this address's subnet ie
+ * the last address prior to the Broadcast Address
+ * @returns {Address6}
+ */
+ endAddressExclusive() {
+ const adjust = BigInt('1');
+ return Address6.fromBigInt(this._endAddress() - adjust);
+ }
+ /**
+ * The hex form of the subnet mask, e.g. `ffff:ffff:ffff:ffff::` for a
+ * `/64`. Returns an `Address6`; call `.correctForm()` for the string.
+ * @returns {Address6}
+ */
+ subnetMaskAddress() {
+ return Address6.fromBigInt(BigInt(`0b${'1'.repeat(this.subnetMask)}${'0'.repeat(constants6.BITS - this.subnetMask)}`));
+ }
+ /**
+ * The Cisco-style wildcard mask, e.g. `::ffff:ffff:ffff:ffff` for a
+ * `/64`. This is the bitwise inverse of `subnetMaskAddress()`. Returns
+ * an `Address6`; call `.correctForm()` for the string.
+ * @returns {Address6}
+ */
+ wildcardMask() {
+ return Address6.fromBigInt(BigInt(`0b${'0'.repeat(this.subnetMask)}${'1'.repeat(constants6.BITS - this.subnetMask)}`));
+ }
+ /**
+ * The network address in CIDR string form, e.g. `2001:db8::/32` for
+ * `2001:db8::1/32`. For an address with no explicit subnet the prefix
+ * is `/128`, e.g. `networkForm()` on `2001:db8::1` returns
+ * `2001:db8::1/128`.
+ * @returns {string}
+ */
+ networkForm() {
+ return `${this.startAddress().correctForm()}/${this.subnetMask}`;
+ }
+ /**
+ * Return the scope of the address. The 4-bit scope field
+ * ([RFC 4291 §2.7](https://datatracker.ietf.org/doc/html/rfc4291#section-2.7))
+ * is only defined for multicast addresses; for unicast addresses the scope
+ * is derived from the address type per
+ * [RFC 4007 §6](https://datatracker.ietf.org/doc/html/rfc4007#section-6).
+ * @returns {String}
+ */
+ getScope() {
+ const type = this.getType();
+ if (type === 'Multicast' || type.startsWith('Multicast ')) {
+ const scope = constants6.SCOPES[parseInt(this.getBits(12, 16).toString(10), 10)];
+ return scope || 'Unknown';
+ }
+ // RFC 4291 §2.5.3: the loopback address is treated as having Link-Local
+ // scope. (Multicast scope 1, "Interface-Local", is a different concept
+ // used only for loopback transmission of multicast.)
+ if (type === 'Link-local unicast' || type === 'Loopback') {
+ return 'Link local';
+ }
+ // RFC 4007 §6: the unspecified address has no scope.
+ if (type === 'Unspecified') {
+ return 'Unknown';
+ }
+ return 'Global';
+ }
+ /**
+ * Return the type of the address
+ * @returns {String}
+ */
+ getType() {
+ for (let i = 0; i < TYPE_SUBNETS.length; i++) {
+ const entry = TYPE_SUBNETS[i];
+ if (this.isInSubnet(entry[0])) {
+ return entry[1];
+ }
+ }
+ return 'Global unicast';
+ }
+ /**
+ * Return the bits in the given range as a BigInt
+ * @returns {bigint}
+ */
+ getBits(start, end) {
+ return BigInt(`0b${this.getBitsBase2(start, end)}`);
+ }
+ /**
+ * Return the bits in the given range as a base-2 string
+ * @returns {String}
+ */
+ getBitsBase2(start, end) {
+ return this.binaryZeroPad().slice(start, end);
+ }
+ /**
+ * Return the bits in the given range as a base-16 string
+ * @returns {String}
+ */
+ getBitsBase16(start, end) {
+ const length = end - start;
+ if (length % 4 !== 0) {
+ throw new Error('Length of bits to retrieve must be divisible by four');
+ }
+ return this.getBits(start, end)
+ .toString(16)
+ .padStart(length / 4, '0');
+ }
+ /**
+ * Return the bits that are set past the subnet mask length
+ * @returns {String}
+ */
+ getBitsPastSubnet() {
+ return this.getBitsBase2(this.subnetMask, constants6.BITS);
+ }
+ /**
+ * Return the reversed ip6.arpa form of the address
+ * @param {Object} options
+ * @param {boolean} options.omitSuffix - omit the "ip6.arpa" suffix
+ * @returns {String}
+ */
+ reverseForm(options) {
+ if (!options) {
+ options = {};
+ }
+ const characters = Math.floor(this.subnetMask / 4);
+ const reversed = this.canonicalForm()
+ .replace(/:/g, '')
+ .split('')
+ .slice(0, characters)
+ .reverse()
+ .join('.');
+ if (characters > 0) {
+ if (options.omitSuffix) {
+ return reversed;
+ }
+ return `${reversed}.ip6.arpa.`;
+ }
+ if (options.omitSuffix) {
+ return '';
+ }
+ return 'ip6.arpa.';
+ }
+ /**
+ * Returns the address in correct form, per
+ * [RFC 5952](https://datatracker.ietf.org/doc/html/rfc5952): leading zeros
+ * stripped, the longest run of zero groups collapsed to `::`, and hex digits
+ * lowercased (e.g. `2001:db8::1`). This is the recommended form for display.
+ */
+ correctForm() {
+ let i;
+ let groups = [];
+ let zeroCounter = 0;
+ const zeroes = [];
+ for (i = 0; i < this.parsedAddress.length; i++) {
+ const value = parseInt(this.parsedAddress[i], 16);
+ if (value === 0) {
+ zeroCounter++;
+ }
+ if (value !== 0 && zeroCounter > 0) {
+ if (zeroCounter > 1) {
+ zeroes.push([i - zeroCounter, i - 1]);
+ }
+ zeroCounter = 0;
+ }
+ }
+ // Do we end with a string of zeroes?
+ if (zeroCounter > 1) {
+ zeroes.push([this.parsedAddress.length - zeroCounter, this.parsedAddress.length - 1]);
+ }
+ const zeroLengths = zeroes.map((n) => n[1] - n[0] + 1);
+ if (zeroes.length > 0) {
+ const index = zeroLengths.indexOf(Math.max(...zeroLengths));
+ groups = compact(this.parsedAddress, zeroes[index]);
+ }
+ else {
+ groups = this.parsedAddress;
+ }
+ for (i = 0; i < groups.length; i++) {
+ if (groups[i] !== 'compact') {
+ groups[i] = parseInt(groups[i], 16).toString(16);
+ }
+ }
+ let correct = groups.join(':');
+ correct = correct.replace(/^compact$/, '::');
+ correct = correct.replace(/(^compact)|(compact$)/, ':');
+ correct = correct.replace(/compact/, '');
+ return correct;
+ }
+ /**
+ * Return a zero-padded base-2 string representation of the address
+ * @returns {String}
+ * @example
+ * var address = new Address6('2001:4860:4001:803::1011');
+ * address.binaryZeroPad();
+ * // '0010000000000001010010000110000001000000000000010000100000000011
+ * // 0000000000000000000000000000000000000000000000000001000000010001'
+ */
+ binaryZeroPad() {
+ if (this._binaryZeroPad === undefined) {
+ this._binaryZeroPad = this.bigInt().toString(2).padStart(constants6.BITS, '0');
+ }
+ return this._binaryZeroPad;
+ }
+ /**
+ * Parses a v4-in-v6 string (e.g. `::ffff:192.168.0.1`) by extracting the
+ * trailing IPv4 address into `this.address4` / `this.parsedAddress4` and
+ * returning the address with the v4 portion converted to two v6 groups.
+ * Used internally by `parse()`.
+ */
+ // TODO: Improve the semantics of this helper function
+ parse4in6(address) {
+ if (address.indexOf('.') === -1) {
+ return address;
+ }
+ const groups = address.split(':');
+ const lastGroup = groups.slice(-1)[0];
+ const address4 = lastGroup.match(constants4.RE_ADDRESS);
+ if (address4) {
+ this.parsedAddress4 = address4[0];
+ this.address4 = new ipv4_1.Address4(this.parsedAddress4);
+ for (let i = 0; i < this.address4.groups; i++) {
+ if (/^0[0-9]+/.test(this.address4.parsedAddress[i])) {
+ // The prefix groups haven't been through the bad-character check
+ // yet, so escape them before including in the error HTML.
+ const highlighted = this.address4.parsedAddress.map(spanLeadingZeroes4).join('.');
+ const prefix = groups.slice(0, -1).map(helpers.escapeHtml).join(':');
+ const separator = groups.length > 1 ? ':' : '';
+ throw new address_error_1.AddressError("IPv4 addresses can't have leading zeroes.", `${prefix}${separator}${highlighted}`);
+ }
+ }
+ this.v4 = true;
+ groups[groups.length - 1] = this.address4.toGroup6();
+ address = groups.join(':');
+ }
+ return address;
+ }
+ /**
+ * Parses an IPv6 address string into its 8 hexadecimal groups (expanding
+ * any `::` elision and any trailing v4-in-v6 portion) and stores the result
+ * on `this.parsedAddress`. Called automatically by the constructor; you
+ * typically don't need to call it directly. Throws `AddressError` if the
+ * input is malformed.
+ */
+ // TODO: Make private?
+ parse(address) {
+ address = this.parse4in6(address);
+ const badCharacters = address.match(constants6.RE_BAD_CHARACTERS);
+ if (badCharacters) {
+ throw new address_error_1.AddressError(`Bad character${badCharacters.length > 1 ? 's' : ''} detected in address: ${badCharacters.join('')}`, address.replace(constants6.RE_BAD_CHARACTERS, '<span class="parse-error">$1</span>'));
+ }
+ const badAddress = address.match(constants6.RE_BAD_ADDRESS);
+ if (badAddress) {
+ throw new address_error_1.AddressError(`Address failed regex: ${badAddress.join('')}`, address.replace(constants6.RE_BAD_ADDRESS, '<span class="parse-error">$1</span>'));
+ }
+ let groups = [];
+ const halves = address.split('::');
+ if (halves.length === 2) {
+ let first = halves[0].split(':');
+ let last = halves[1].split(':');
+ if (first.length === 1 && first[0] === '') {
+ first = [];
+ }
+ if (last.length === 1 && last[0] === '') {
+ last = [];
+ }
+ const remaining = this.groups - (first.length + last.length);
+ if (!remaining) {
+ throw new address_error_1.AddressError('Error parsing groups');
+ }
+ this.elidedGroups = remaining;
+ this.elisionBegin = first.length;
+ this.elisionEnd = first.length + this.elidedGroups;
+ groups = groups.concat(first);
+ for (let i = 0; i < remaining; i++) {
+ groups.push('0');
+ }
+ groups = groups.concat(last);
+ }
+ else if (halves.length === 1) {
+ groups = address.split(':');
+ this.elidedGroups = 0;
+ }
+ else {
+ throw new address_error_1.AddressError('Too many :: groups found');
+ }
+ groups = groups.map((group) => parseInt(group, 16).toString(16));
+ if (groups.length !== this.groups) {
+ throw new address_error_1.AddressError('Incorrect number of groups found');
+ }
+ return groups;
+ }
+ /**
+ * Returns the canonical (fully expanded) form of the address: all 8 groups,
+ * each padded to 4 hex digits, with no `::` collapsing
+ * (e.g. `2001:0db8:0000:0000:0000:0000:0000:0001`). Useful for sorting and
+ * byte-exact comparison.
+ */
+ canonicalForm() {
+ return this.parsedAddress.map(paddedHex).join(':');
+ }
+ /**
+ * Return the decimal form of the address
+ * @returns {String}
+ */
+ decimal() {
+ return this.parsedAddress.map((n) => parseInt(n, 16).toString(10).padStart(5, '0')).join(':');
+ }
+ /**
+ * Return the address as a BigInt
+ * @returns {bigint}
+ */
+ bigInt() {
+ return BigInt(`0x${this.parsedAddress.map(paddedHex).join('')}`);
+ }
+ /**
+ * Return the last two groups of this address as an IPv4 address string
+ * @returns {Address4}
+ * @example
+ * var address = new Address6('2001:4860:4001::1825:bf11');
+ * address.to4().correctForm(); // '24.37.191.17'
+ */
+ to4() {
+ const binary = this.binaryZeroPad().split('');
+ return ipv4_1.Address4.fromHex(BigInt(`0b${binary.slice(96, 128).join('')}`).toString(16).padStart(8, '0'));
+ }
+ /**
+ * Return the v4-in-v6 form of the address
+ * @returns {String}
+ */
+ to4in6() {
+ const address4 = this.to4();
+ const address6 = new Address6(this.parsedAddress.slice(0, 6).join(':'), 6);
+ const correct = address6.correctForm();
+ let infix = '';
+ if (!/:$/.test(correct)) {
+ infix = ':';
+ }
+ return correct + infix + address4.address;
+ }
+ /**
+ * Decodes the Teredo tunneling fields embedded in this address. Returns the
+ * Teredo prefix, server IPv4, client IPv4, raw flag bits, cone-NAT flag,
+ * UDP port, and Microsoft-format flag breakdown (reserved, universal/local,
+ * group/individual, nonce). Only meaningful for addresses in `2001::/32`.
+ */
+ inspectTeredo() {
+ /*
+ - Bits 0 to 31 are set to the Teredo prefix (normally 2001:0000::/32).
+ - Bits 32 to 63 embed the primary IPv4 address of the Teredo server that
+ is used.
+ - Bits 64 to 79 can be used to define some flags. Currently only the
+ higher order bit is used; it is set to 1 if the Teredo client is
+ located behind a cone NAT, 0 otherwise. For Microsoft's Windows Vista
+ and Windows Server 2008 implementations, more bits are used. In those
+ implementations, the format for these 16 bits is "CRAAAAUG AAAAAAAA",
+ where "C" remains the "Cone" flag. The "R" bit is reserved for future
+ use. The "U" bit is for the Universal/Local flag (set to 0). The "G" bit
+ is Individual/Group flag (set to 0). The A bits are set to a 12-bit
+ randomly generated number chosen by the Teredo client to introduce
+ additional protection for the Teredo node against IPv6-based scanning
+ attacks.
+ - Bits 80 to 95 contains the obfuscated UDP port number. This is the
+ port number that is mapped by the NAT to the Teredo client with all
+ bits inverted.
+ - Bits 96 to 127 contains the obfuscated IPv4 address. This is the
+ public IPv4 address of the NAT with all bits inverted.
+ */
+ const prefix = this.getBitsBase16(0, 32);
+ const bitsForUdpPort = this.getBits(80, 96);
+ // eslint-disable-next-line no-bitwise
+ const udpPort = (bitsForUdpPort ^ BigInt('0xffff')).toString();
+ const server4 = ipv4_1.Address4.fromHex(this.getBitsBase16(32, 64));
+ const bitsForClient4 = this.getBits(96, 128);
+ // eslint-disable-next-line no-bitwise
+ const client4 = ipv4_1.Address4.fromHex((bitsForClient4 ^ BigInt('0xffffffff')).toString(16).padStart(8, '0'));
+ const flagsBase2 = this.getBitsBase2(64, 80);
+ const coneNat = (0, common_1.testBit)(flagsBase2, 15);
+ const reserved = (0, common_1.testBit)(flagsBase2, 14);
+ const groupIndividual = (0, common_1.testBit)(flagsBase2, 8);
+ const universalLocal = (0, common_1.testBit)(flagsBase2, 9);
+ const nonce = BigInt(`0b${flagsBase2.slice(2, 6) + flagsBase2.slice(8, 16)}`).toString(10);
+ return {
+ prefix: `${prefix.slice(0, 4)}:${prefix.slice(4, 8)}`,
+ server4: server4.address,
+ client4: client4.address,
+ flags: flagsBase2,
+ coneNat,
+ microsoft: {
+ reserved,
+ universalLocal,
+ groupIndividual,
+ nonce,
+ },
+ udpPort,
+ };
+ }
+ /**
+ * Decodes the 6to4 tunneling fields embedded in this address. Returns the
+ * 6to4 prefix and the embedded IPv4 gateway address. Only meaningful for
+ * addresses in `2002::/16`.
+ */
+ inspect6to4() {
+ /*
+ - Bits 0 to 15 are set to the 6to4 prefix (2002::/16).
+ - Bits 16 to 48 embed the IPv4 address of the 6to4 gateway that is used.
+ */
+ const prefix = this.getBitsBase16(0, 16);
+ const gateway = ipv4_1.Address4.fromHex(this.getBitsBase16(16, 48));
+ return {
+ prefix: prefix.slice(0, 4),
+ gateway: gateway.address,
+ };
+ }
+ /**
+ * Return a v6 6to4 address from a v6 v4inv6 address
+ * @returns {Address6}
+ */
+ to6to4() {
+ if (!this.is4()) {
+ return null;
+ }
+ const addr6to4 = [
+ '2002',
+ this.getBitsBase16(96, 112),
+ this.getBitsBase16(112, 128),
+ '',
+ '/16',
+ ].join(':');
+ return new Address6(addr6to4);
+ }
+ /**
+ * Embed an IPv4 address into a NAT64 IPv6 address using the encoding
+ * defined by [RFC 6052](https://datatracker.ietf.org/doc/html/rfc6052).
+ * The default prefix is the well-known prefix `64:ff9b::/96`. The prefix
+ * length must be one of 32, 40, 48, 56, 64, or 96; for prefixes shorter
+ * than /64 the IPv4 octets are split around the reserved bits 64–71.
+ * @example
+ * Address6.fromAddress4Nat64('192.0.2.33').correctForm(); // '64:ff9b::c000:221'
+ * Address6.fromAddress4Nat64('192.0.2.33', '2001:db8::/32').correctForm(); // '2001:db8:c000:221::'
+ */
+ static fromAddress4Nat64(address, prefix = '64:ff9b::/96') {
+ const v4 = new ipv4_1.Address4(address);
+ const prefix6 = new Address6(prefix);
+ const pl = prefix6.subnetMask;
+ if (pl !== 32 && pl !== 40 && pl !== 48 && pl !== 56 && pl !== 64 && pl !== 96) {
+ throw new address_error_1.AddressError('NAT64 prefix length must be 32, 40, 48, 56, 64, or 96');
+ }
+ const prefixBits = prefix6.binaryZeroPad();
+ const v4Bits = v4.binaryZeroPad();
+ let bits;
+ if (pl === 96) {
+ bits = prefixBits.slice(0, 96) + v4Bits;
+ }
+ else {
+ const beforeU = 64 - pl;
+ bits =
+ prefixBits.slice(0, pl) +
+ v4Bits.slice(0, beforeU) +
+ '00000000' +
+ v4Bits.slice(beforeU) +
+ '0'.repeat(128 - 72 - (32 - beforeU));
+ }
+ const hex = BigInt(`0b${bits}`).toString(16).padStart(32, '0');
+ const groups = [];
+ for (let i = 0; i < 8; i++) {
+ groups.push(hex.slice(i * 4, (i + 1) * 4));
+ }
+ return new Address6(groups.join(':'));
+ }
+ /**
+ * Extract the embedded IPv4 address from a NAT64 IPv6 address using the
+ * encoding defined by [RFC 6052](https://datatracker.ietf.org/doc/html/rfc6052).
+ * The default prefix is the well-known prefix `64:ff9b::/96`. Returns
+ * `null` if this address is not contained within the given prefix.
+ * @example
+ * new Address6('64:ff9b::c000:221').toAddress4Nat64()!.correctForm(); // '192.0.2.33'
+ */
+ toAddress4Nat64(prefix = '64:ff9b::/96') {
+ const prefix6 = new Address6(prefix);
+ const pl = prefix6.subnetMask;
+ if (pl !== 32 && pl !== 40 && pl !== 48 && pl !== 56 && pl !== 64 && pl !== 96) {
+ throw new address_error_1.AddressError('NAT64 prefix length must be 32, 40, 48, 56, 64, or 96');
+ }
+ if (!this.isInSubnet(prefix6)) {
+ return null;
+ }
+ const bits = this.binaryZeroPad();
+ let v4Bits;
+ if (pl === 96) {
+ v4Bits = bits.slice(96, 128);
+ }
+ else {
+ const beforeU = 64 - pl;
+ v4Bits = bits.slice(pl, pl + beforeU) + bits.slice(72, 72 + (32 - beforeU));
+ }
+ const octets = [];
+ for (let i = 0; i < 4; i++) {
+ octets.push(parseInt(v4Bits.slice(i * 8, (i + 1) * 8), 2).toString());
+ }
+ return new ipv4_1.Address4(octets.join('.'));
+ }
+ /**
+ * Return a byte array.
+ *
+ * To get a Node.js `Buffer`, wrap the result: `Buffer.from(address.toByteArray())`.
+ * @returns {Array}
+ */
+ toByteArray() {
+ const valueWithoutPadding = this.bigInt().toString(16);
+ const leadingPad = '0'.repeat(valueWithoutPadding.length % 2);
+ const value = `${leadingPad}${valueWithoutPadding}`;
+ const bytes = [];
+ for (let i = 0, length = value.length; i < length; i += 2) {
+ bytes.push(parseInt(value.substring(i, i + 2), 16));
+ }
+ return bytes;
+ }
+ /**
+ * Return an unsigned byte array.
+ *
+ * To get a Node.js `Buffer`, wrap the result: `Buffer.from(address.toUnsignedByteArray())`.
+ * @returns {Array}
+ */
+ toUnsignedByteArray() {
+ return this.toByteArray().map(unsignByte);
+ }
+ /**
+ * Convert a byte array to an Address6 object.
+ *
+ * To convert from a Node.js `Buffer`, spread it: `Address6.fromByteArray([...buf])`.
+ * @returns {Address6}
+ */
+ static fromByteArray(bytes) {
+ return this.fromUnsignedByteArray(bytes.map(unsignByte));
+ }
+ /**
+ * Convert an unsigned byte array to an Address6 object.
+ *
+ * To convert from a Node.js `Buffer`, spread it: `Address6.fromUnsignedByteArray([...buf])`.
+ * @returns {Address6}
+ */
+ static fromUnsignedByteArray(bytes) {
+ const BYTE_MAX = BigInt('256');
+ let result = BigInt('0');
+ let multiplier = BigInt('1');
+ for (let i = bytes.length - 1; i >= 0; i--) {
+ result += multiplier * BigInt(bytes[i].toString(10));
+ multiplier *= BYTE_MAX;
+ }
+ return Address6.fromBigInt(result);
+ }
+ /**
+ * Returns true if the address is in the canonical form, false otherwise
+ * @returns {boolean}
+ */
+ isCanonical() {
+ return this.addressMinusSuffix === this.canonicalForm();
+ }
+ /**
+ * Returns true if the address is a link local address, false otherwise
+ * @returns {boolean}
+ */
+ isLinkLocal() {
+ // Zeroes are required, i.e. we can't check isInSubnet with 'fe80::/10'
+ if (this.getBitsBase2(0, 64) ===
+ '1111111010000000000000000000000000000000000000000000000000000000') {
+ return true;
+ }
+ return false;
+ }
+ /**
+ * Returns true if the address is a multicast address, false otherwise
+ * @returns {boolean}
+ */
+ isMulticast() {
+ const type = this.getType();
+ return type === 'Multicast' || type.startsWith('Multicast ');
+ }
+ /**
+ * Returns true if the address was written in v4-in-v6 dotted-quad notation
+ * (e.g. `::ffff:127.0.0.1`), false otherwise. This is a notation-level flag
+ * and does not reflect whether the address bits lie in the IPv4-mapped
+ * (`::ffff:0:0/96`) subnet — for that, see {@link isMapped4}.
+ * @returns {boolean}
+ */
+ is4() {
+ return this.v4;
+ }
+ /**
+ * Returns true if the address is an IPv4-mapped IPv6 address in
+ * `::ffff:0:0/96` ([RFC 4291 §2.5.5.2](https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2)),
+ * false otherwise. Unlike {@link is4}, this checks the underlying address
+ * bits rather than the textual notation, so `::ffff:127.0.0.1` and
+ * `::ffff:7f00:1` both return true.
+ * @returns {boolean}
+ */
+ isMapped4() {
+ return this.isInSubnet(IPV4_MAPPED_SUBNET);
+ }
+ /**
+ * Returns true if the address is a Teredo address, false otherwise
+ * @returns {boolean}
+ */
+ isTeredo() {
+ return this.isInSubnet(TEREDO_SUBNET);
+ }
+ /**
+ * Returns true if the address is a 6to4 address, false otherwise
+ * @returns {boolean}
+ */
+ is6to4() {
+ return this.isInSubnet(SIX_TO_FOUR_SUBNET);
+ }
+ /**
+ * Returns true if the address is a loopback address, false otherwise
+ * @returns {boolean}
+ */
+ isLoopback() {
+ return this.getType() === 'Loopback';
+ }
+ /**
+ * Returns true if the address is a Unique Local Address in `fc00::/7` ([RFC 4193](https://datatracker.ietf.org/doc/html/rfc4193)). ULAs are the IPv6 equivalent of IPv4 [RFC 1918](https://datatracker.ietf.org/doc/html/rfc1918) private addresses.
+ * @returns {boolean}
+ */
+ isULA() {
+ return this.isInSubnet(ULA_SUBNET);
+ }
+ /**
+ * Returns true if the address is the unspecified address `::`.
+ * @returns {boolean}
+ */
+ isUnspecified() {
+ return this.getType() === 'Unspecified';
+ }
+ /**
+ * Returns true if the address is in the documentation prefix `2001:db8::/32` ([RFC 3849](https://datatracker.ietf.org/doc/html/rfc3849)).
+ * @returns {boolean}
+ */
+ isDocumentation() {
+ return this.isInSubnet(DOCUMENTATION_SUBNET);
+ }
+ // #endregion
+ // #region HTML
+ /**
+ * Returns the address as an HTTP URL with the host bracketed, e.g.
+ * `http://[2001:db8::1]/`. If `optionalPort` is provided it is appended,
+ * e.g. `http://[2001:db8::1]:8080/`.
+ */
+ href(optionalPort) {
+ if (optionalPort === undefined) {
+ optionalPort = '';
+ }
+ else {
+ optionalPort = `:${optionalPort}`;
+ }
+ return `http://[${this.correctForm()}]${optionalPort}/`;
+ }
+ /**
+ * Returns an HTML `<a>` element whose `href` encodes the address in a URL
+ * hash fragment (default prefix `/#address=`). Useful for linking between
+ * pages of an address-inspector UI.
+ * @param options.className - CSS class for the rendered `<a>` element
+ * @param options.prefix - hash prefix prepended to the address (default `/#address=`)
+ * @param options.v4 - when true, render the address in v4-in-v6 form
+ */
+ link(options) {
+ if (!options) {
+ options = {};
+ }
+ if (options.className === undefined) {
+ options.className = '';
+ }
+ if (options.prefix === undefined) {
+ options.prefix = '/#address=';
+ }
+ if (options.v4 === undefined) {
+ options.v4 = false;
+ }
+ let formFunction = this.correctForm;
+ if (options.v4) {
+ formFunction = this.to4in6;
+ }
+ const form = formFunction.call(this);
+ const safeHref = helpers.escapeHtml(`${options.prefix}${form}`);
+ const safeForm = helpers.escapeHtml(form);
+ if (options.className) {
+ const safeClass = helpers.escapeHtml(options.className);
+ return `<a href="${safeHref}" class="${safeClass}">${safeForm}</a>`;
+ }
+ return `<a href="${safeHref}">${safeForm}</a>`;
+ }
+ /**
+ * Groups an address
+ * @returns {String}
+ */
+ group() {
+ if (this.elidedGroups === 0) {
+ // The simple case
+ return helpers.simpleGroup(this.addressMinusSuffix).join(':');
+ }
+ assert(typeof this.elidedGroups === 'number');
+ assert(typeof this.elisionBegin === 'number');
+ // The elided case
+ const output = [];
+ const [left, right] = this.addressMinusSuffix.split('::');
+ if (left.length) {
+ output.push(...helpers.simpleGroup(left));
+ }
+ else {
+ output.push('');
+ }
+ const classes = ['hover-group'];
+ for (let i = this.elisionBegin; i < this.elisionBegin + this.elidedGroups; i++) {
+ classes.push(`group-${i}`);
+ }
+ output.push(`<span class="${classes.join(' ')}"></span>`);
+ if (right.length) {
+ output.push(...helpers.simpleGroup(right, this.elisionEnd));
+ }
+ else {
+ output.push('');
+ }
+ if (this.is4()) {
+ assert(this.address4 instanceof ipv4_1.Address4);
+ output.pop();
+ output.push(this.address4.groupForV6());
+ }
+ return output.join(':');
+ }
+ // #endregion
+ // #region Regular expressions
+ /**
+ * Generate a regular expression string that can be used to find or validate
+ * all variations of this address
+ * @param {boolean} substringSearch
+ * @returns {string}
+ */
+ regularExpressionString(substringSearch = false) {
+ let output = [];
+ // TODO: revisit why this is necessary
+ const address6 = new Address6(this.correctForm());
+ if (address6.elidedGroups === 0) {
+ // The simple case
+ output.push((0, regular_expressions_1.simpleRegularExpression)(address6.parsedAddress));
+ }
+ else if (address6.elidedGroups === constants6.GROUPS) {
+ // A completely elided address
+ output.push((0, regular_expressions_1.possibleElisions)(constants6.GROUPS));
+ }
+ else {
+ // A partially elided address
+ const halves = address6.address.split('::');
+ if (halves[0].length) {
+ output.push((0, regular_expressions_1.simpleRegularExpression)(halves[0].split(':')));
+ }
+ assert(typeof address6.elidedGroups === 'number');
+ output.push((0, regular_expressions_1.possibleElisions)(address6.elidedGroups, halves[0].length !== 0, halves[1].length !== 0));
+ if (halves[1].length) {
+ output.push((0, regular_expressions_1.simpleRegularExpression)(halves[1].split(':')));
+ }
+ output = [output.join(':')];
+ }
+ if (!substringSearch) {
+ output = [
+ '(?=^|',
+ regular_expressions_1.ADDRESS_BOUNDARY,
+ '|[^\\w\\:])(',
+ ...output,
+ ')(?=[^\\w\\:]|',
+ regular_expressions_1.ADDRESS_BOUNDARY,
+ '|$)',
+ ];
+ }
+ return output.join('');
+ }
+ /**
+ * Generate a regular expression that can be used to find or validate all
+ * variations of this address.
+ * @param {boolean} substringSearch
+ * @returns {RegExp}
+ */
+ regularExpression(substringSearch = false) {
+ return new RegExp(this.regularExpressionString(substringSearch), 'i');
+ }
+}
+exports.Address6 = Address6;
+const TYPE_SUBNETS = Object.keys(constants6.TYPES).map((subnet) => [
+ new Address6(subnet),
+ constants6.TYPES[subnet],
+]);
+const TEREDO_SUBNET = new Address6('2001::/32');
+const SIX_TO_FOUR_SUBNET = new Address6('2002::/16');
+const ULA_SUBNET = new Address6('fc00::/7');
+const DOCUMENTATION_SUBNET = new Address6('2001:db8::/32');
+const IPV4_MAPPED_SUBNET = new Address6('::ffff:0:0/96');
+
+},{"./address-error":1,"./common":2,"./ipv4":3,"./v4/constants":5,"./v6/constants":6,"./v6/helpers":7,"./v6/regular-expressions":8}],5:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.RE_SUBNET_STRING = exports.RE_ADDRESS = exports.GROUPS = exports.BITS = void 0;
+exports.BITS = 32;
+exports.GROUPS = 4;
+exports.RE_ADDRESS = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/g;
+exports.RE_SUBNET_STRING = /\/\d{1,2}$/;
+
+},{}],6:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.RE_URL_WITH_PORT = exports.RE_URL = exports.RE_ZONE_STRING = exports.RE_SUBNET_STRING = exports.RE_BAD_ADDRESS = exports.RE_BAD_CHARACTERS = exports.TYPES = exports.SCOPES = exports.GROUPS = exports.BITS = void 0;
+exports.BITS = 128;
+exports.GROUPS = 8;
+/**
+ * Represents IPv6 address scopes
+ * @memberof Address6
+ * @static
+ */
+exports.SCOPES = {
+ 0: 'Reserved',
+ 1: 'Interface local',
+ 2: 'Link local',
+ 4: 'Admin local',
+ 5: 'Site local',
+ 8: 'Organization local',
+ 14: 'Global',
+ 15: 'Reserved',
+};
+/**
+ * Represents IPv6 address types
+ * @memberof Address6
+ * @static
+ */
+exports.TYPES = {
+ 'ff01::1/128': 'Multicast (All nodes on this interface)',
+ 'ff01::2/128': 'Multicast (All routers on this interface)',
+ 'ff02::1/128': 'Multicast (All nodes on this link)',
+ 'ff02::2/128': 'Multicast (All routers on this link)',
+ 'ff05::2/128': 'Multicast (All routers in this site)',
+ 'ff02::5/128': 'Multicast (OSPFv3 AllSPF routers)',
+ 'ff02::6/128': 'Multicast (OSPFv3 AllDR routers)',
+ 'ff02::9/128': 'Multicast (RIP routers)',
+ 'ff02::a/128': 'Multicast (EIGRP routers)',
+ 'ff02::d/128': 'Multicast (PIM routers)',
+ 'ff02::16/128': 'Multicast (MLDv2 reports)',
+ 'ff01::fb/128': 'Multicast (mDNSv6)',
+ 'ff02::fb/128': 'Multicast (mDNSv6)',
+ 'ff05::fb/128': 'Multicast (mDNSv6)',
+ 'ff02::1:2/128': 'Multicast (All DHCP servers and relay agents on this link)',
+ 'ff05::1:2/128': 'Multicast (All DHCP servers and relay agents in this site)',
+ 'ff02::1:3/128': 'Multicast (All DHCP servers on this link)',
+ 'ff05::1:3/128': 'Multicast (All DHCP servers in this site)',
+ '::/128': 'Unspecified',
+ '::1/128': 'Loopback',
+ 'ff00::/8': 'Multicast',
+ 'fe80::/10': 'Link-local unicast',
+ 'fc00::/7': 'Unique local',
+ '2002::/16': '6to4',
+ '2001:db8::/32': 'Documentation',
+ '64:ff9b::/96': 'NAT64 (well-known)',
+ '64:ff9b:1::/48': 'NAT64 (local-use)',
+};
+/**
+ * A regular expression that matches bad characters in an IPv6 address
+ * @memberof Address6
+ * @static
+ */
+exports.RE_BAD_CHARACTERS = /([^0-9a-f:/%])/gi;
+/**
+ * A regular expression that matches an incorrect IPv6 address
+ * @memberof Address6
+ * @static
+ */
+exports.RE_BAD_ADDRESS = /([0-9a-f]{5,}|:{3,}|[^:]:$|^:[^:]|\/$)/gi;
+/**
+ * A regular expression that matches an IPv6 subnet
+ * @memberof Address6
+ * @static
+ */
+exports.RE_SUBNET_STRING = /\/\d{1,3}(?=%|$)/;
+/**
+ * A regular expression that matches an IPv6 zone
+ * @memberof Address6
+ * @static
+ */
+exports.RE_ZONE_STRING = /%.*$/;
+exports.RE_URL = /^\[{0,1}([0-9a-f:]+)\]{0,1}/;
+exports.RE_URL_WITH_PORT = /\[([0-9a-f:]+)\]:([0-9]{1,5})/;
+
+},{}],7:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.escapeHtml = escapeHtml;
+exports.spanAllZeroes = spanAllZeroes;
+exports.spanAll = spanAll;
+exports.spanLeadingZeroes = spanLeadingZeroes;
+exports.simpleGroup = simpleGroup;
+function escapeHtml(s) {
+ return s
+ .replace(/&/g, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;')
+ .replace(/"/g, '&quot;')
+ .replace(/'/g, '&#39;');
+}
+/**
+ * @returns {String} the string with all zeroes contained in a <span>
+ */
+function spanAllZeroes(s) {
+ return escapeHtml(s).replace(/(0+)/g, '<span class="zero">$1</span>');
+}
+/**
+ * @returns {String} the string with each character contained in a <span>
+ */
+function spanAll(s, offset = 0) {
+ const letters = s.split('');
+ return letters
+ .map((n, i) => `<span class="digit value-${escapeHtml(n)} position-${i + offset}">${spanAllZeroes(n)}</span>`)
+ .join('');
+}
+function spanLeadingZeroesSimple(group) {
+ return escapeHtml(group).replace(/^(0+)/, '<span class="zero">$1</span>');
+}
+/**
+ * @returns {String} the string with leading zeroes contained in a <span>
+ */
+function spanLeadingZeroes(address) {
+ const groups = address.split(':');
+ return groups.map((g) => spanLeadingZeroesSimple(g)).join(':');
+}
+/**
+ * Groups an address
+ * @returns {String} a grouped address
+ */
+function simpleGroup(addressString, offset = 0) {
+ const groups = addressString.split(':');
+ return groups.map((g, i) => {
+ if (/group-v4/.test(g)) {
+ return g;
+ }
+ return `<span class="hover-group group-${i + offset}">${spanLeadingZeroesSimple(g)}</span>`;
+ });
+}
+
+},{}],8:[function(require,module,exports){
+"use strict";
+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ var desc = Object.getOwnPropertyDescriptor(m, k);
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
+ desc = { enumerable: true, get: function() { return m[k]; } };
+ }
+ Object.defineProperty(o, k2, desc);
+}) : (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ o[k2] = m[k];
+}));
+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
+}) : function(o, v) {
+ o["default"] = v;
+});
+var __importStar = (this && this.__importStar) || function (mod) {
+ if (mod && mod.__esModule) return mod;
+ var result = {};
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
+ __setModuleDefault(result, mod);
+ return result;
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.ADDRESS_BOUNDARY = void 0;
+exports.groupPossibilities = groupPossibilities;
+exports.padGroup = padGroup;
+exports.simpleRegularExpression = simpleRegularExpression;
+exports.possibleElisions = possibleElisions;
+const v6 = __importStar(require("./constants"));
+function groupPossibilities(possibilities) {
+ return `(${possibilities.join('|')})`;
+}
+function padGroup(group) {
+ if (group.length < 4) {
+ return `0{0,${4 - group.length}}${group}`;
+ }
+ return group;
+}
+exports.ADDRESS_BOUNDARY = '[^A-Fa-f0-9:]';
+function simpleRegularExpression(groups) {
+ const zeroIndexes = [];
+ groups.forEach((group, i) => {
+ const groupInteger = parseInt(group, 16);
+ if (groupInteger === 0) {
+ zeroIndexes.push(i);
+ }
+ });
+ // You can technically elide a single 0, this creates the regular expressions
+ // to match that eventuality
+ const possibilities = zeroIndexes.map((zeroIndex) => groups
+ .map((group, i) => {
+ if (i === zeroIndex) {
+ const elision = i === 0 || i === v6.GROUPS - 1 ? ':' : '';
+ return groupPossibilities([padGroup(group), elision]);
+ }
+ return padGroup(group);
+ })
+ .join(':'));
+ // The simplest case
+ possibilities.push(groups.map(padGroup).join(':'));
+ return groupPossibilities(possibilities);
+}
+function possibleElisions(elidedGroups, moreLeft, moreRight) {
+ const left = moreLeft ? '' : ':';
+ const right = moreRight ? '' : ':';
+ const possibilities = [];
+ // 1. elision of everything (::)
+ if (!moreLeft && !moreRight) {
+ possibilities.push('::');
+ }
+ // 2. complete elision of the middle
+ if (moreLeft && moreRight) {
+ possibilities.push('');
+ }
+ if ((moreRight && !moreLeft) || (!moreRight && moreLeft)) {
+ // 3. complete elision of one side
+ possibilities.push(':');
+ }
+ // 4. elision from the left side
+ possibilities.push(`${left}(:0{1,4}){1,${elidedGroups - 1}}`);
+ // 5. elision from the right side
+ possibilities.push(`(0{1,4}:){1,${elidedGroups - 1}}${right}`);
+ // 6. no elision
+ possibilities.push(`(0{1,4}:){${elidedGroups - 1}}0{1,4}`);
+ // 7. elision (including sloppy elision) from the middle
+ for (let groups = 1; groups < elidedGroups - 1; groups++) {
+ for (let position = 1; position < elidedGroups - groups; position++) {
+ possibilities.push(`(0{1,4}:){${position}}:(0{1,4}:){${elidedGroups - position - groups - 1}}0{1,4}`);
+ }
+ }
+ return groupPossibilities(possibilities);
+}
+
+},{"./constants":6}],"ip-address":[function(require,module,exports){
+"use strict";
+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ var desc = Object.getOwnPropertyDescriptor(m, k);
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
+ desc = { enumerable: true, get: function() { return m[k]; } };
+ }
+ Object.defineProperty(o, k2, desc);
+}) : (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ o[k2] = m[k];
+}));
+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
+}) : function(o, v) {
+ o["default"] = v;
+});
+var __importStar = (this && this.__importStar) || function (mod) {
+ if (mod && mod.__esModule) return mod;
+ var result = {};
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
+ __setModuleDefault(result, mod);
+ return result;
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.v6 = exports.AddressError = exports.Address6 = exports.Address4 = void 0;
+var ipv4_1 = require("./ipv4");
+Object.defineProperty(exports, "Address4", { enumerable: true, get: function () { return ipv4_1.Address4; } });
+var ipv6_1 = require("./ipv6");
+Object.defineProperty(exports, "Address6", { enumerable: true, get: function () { return ipv6_1.Address6; } });
+var address_error_1 = require("./address-error");
+Object.defineProperty(exports, "AddressError", { enumerable: true, get: function () { return address_error_1.AddressError; } });
+const helpers = __importStar(require("./v6/helpers"));
+exports.v6 = { helpers };
+
+},{"./address-error":1,"./ipv4":3,"./ipv6":4,"./v6/helpers":7}]},{},[]);
diff --git a/src/assets/js/main.js b/src/assets/js/main.js
new file mode 100644
index 0000000..ef57764
--- /dev/null
+++ b/src/assets/js/main.js
@@ -0,0 +1,1117 @@
+var iface = 'wg0';
+
+function parse_placeholder(val) {
+ return val;
+}
+
+function parse_boolean(val) {
+ if (val === 'true')
+ return true;
+ if (val === 'false')
+ return false;
+ throw new Error('Cannot parse as a boolean.');
+}
+
+function parse_boolean_or_string(val) {
+ if (typeof val == 'boolean') {
+ return val;
+ }
+ return parse_boolean(val);
+}
+
+function try_parse_boolean_or_string(val) {
+ try {
+ return parse_boolean_or_string(val);
+ } catch (err) {
+ return null;
+ }
+}
+
+function parse_positive_integer(src) {
+ var val = parseInt(src);
+ if (isNaN(val) || val < 1)
+ throw new Error('Cannot parse as a positive integer.');
+ return val;
+}
+
+function parse_keepalive(src) {
+ return parse_positive_integer(src);
+}
+
+function parse_tunnel_everything(src) {
+ return parse_boolean_or_string(src);
+}
+
+function parse_endpoint(val) {
+ val = val.trim();
+ if (val.length == 0)
+ throw new Error('Server endpoint cannot be an empty string.');
+ if (!val.match(/^.+:[0-9]+$/))
+ throw new Error('Please specify a host and a port in the HOST:PORT format.');
+ return val;
+}
+
+function parse_key(val) {
+ val = val.trim();
+ if (val.length == 0)
+ throw new Error('Key as used by WireGuard cannot be an empty string.');
+ if (!val.match(/^[0-9a-zA-Z+/=]+$/))
+ throw new Error('Key as used by WireGuard must be Base64-encoded.');
+ return val;
+}
+
+var ip_address_lib = require('ip-address');
+
+function parse_ip_internal(src, parser) {
+ src = src.trim();
+ if (src.length == 0)
+ throw new Error('IP address cannot be an empty string.');
+
+ try {
+ var result = new parser(src);
+ } catch (err) {
+ throw new Error('Sorry, IP address validation failed with the following error:\n' + err.message);
+ }
+ return result;
+}
+
+function parse_ip(src, parser) {
+ var result = parse_ip_internal(src, parser);
+
+ if (result.parsedSubnet)
+ throw new Error('Please specify address without the netmask.')
+
+ return result.correctForm();
+}
+
+function parse_ip_cidr(src, parser) {
+ var result = parse_ip_internal(src, parser);
+
+ if (!result.parsedSubnet)
+ throw new Error('Please specify address using the CIDR format (including the netmask).');
+
+ var addr = result.correctForm();
+ var netmask = result.subnetMask;
+ var network_id = result.startAddress().correctForm();
+
+ return [addr, netmask, network_id];
+}
+
+function parse_ipv4(src) {
+ var result = parse_ip(src, ip_address_lib.Address4);
+ return result;
+}
+
+function parse_ipv4_cidr(src) {
+ var parser = ip_address_lib.Address4;
+ var result = parse_ip_cidr(src, ip_address_lib.Address4);
+
+ var addr = result[0];
+ var netmask = result[1];
+ var network_id = result[2];
+ var broadcast_addr = new parser(src).endAddress().correctForm();
+
+ if (addr == network_id && netmask != 32)
+ throw new Error('Please use a real host IP address, not a network identifier.');
+ if (addr == broadcast_addr && netmask != 32)
+ throw new Error('Please use a real host IP address, not a broadcast address.');
+
+ return result;
+}
+
+function parse_ipv6(val) {
+ return parse_ip(val, ip_address_lib.Address6);
+}
+
+function parse_ipv6_cidr(val) {
+ return parse_ip_cidr(val, ip_address_lib.Address6);
+}
+
+function parse_ip_list(val, addr_parser) {
+ var parts = val.split(/, */);
+ var result = [];
+ parts.forEach(function(part) {
+ result.push(addr_parser(part));
+ });
+ return result;
+}
+
+var Input = function(name) {
+ this.name = name;
+
+ if (this.is_checkbox()) {
+ // Keep the checked property and the value consistent.
+ $(this.input_id()).change(function(evt) {
+ var elem = evt.target;
+ elem.value = elem.checked;
+ });
+ }
+}
+
+Input.prototype.input_id = function() {
+ return '#param_' + this.name;
+}
+
+Input.prototype.error_id = function() {
+ return this.input_id() + '_error';
+}
+
+Input.prototype.container_id = function() {
+ return this.input_id() + '_container';
+}
+
+Input.prototype.is_checkbox = function() {
+ return $(this.input_id()).attr('type') == 'checkbox';
+}
+
+Input.prototype.value = function() {
+ return $(this.input_id()).val();
+}
+
+Input.prototype.has_value = function() {
+ return this.value().length > 0;
+}
+
+Input.prototype.set = function(value) {
+ if (this.is_checkbox() && try_parse_boolean_or_string(value))
+ $(this.input_id()).prop('checked', true);
+ $(this.input_id()).val(value);
+}
+
+Input.prototype.show = function() {
+ $(this.container_id()).show();
+}
+
+Input.prototype.hide = function() {
+ $(this.container_id()).hide();
+}
+
+Input.prototype.show_error = function(msg) {
+ this.show();
+ $(this.error_id()).text(msg);
+ $(this.error_id()).show();
+}
+
+Input.prototype.hide_error = function() {
+ $(this.error_id()).hide();
+}
+
+var Value = function(name, parser) {
+ this.name = name;
+ this.input = new Input(name);
+ this.parser = parser;
+ this.optional = false;
+ this.advanced = false;
+
+ this.value = null;
+ this.error = null;
+}
+
+Value.prototype.parse = function() {
+ return this.parse_from(this.input.value());
+}
+
+Value.prototype.parse_from = function(src) {
+ try {
+ var value = src;
+ switch (typeof value) {
+ case 'boolean':
+ value = this.parser(value);
+ break;
+ default:
+ if (this.optional && value.length == 0)
+ break;
+ value = this.parser(value);
+ break;
+ }
+ this.input.set(src);
+ this.set_value(value);
+ return true;
+ } catch (err) {
+ this.set_error(err.message);
+ return false;
+ }
+}
+
+Value.prototype.set_value = function(value) {
+ this.value = value;
+ this.error = null;
+ this.input.hide_error();
+}
+
+Value.prototype.set_error = function(msg) {
+ this.value = null;
+ this.error = msg;
+ this.input.show_error(this.error);
+}
+
+Value.prototype.is_set = function() {
+ return this.input.has_value();
+}
+
+Value.prototype.show = function() {
+ this.input.show();
+}
+
+Value.prototype.hide = function() {
+ this.input.hide();
+}
+
+var Endpoint = function(name) {
+ Value.call(this, name, parse_endpoint);
+}
+
+Endpoint.prototype = Object.create(Value.prototype);
+Endpoint.prototype.constructor = Endpoint;
+
+var Key = function(name) {
+ Value.call(this, name, parse_key);
+}
+
+Key.prototype = Object.create(Value.prototype);
+Key.prototype.constructor = Key;
+
+var IPAddr = function(name, parser) {
+ Value.call(this, name, parser);
+}
+
+IPAddr.prototype = Object.create(Value.prototype);
+IPAddr.prototype.constructor = IPAddr;
+
+IPAddr.prototype.set_value = function(value) {
+ Value.prototype.set_value.call(this, value);
+
+ this.addr = this.value[0];
+ this.netmask = this.value[1];
+ this.network_id = this.value[2];
+}
+
+IPAddr.prototype.full_address = function() {
+ return this.addr + '/' + this.netmask;
+}
+
+IPAddr.prototype.allowed_ips_client = function() {
+ // As of this writing, _every_ tool accepts something like 192.168.0.1/24
+ // here, _except_ for the Android app, which _requires_ the least
+ // significant bits to be zeroed out.
+ return this.network_id + '/' + this.netmask;
+}
+
+var IPv4 = function(name) {
+ IPAddr.call(this, name, parse_ipv4_cidr);
+}
+
+IPv4.prototype = Object.create(IPAddr.prototype);
+IPv4.prototype.constructor = IPv4;
+
+IPv4.prototype.allowed_ips_server = function() {
+ return this.addr + '/' + 32;
+}
+
+var IPv6 = function(name) {
+ IPAddr.call(this, name, parse_ipv6_cidr);
+}
+
+IPv6.prototype = Object.create(IPAddr.prototype);
+IPv6.prototype.constructor = IPv6;
+
+IPv6.prototype.allowed_ips_server = function() {
+ return this.addr + '/' + 128;
+}
+
+var Keepalive = function(name) {
+ Value.call(this, name, parse_keepalive);
+ this.optional = true;
+ this.advanced = true;
+}
+
+Keepalive.prototype = Object.create(Value.prototype);
+Keepalive.prototype.constructor = Keepalive;
+
+var TunnelEverything = function(name) {
+ Value.call(this, name, parse_tunnel_everything);
+ this.optional = true;
+ this.advanced = true;
+}
+
+TunnelEverything.prototype = Object.create(Value.prototype);
+TunnelEverything.prototype.constructor = TunnelEverything;
+
+var IPAddrList = function(name, parser) {
+ Value.call(this, name, function(val) {
+ return parse_ip_list(val, parser);
+ });
+}
+
+IPAddrList.prototype = Object.create(Value.prototype);
+IPAddrList.prototype.constructor = IPAddrList;
+
+var DNS_IPv4 = function(name) {
+ IPAddrList.call(this, name, parse_ipv4);
+ this.optional = true;
+ this.advanced = true;
+}
+
+DNS_IPv4.prototype = Object.create(IPAddrList.prototype);
+DNS_IPv4.prototype.constructor = DNS_IPv4;
+
+var DNS_IPv6 = function(name) {
+ IPAddrList.call(this, name, parse_ipv6);
+ this.optional = true;
+ this.advanced = true;
+}
+
+DNS_IPv6.prototype = Object.create(IPAddrList.prototype);
+DNS_IPv6.prototype.constructor = DNS_IPv6;
+
+var Data = function() {
+ this.server_public = new Key('server_public');
+ this.server_endpoint = new Endpoint('server_endpoint');
+ this.preshared = new Key('preshared');
+ this.client_public = new Key('client_public');
+ this.client_private = new Key('client_private');
+ this.client_ipv4 = new IPv4('client_ipv4');
+ this.client_ipv6 = new IPv6('client_ipv6');
+ this.keepalive = new Keepalive('keepalive');
+ this.tunnel_everything = new TunnelEverything('tunnel_everything');
+ this.dns_ipv4 = new DNS_IPv4('dns_ipv4');
+ this.dns_ipv6 = new DNS_IPv6('dns_ipv6');
+
+ this.values = [
+ this.server_public,
+ this.server_endpoint,
+ this.preshared,
+ this.client_public,
+ this.client_private,
+ this.client_ipv4,
+ this.client_ipv6,
+ this.keepalive,
+ this.tunnel_everything,
+ this.dns_ipv4,
+ this.dns_ipv6
+ ];
+}
+
+Data.prototype.set_from_url = function(url) {
+ var url = new URL(url);
+ var params = new URLSearchParams(url.search);
+ var has = false;
+
+ this.values.forEach(function(value) {
+ if (params.has(value.name)) {
+ has = true;
+ value.input.set(params.get(value.name));
+ }
+ });
+
+ return has;
+}
+
+Data.prototype.set_from_this_url = function() {
+ return this.set_from_url(window.location.href);
+};
+
+Data.prototype.get_perma_url = function() {
+ var url = new URL(window.location.href);
+ url.search = '';
+ var params = new URLSearchParams();
+ this.values.forEach(function(value) {
+ params.append(value.name, value.input.value());
+ });
+ url.search = params.toString();
+ return url.toString();
+}
+
+Data.prototype.parse = function() {
+ var success = true;
+
+ this.values.forEach(function(value) {
+ if (!value.parse())
+ success = false;
+ });
+
+ if (success) {
+ if (!$('#advanced_params').prop('checked'))
+ this.hide_advanced();
+ this.hide_error();
+ } else {
+ this.show_error();
+ }
+ return success;
+}
+
+Data.prototype.show_error = function() {
+ $('#params_error').text('Please correct the input errors above first.');
+ $('#params_error').show();
+}
+
+Data.prototype.hide_error = function() {
+ $('#params_error').hide();
+}
+
+Data.prototype.show_advanced = function() {
+ this.values.forEach(function(value) {
+ if (!value.advanced)
+ return;
+ value.show();
+ });
+}
+
+Data.prototype.hide_advanced = function() {
+ this.values.forEach(function(value) {
+ if (!value.advanced)
+ return;
+ value.hide();
+ });
+}
+
+function basename(path) {
+ return path.substring(path.lastIndexOf('/') + 1);
+}
+
+function download(file_name, text) {
+ // The type must be application/octet-stream; if it's text/plain, the
+ // Android Chrome appends the .txt suffix to the downloaded file names.
+ var blob = new Blob([text], {type: 'application/octet-stream'});
+ var url = URL.createObjectURL(blob);
+
+ var link = $('<a/>');
+ link.prop('href', url);
+ link.prop('download', file_name);
+
+ // Whoever thought of this [0] is fucking crazy:
+ // https://stackoverflow.com/a/36483380/514684
+ // It literally silently does nothing if this is omitted.
+ link[0].click();
+}
+
+function copy_to_clipboard(elem) {
+ // This is rather neat, thanks:
+ // https://stackoverflow.com/a/22581382/514684
+
+ // Apparently, you can only do execCommand('copy') from <input> and
+ // <textarea> elements. Another stupidity, I'm so surprised.
+ var is_input = elem.tagName === 'INPUT' || elem.tagName === 'TEXTAREA';
+ var orig_select_start, orig_select_end;
+
+ if (is_input) {
+ var target = elem;
+ orig_select_start = elem.selectionStart;
+ orig_select_end = elem.selectionEnd;
+ } else {
+ var target = document.createElement('textarea');
+ target.textContent = elem.textContent;
+ document.body.appendChild(target);
+ }
+
+ var current_focus = document.activeElement;
+ target.focus();
+ target.setSelectionRange(0, target.value.length);
+
+ var succeed;
+ try {
+ // Supposedly, this is deprecated. But the "proper" method sucks balls,
+ // so I'm using this one:
+ // https://stackoverflow.com/a/30810322/514684
+ succeed = document.execCommand('copy');
+ } catch (e) {
+ succeed = false;
+ }
+
+ if (current_focus && typeof current_focus.focus === "function") {
+ current_focus.focus();
+ }
+
+ if (is_input) {
+ elem.setSelectionRange(orig_select_start, orig_select_end);
+ } else {
+ document.body.removeChild(target);
+ }
+ return succeed;
+}
+
+var OverlayButton = function(title, icon, click_handler) {
+ this.btn = $('<button/>', {
+ 'class': 'btn btn-default',
+ type: 'button',
+ on: {
+ click: click_handler
+ }
+ });
+ this.title = title;
+ this.icon = icon;
+ this.show_icon(title, icon);
+}
+
+OverlayButton.prototype.show_icon = function(title, icon) {
+ this.btn.empty();
+ this.btn.prop('title', title);
+ this.btn.append($('<span/>', {
+ 'class': `glyphicon glyphicon-${icon}`
+ }));
+}
+
+var EditButton = function(cfg) {
+ var self = this;
+ OverlayButton.call(this, 'Edit', 'pencil', function(evt) {
+ return self.on_click(cfg);
+ });
+}
+
+EditButton.prototype = Object.create(OverlayButton.prototype);
+EditButton.prototype.constructor = EditButton;
+
+EditButton.prototype.on_click = function(cfg) {
+ var editable = cfg.pre.prop('isContentEditable');
+ cfg.pre.prop('contentEditable', !editable);
+ if (editable) {
+ this.show_icon(this.title, this.icon);
+ this.btn.blur(); // a.k.a. unfocus
+ } else {
+ this.show_icon('Save', 'floppy-disk');
+ cfg.pre.focus();
+ }
+}
+
+var DownloadButton = function(cfg) {
+ var self = this;
+ OverlayButton.call(this, 'Download', 'download-alt', function(evt) {
+ return self.on_click(cfg);
+ });
+}
+
+DownloadButton.prototype = Object.create(OverlayButton.prototype);
+DownloadButton.prototype.constructor = DownloadButton;
+
+DownloadButton.prototype.on_click = function(cfg) {
+ return download(basename(cfg.name), cfg.pre.text());
+}
+
+var CopyButton = function(cfg) {
+ var self = this;
+ OverlayButton.call(this, 'Copy', 'copy', function(evt) {
+ return self.on_click(cfg);
+ });
+}
+
+CopyButton.prototype = Object.create(OverlayButton.prototype);
+CopyButton.prototype.constructor = CopyButton;
+
+CopyButton.prototype.on_click = function(cfg) {
+ return copy_to_clipboard(cfg.pre[0]);
+}
+
+var ConfigFile = function(name, text) {
+ this.name = name;
+ this.text = text;
+ this.pre = $('<pre/>').text(text);
+}
+
+ConfigFile.prototype.format = function() {
+ var buttons = [
+ new CopyButton(this),
+ new DownloadButton(this),
+ new EditButton(this)
+ ];
+
+ var overlay = $('<div class="pre_buttons btn-group btn-group-xs" role="group"/>');
+ buttons.forEach(function(btn) {
+ overlay.append(btn.btn);
+ });
+
+ return $('<div class="pre_container"/>')
+ .append(this.pre)
+ .append(overlay);
+}
+
+var QRCode = function(pre) {
+ this.pre = pre;
+}
+
+function draw_qr_code(container, text) {
+ var canvas = $('<canvas/>');
+ var qr = new QRious({
+ element: canvas[0],
+ value: text,
+ size: 350
+ });
+ container.append(canvas);
+}
+
+QRCode.prototype.format = function() {
+ var container = $('<div class="text-center"/>');
+ var pre = this.pre;
+ pre.on('blur', function() {
+ container.empty();
+ draw_qr_code(container, pre.text());
+ });
+ draw_qr_code(container, pre.text());
+ return container;
+}
+
+var catchall_ipv4 = '0.0.0.0/0';
+var catchall_ipv6 = '::/0';
+
+function wg_quick_client_file(data) {
+ var path = `/etc/wireguard/${iface}.conf`;
+
+ var dns = '';
+ var allowed_ips = `${data.client_ipv4.allowed_ips_client()}, ${data.client_ipv6.allowed_ips_client()}`;
+ var keepalive = '';
+
+ if (data.tunnel_everything.value) {
+ dns = `DNS = ${(data.dns_ipv4.value.concat(data.dns_ipv6.value)).join(',')}\n`;
+ allowed_ips = `${catchall_ipv4}, ${catchall_ipv6}`;
+ }
+ if (data.keepalive.is_set()) {
+ keepalive = `PersistentKeepalive = ${data.keepalive.value}\n`;
+ }
+
+ var contents =
+`# On the client, put this to
+# ${path}
+# and either
+# * start the wg-quick@${iface} systemd service,
+# * or run \`wg-quick up ${iface}\`.
+
+[Interface]
+PrivateKey = ${data.client_private.value}
+Address = ${data.client_ipv4.full_address()}, ${data.client_ipv6.full_address()}
+${dns}
+[Peer]
+Endpoint = ${data.server_endpoint.value}
+PublicKey = ${data.server_public.value}
+PresharedKey = ${data.preshared.value}
+AllowedIPs = ${allowed_ips}
+${keepalive}`;
+
+ return new ConfigFile(path, contents);
+}
+
+function wg_quick_server_file(data) {
+ var path = `/etc/wireguard/${iface}.conf`;
+
+ var contents =
+`# On the server, add this to
+# ${path}
+# and restart the wg-quick@${iface} systemd service.
+
+# Previous contents goes here...
+
+[Peer]
+PublicKey = ${data.client_public.value}
+PresharedKey = ${data.preshared.value}
+AllowedIPs = ${data.client_ipv4.allowed_ips_server()}, ${data.client_ipv6.allowed_ips_server()}
+`;
+
+ return new ConfigFile(path, contents);
+}
+
+function systemd_client_netdev_file(data) {
+ var path = `/etc/systemd/network/${iface}.netdev`;
+
+ var fwmark = '';
+ var allowed_ips = `${data.client_ipv4.allowed_ips_client()}, ${data.client_ipv6.allowed_ips_client()}`;
+ var keepalive = '';
+
+ if (data.tunnel_everything.value) {
+ fwmark = `FirewallMark = 0x8888\n`;
+ allowed_ips = `${catchall_ipv4}, ${catchall_ipv6}`;
+ }
+ if (data.keepalive.is_set()) {
+ keepalive = `PersistentKeepalive = ${data.keepalive.value}\n`;
+ }
+
+ var contents =
+`# On the client, you need two files. Put this into
+# ${path}
+# and after you're done with both files, run
+# systemctl daemon-reload
+# systemctl restart systemd-networkd
+
+[NetDev]
+Name = ${iface}
+Kind = wireguard
+
+[WireGuard]
+PrivateKey = ${data.client_private.value}
+${fwmark}
+[WireGuardPeer]
+Endpoint = ${data.server_endpoint.value}
+PublicKey = ${data.server_public.value}
+PresharedKey = ${data.preshared.value}
+AllowedIPs = ${allowed_ips}
+${keepalive}`;
+
+ return new ConfigFile(path, contents);
+}
+
+function systemd_client_network_file(data) {
+ var path = `/etc/systemd/network/${iface}.network`;
+
+ var dns = '';
+
+ if (data.tunnel_everything.value) {
+ data.dns_ipv4.value.forEach(function(val) {
+ dns += `DNS = ${val}\n`;
+ });
+ data.dns_ipv6.value.forEach(function(val) {
+ dns += `DNS = ${val}\n`;
+ });
+ dns +=
+`Domains = ~.
+DNSDefaultRoute = true
+
+[RoutingPolicyRule]
+FirewallMark = 0x8888
+InvertRule = true
+Table = 1000
+Priority = 10
+`;
+ }
+
+ var contents =
+`# This is the second file. Put this into
+# ${path}
+# and if you're done with the first file already, run
+# systemctl daemon-reload
+# systemctl restart systemd-networkd
+
+[Match]
+Name = ${iface}
+
+[Network]
+Address = ${data.client_ipv4.full_address()}
+Address = ${data.client_ipv6.full_address()}
+${dns}`;
+
+ // I know the above doesn't work, but I don't care, systemd-networkd is a
+ // fucking nightmare.
+ return new ConfigFile(path, contents);
+}
+
+function systemd_server_netdev_file(data) {
+ var path = `/etc/systemd/network/${iface}.netdev`;
+
+ var contents =
+`# On the server, add this to
+# ${path}
+# and run
+# systemctl daemon-reload
+# systemctl restart systemd-networkd
+
+# Previous contents goes here...
+
+[WireGuardPeer]
+PublicKey = ${data.client_public.value}
+PresharedKey = ${data.preshared.value}
+AllowedIPs = ${data.client_ipv4.allowed_ips_server()}, ${data.client_ipv6.allowed_ips_server()}
+`;
+
+ return new ConfigFile(path, contents);
+}
+
+function nmcli_client_file(data) {
+ var path = `/etc/NetworkManager/system-connections/${iface}.nmconnection`;
+
+ var allowed_ips = `${data.client_ipv4.allowed_ips_client()};${data.client_ipv6.allowed_ips_client()};`;
+ var dns4 = '';
+ var dns6 = '';
+ var keepalive = '';
+
+ if (data.tunnel_everything.value) {
+ allowed_ips = `${catchall_ipv4};${catchall_ipv6};`;
+ dns4 =
+`dns=${data.dns_ipv4.value.join(';')};
+dns-priority=-10
+dns-search=~;
+`;
+ dns6 =
+`dns=${data.dns_ipv6.value.join(';')};
+dns-priority=-10
+dns-search=~;
+`;
+ }
+ if (data.keepalive.is_set()) {
+ keepalive = `persistent-keepalive=${data.keepalive.value}\n`;
+ }
+
+ var contents =
+`# On the client, put this to
+# ${path}
+# and run
+# chmod 0600 ${path}
+# nmcli c reload
+# nmcli c up ${iface}
+
+[connection]
+id=${iface}
+type=wireguard
+interface-name=${iface}
+
+[wireguard]
+private-key=${data.client_private.value}
+private-key-flags=0
+
+[wireguard-peer.${data.server_public.value}]
+endpoint=${data.server_endpoint.value}
+preshared-key=${data.preshared.value}
+preshared-key-flags=0
+allowed-ips=${allowed_ips}
+${keepalive}
+[ipv4]
+address1=${data.client_ipv4.full_address()}
+method=manual
+${dns4}
+[ipv6]
+addr-gen-mode=stable-privacy
+address1=${data.client_ipv6.full_address()}
+method=manual
+${dns6}`;
+
+ return new ConfigFile(path, contents);
+}
+
+function nmcli_server_file(data) {
+ var path = `/etc/NetworkManager/system-connections/${iface}.nmconnection`;
+
+ var contents =
+`# On the server, add this to
+# ${path}
+# and run
+# nmcli c reload
+# nmcli c up ${iface}
+
+# Previous contents goes here...
+
+[wireguard-peer.${data.client_public.value}]
+preshared-key=${data.preshared.value}
+preshared-key-flags=0
+allowed-ips=${data.client_ipv4.allowed_ips_server()};${data.client_ipv6.allowed_ips_server()};
+`;
+
+ return new ConfigFile(path, contents);
+}
+
+var shell_info =
+`# Make sure your .bash_history or whatever is
+# secure before running this.
+# Better yet, put into a file and run (possibly, using sudo).`;
+
+function manual_client_script(data) {
+ var path = 'client_setup.sh';
+
+ var allowed_ips = `${data.client_ipv4.allowed_ips_client()},${data.client_ipv6.allowed_ips_client()}`;
+ var keepalive = '';
+
+ if (data.tunnel_everything.value) {
+ allowed_ips = `${catchall_ipv4},${catchall_ipv6}`;
+ }
+ if (data.keepalive.is_set()) {
+ keepalive = ` persistent-keepalive ${data.keepalive.value} \\\n`;
+ }
+
+ var contents =
+`# On the client, run this to set up a connection.
+${shell_info}
+
+ip link add dev ${iface} type wireguard
+ip addr add ${data.client_ipv4.full_address()} dev ${iface}
+ip addr add ${data.client_ipv6.full_address()} dev ${iface}
+wg set ${iface} \\
+ private-key <( echo ${data.client_private.value} )
+wg set ${iface} \\
+ peer ${data.server_public.value} \\
+ preshared-key <( echo ${data.preshared.value} ) \\
+${keepalive} endpoint ${data.server_endpoint.value} \\
+ allowed-ips ${allowed_ips}
+ip link set ${iface} up
+`;
+
+ return new ConfigFile(path, contents);
+}
+
+function manual_server_script(data) {
+ var path = 'server_setup.sh';
+
+ var contents =
+`# On the server, run this to tweak an existing connection.
+${shell_info}
+
+wg set ${iface} \\
+ peer ${data.client_public.value} \\
+ preshared-key <( echo ${data.preshared.value} ) \\
+ allowed-ips ${data.client_ipv4.allowed_ips_server()},${data.client_ipv6.allowed_ips_server()}
+`;
+
+ return new ConfigFile(path, contents);
+}
+
+var Guide = function(name) {
+ this.name = name;
+}
+
+Guide.prototype.format = function(data) {
+ var container = $('<div/>');
+ container.append($('<h2/>').html(this.name));
+ return container;
+}
+
+var GuidePermaURL = function() {
+ Guide.call(this, 'Permanent URL');
+}
+
+GuidePermaURL.prototype = Object.create(Guide.prototype);
+GuidePermaURL.prototype.constructor = GuidePermaURL;
+
+GuidePermaURL.prototype.format = function(data) {
+ var container = Guide.prototype.format.call(this, data);
+ container
+ .append($('<p/>').text('Use the following URL to share this configuration. Be careful: it probably contains sensitive data, like your private keys.'))
+ .append($('<pre/>').append($('<a/>', {href: data.get_perma_url()}).text(data.get_perma_url())));
+ return container;
+}
+
+var GuideServerClient = function(name) {
+ Guide.call(this, name);
+}
+
+GuideServerClient.prototype = Object.create(Guide.prototype);
+GuideServerClient.prototype.constructor = GuideServerClient;
+
+GuideServerClient.prototype.for_server = function(data) { return []; }
+GuideServerClient.prototype.for_client = function(data) { return []; }
+
+GuideServerClient.prototype.join_guides = function(guides) {
+ var container = $('<div/>');
+ guides.forEach(function(guide) {
+ container.append(guide.format());
+ });
+ return container;
+}
+
+GuideServerClient.prototype.format = function(data) {
+ var container = Guide.prototype.format.call(this, data);
+ container
+ .append($('<div class="row"/>')
+ .append($('<div class="col-md-6"/>')
+ .append(this.join_guides(this.for_server(data))))
+ .append($('<div class="col-md-6"/>')
+ .append(this.join_guides(this.for_client(data)))));
+ return container;
+}
+
+var GuideWgQuick = function() {
+ GuideServerClient.call(this, 'wg-quick');
+}
+
+GuideWgQuick.prototype = Object.create(GuideServerClient.prototype);
+GuideWgQuick.prototype.constructor = GuideWgQuick;
+
+GuideWgQuick.prototype.for_client = function(data) {
+ var config = wg_quick_client_file(data);
+ var qr = new QRCode(config.pre);
+ return [config, qr];
+}
+
+GuideWgQuick.prototype.for_server = function(data) {
+ return [wg_quick_server_file(data)];
+}
+
+var GuideSystemd = function() {
+ GuideServerClient.call(this, 'systemd-networkd');
+}
+
+GuideSystemd.prototype = Object.create(GuideServerClient.prototype);
+GuideSystemd.prototype.constructor = GuideSystemd;
+
+GuideSystemd.prototype.for_client = function(data) {
+ return [systemd_client_netdev_file(data), systemd_client_network_file(data)];
+}
+
+GuideSystemd.prototype.for_server = function(data) {
+ return [systemd_server_netdev_file(data)];
+}
+
+var GuideNetworkManager = function() {
+ GuideServerClient.call(this, 'NetworkManager');
+}
+
+GuideNetworkManager.prototype = Object.create(GuideServerClient.prototype);
+GuideNetworkManager.prototype.constructor = GuideNetworkManager;
+
+GuideNetworkManager.prototype.for_client = function(data) {
+ return [nmcli_client_file(data)];
+}
+
+GuideNetworkManager.prototype.for_server = function(data) {
+ return [nmcli_server_file(data)];
+}
+
+var GuideManual = function() {
+ GuideServerClient.call(this, '<code>ip</code> &amp; <code>wg</code>');
+}
+
+GuideManual.prototype = Object.create(GuideServerClient.prototype);
+GuideManual.prototype.constructor = GuideManual;
+
+GuideManual.prototype.for_client = function(data) {
+ return [manual_client_script(data)];
+}
+
+GuideManual.prototype.for_server = function(data) {
+ return [manual_server_script(data)];
+}
+
+function clear_guides() {
+ $('#guides').empty();
+}
+
+function add_guide(guide, data) {
+ $('#guides').append(guide.format(data));
+}
+
+function guides_from_data(data) {
+ if (!data.parse()) {
+ return;
+ }
+
+ var guides = [
+ new GuidePermaURL(),
+ new GuideWgQuick(),
+ new GuideNetworkManager(),
+ new GuideSystemd(),
+ new GuideManual()
+ ];
+
+ guides.forEach(function(guide) {
+ add_guide(guide, data);
+ });
+}
+
+function form_on_submit() {
+ clear_guides();
+
+ var data = new Data();
+ guides_from_data(data);
+
+ return false;
+}
+
+function advanced_params_on_click(input) {
+ var data = new Data();
+ if (input.checked)
+ data.show_advanced();
+ else
+ data.hide_advanced();
+}
+
+function main() {
+ var data = new Data();
+ if (data.set_from_this_url())
+ guides_from_data(data);
+}
+
+$(function() {
+ main();
+});
diff --git a/src/index.html b/src/index.html
new file mode 100644
index 0000000..8ad2cfd
--- /dev/null
+++ b/src/index.html
@@ -0,0 +1,187 @@
+---
+title: Main page
+layout: plain
+navbar:
+ link: Project
+links:
+ - {rel: stylesheet, href: assets/css/pre_overlay.css}
+scripts:
+ # ipaddr.js is buggy: https://github.com/whitequark/ipaddr.js/issues/160.
+ # TODO: revert back to using ipaddr.js if it's fixed?
+ #- {src: 'https://cdn.jsdelivr.net/npm/ipaddr.js@2.0.1/ipaddr.min.js'}
+
+ # This is the "browserified" replacement, ip-address. This bundling procedure
+ # fucking sucks, but it's the best I've got for now.
+ - {src: assets/js/bundle.js}
+
+ - {src: 'https://cdn.jsdelivr.net/npm/qrious@4.0.2/dist/qrious.min.js'}
+ - {src: assets/js/main.js}
+
+params:
+ - id: server
+ desc: Server
+ items:
+ - id: server_public
+ name: Public key
+ placeholder: "Server's public key"
+ help: |
+ Query using <code>wg show wg0 public-key</code>
+ example: iNqGDQ2tltbSN4s3Fpb/7PRc2OSwR3/bPjOrf8V/SmA=
+ - id: server_endpoint
+ name: Endpoint
+ placeholder: "Server's endpoint in the HOST:PORT format"
+ help: |
+ Server's <em>public</em> IP address or a hostname.
+ Query the port using <code>wg show wg0 listen-port</code>
+ example: 123.123.123.123:123
+ - id: preshared
+ name: Preshared key
+ placeholder: Preshared key (shared between server and client)
+ help: |
+ Generate using <code>wg genpsk</code>
+ example: Moshdr8RNfYUWG/0MVOlglzlze3beATD6YumDwCZf5E=
+ - id: client
+ desc: Client
+ items:
+ - id: client_public
+ name: Public key
+ placeholder: "Client's public key"
+ help: |
+ Generate both keys using <code>wg genkey | tee private.key | wg pubkey > public.key</code>
+ example: hvfo/MgizTRbrktfx3k2Q0Ib0mx0P2N6LRZEYWqkpXc=
+ - id: client_private
+ name: Private key
+ placeholder: "Client's private key"
+ help: |
+ Unless generated in one go with the public key, generate one using <code>wg genkey</code>
+ example: qKgmDq8HWaU432qJhEa2Q6pE52P55xBHNOgzB0roP3A=
+ - id: client_ipv4
+ name: IPv4
+ placeholder: Address and netmask, like 192.168.123.123/24
+ help: |
+ IPv4 address to assign to the client and its netmask in the CIDR format.
+ example: 192.168.123.123/24
+ - id: client_ipv6
+ name: IPv6
+ placeholder: Address and netmask, like fd::192.168.123.123/48
+ help: |
+ IPv6 address to assign to the client and its netmask in the CIDR format.
+ example: fd01:2345:6789::192.168.123.123/48
+ - id: keepalive
+ name: Keepalive
+ default: 25
+ placeholder: Seconds between keepalive packets, typically 25
+ help: |
+ Time to wait between sending keepalive packets, seconds.
+ example: 25
+ advanced: true
+ - id: tunnel_everything
+ name: Tunnel all traffic
+ type: checkbox
+ default: false
+ advanced: true
+ - id: dns_ipv4
+ name: DNS (IPv4)
+ default: 1.1.1.1,1.0.0.1
+ placeholder: IPv4 addresses of DNS servers
+ help: |
+ Comma-separated list of IPv4 addresses to use as DNS servers.
+ example: 1.1.1.1,1.0.0.1
+ advanced: true
+ - id: dns_ipv6
+ name: DNS (IPv6)
+ default: 2606:4700:4700::1111,2606:4700:4700::1001
+ placeholder: IPv6 addresses of DNS servers
+ help: |
+ Comma-separated list of IPv6 addresses to use as DNS servers.
+ example: 2606:4700:4700::1111,2606:4700:4700::1001
+ advanced: true
+---
+<h1>{{ site.settings.project.name }}</h1>
+
+<div class="row">
+ <div class="col-md-8">
+ <p class="text-muted">This page assumes that you have a WireGuard server
+configured on interface <code>wg0</code>.
+You can then generate the new client's keys, feed them to this page and it will
+show configuration files that can be used by the client.</p>
+{% assign example_url = '?' %}
+{% assign first = true %}
+{% for grp in page.params %}
+ {% for param in grp.items %}
+ {% unless first %}
+ {% capture example_url %}{{ example_url }}&{% endcapture %}
+ {% endunless %}
+ {% assign first = false %}
+ {% capture example_url %}{{ example_url }}{{ param.id }}={{ param.example | url_encode }}{% endcapture %}
+ {% endfor %}
+{% endfor %}
+{% assign example_url = example_url | escape_uri | baseurl %}
+ <p>Take a look at an <a href="{{ example_url }}">example configuration</a> to learn more.</p>
+ </div>
+</div>
+
+<form action="javascript:void(0);" onsubmit="form_on_submit(); return false;">
+ <div class="row">
+{% for grp in page.params %}
+ <div class="col-md-6">
+ <div class="form-horizontal">
+ <div class="form-group">
+ <div class="col-sm-offset-3 col-sm-9">
+ <h2 class="text-center h3" style="margin-bottom: 0;">{{ grp.desc }}</h2>
+ </div>
+ </div>
+ {% for param in grp.items %}
+ {% assign type = param.type | default: "text" %}
+ {% if type == "text" %}
+ <div class="form-group" id="param_{{ param.id }}_container"{% if param.advanced %} style="display: none;"{% endif %}>
+ <label for="param_{{ param.id }}" class="col-sm-3 control-label">{{ param.name }}</label>
+ <div class="col-sm-9">
+ <input type="text" class="form-control" id="param_{{ param.id }}" placeholder="{{ param.placeholder }}" aria-describedby="param_{{ param.id }}_help"{% if param.default != null %} value="{{ param.default }}"{% endif %}>
+ <span id="param_{{ param.id }}_help" class="help-block" style="margin-bottom: 0;">
+ <span id="param_{{ param.id }}_error" class="text-danger small" style="display: block; white-space: pre-line; margin-bottom: 5px;"></span>
+ <span>{{ param.help }}</span>
+ </span>
+ </div>
+ </div>
+ {% endif %}
+ {% if type == "checkbox" %}
+ <div class="form-group" id="param_{{ param.id }}_container"{% if param.advanced %} style="display: none;"{% endif %}>
+ <div class="col-sm-offset-3 col-sm-9">
+ <div class="checkbox">
+ <label><input type="checkbox" id="param_{{ param.id }}" value="{{ param.default }}"{% if param.default %} checked{% endif %}>{{ param.name }}</label>
+ </div>
+ <span id="param_{{ param.id }}_help" class="help-block" style="margin-bottom: 0;">
+ <span id="param_{{ param.id }}_error" class="text-danger small" style="display: block; white-space: pre-line; margin-bottom: 5px;"></span>
+ </span>
+ </div>
+ </div>
+ {% endif %}
+ {% endfor %}
+ </div>
+ </div>
+{% endfor %}
+ <div class="col-md-offset-6 col-md-6">
+ <div class="form-horizontal">
+ <div class="form-group">
+ <div class="col-sm-offset-3 col-sm-9">
+ <div class="checkbox">
+ <label class="text-muted"><input type="checkbox" id="advanced_params" onClick="advanced_params_on_click(this);">Show advanced parameters</label>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-sm-offset-3 col-sm-9 text-center">
+ <button type="submit" class="btn btn-primary">Generate</button>
+ <a href="{{ example_url }}"><button type="button" class="btn btn-info" title="Fill in the form with examples values">Example</button></a>
+ <span class="help-block" style="margin-bottom: 0;">
+ <span id="params_error" class="text-danger small" style="white-space: pre-line;"></span>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+</form>
+
+<div id="guides"></div>