')
}
$('#network').val(rootNetwork)
subnetMap = {}
subnetMap[rootCidr] = {}
maxNetSize = parseInt($('#netsize').val())
renderTable(operatingMode);
}
$('#calcbody').on('click', 'td.split,td.join', function(event) {
// HTML DOM Data elements! Yay! See the `data-*` attributes of the HTML tags
mutate_subnet_map(this.dataset.mutateVerb, this.dataset.subnet, '')
renderTable(operatingMode);
})
$('#calcbody').on('keyup', 'td.note input', function(event) {
// HTML DOM Data elements! Yay! See the `data-*` attributes of the HTML tags
let delay = 1000;
clearTimeout(noteTimeout);
noteTimeout = setTimeout(function(element) {
mutate_subnet_map('note', element.dataset.subnet, '', element.value)
}, delay, this);
})
$('#calcbody').on('focusout', 'td.note input', function(event) {
// HTML DOM Data elements! Yay! See the `data-*` attributes of the HTML tags
clearTimeout(noteTimeout);
mutate_subnet_map('note', this.dataset.subnet, '', this.value)
})
function renderTable(operatingMode) {
// TODO: Validation Code
$('#calcbody').empty();
let maxDepth = get_dict_max_depth(subnetMap, 0)
addRowTree(subnetMap, 0, maxDepth, operatingMode)
}
function addRowTree(subnetTree, depth, maxDepth,operatingMode) {
for (let mapKey in subnetTree) {
if (mapKey.startsWith('_')) { continue; }
if (has_network_sub_keys(subnetTree[mapKey])) {
addRowTree(subnetTree[mapKey], depth + 1, maxDepth,operatingMode)
} else {
let subnet_split = mapKey.split('/')
let notesWidth = '30%';
if ((maxDepth > 5) && (maxDepth <= 10)) {
notesWidth = '25%';
} else if ((maxDepth > 10) && (maxDepth <= 15)) {
notesWidth = '20%';
} else if ((maxDepth > 15) && (maxDepth <= 20)) {
notesWidth = '15%';
} else if (maxDepth > 20) {
notesWidth = '10%';
}
addRow(subnet_split[0], parseInt(subnet_split[1]), (infoColumnCount + maxDepth - depth), (subnetTree[mapKey]['_note'] || ''), notesWidth, (subnetTree[mapKey]['_color'] || ''),operatingMode)
}
}
}
function addRow(network, netSize, colspan, note, notesWidth, color, operatingMode) {
let addressFirst = ip2int(network)
let addressLast = subnet_last_address(addressFirst, netSize)
let usableFirst = subnet_usable_first(addressFirst, netSize, operatingMode)
let usableLast = subnet_usable_last(addressFirst, netSize)
let hostCount = 1 + usableLast - usableFirst
let styleTag = ''
if (color !== '') {
styleTag = ' style="background-color: ' + color + '"'
}
let rangeCol, usableCol;
if (netSize < 32) {
rangeCol = int2ip(addressFirst) + ' - ' + int2ip(addressLast);
usableCol = int2ip(usableFirst) + ' - ' + int2ip(usableLast);
} else {
rangeCol = int2ip(addressFirst);
usableCol = int2ip(usableFirst);
}
let newRow =
'
\n' +
'
' + network + '/' + netSize + '
\n' +
'
' + rangeCol + '
\n' +
'
' + usableCol + '
\n' +
'
' + hostCount + '
\n' +
'
\n' +
'
/' + netSize + '
\n'
if (netSize > maxNetSize) {
// This is wrong. Need to figure out a way to get the number of children so you can set rowspan and the number
// of ancestors so you can set colspan.
// DONE: If the subnet address (without the mask) matches a larger subnet address
// in the heirarchy that is a signal to add more join buttons to that row, since they start at the top row and
// via rowspan extend downward.
let matchingNetworkList = get_matching_network_list(network, subnetMap).slice(1)
for (const i in matchingNetworkList) {
let matchingNetwork = matchingNetworkList[i]
let networkChildrenCount = count_network_children(matchingNetwork, subnetMap, [])
newRow += '
/' + matchingNetwork.split('/')[1] + '
\n'
}
}
newRow += '
';
$('#calcbody').append(newRow)
}
// Helper Functions
function ip2int(ip) {
return ip.split('.').reduce(function(ipInt, octet) { return (ipInt<<8) + parseInt(octet, 10)}, 0) >>> 0;
}
function int2ip (ipInt) {
return ( (ipInt>>>24) +'.' + (ipInt>>16 & 255) +'.' + (ipInt>>8 & 255) +'.' + (ipInt & 255) );
}
function subnet_last_address(subnet, netSize) {
return subnet + subnet_addresses(netSize) - 1;
}
function subnet_addresses(netSize) {
return 2**(32-netSize);
}
function subnet_usable_first(network, netSize, operatingMode) {
if (netSize < 31) {
// https://docs.aws.amazon.com/vpc/latest/userguide/subnet-sizing.html
// AWS reserves 3 additional IPs
// https://learn.microsoft.com/en-us/azure/virtual-network/virtual-networks-faq#are-there-any-restrictions-on-using-ip-addresses-within-these-subnets
// Azure reserves 3 additional IPs
return network + (operatingMode == 'Standard' ? 1 : 4);
} else {
return network;
}
}
function subnet_usable_last(network, netSize) {
let last_address = subnet_last_address(network, netSize);
if (netSize < 31) {
return last_address - 1;
} else {
return last_address;
}
}
function get_dict_max_depth(dict, curDepth) {
let maxDepth = curDepth
for (let mapKey in dict) {
if (mapKey.startsWith('_')) { continue; }
let newDepth = get_dict_max_depth(dict[mapKey], curDepth + 1)
if (newDepth > maxDepth) { maxDepth = newDepth }
}
return maxDepth
}
function get_join_children(subnetTree, childCount) {
for (let mapKey in subnetTree) {
if (mapKey.startsWith('_')) { continue; }
if (has_network_sub_keys(subnetTree[mapKey])) {
childCount += get_join_children(subnetTree[mapKey])
} else {
return childCount
}
}
}
function has_network_sub_keys(dict) {
let allKeys = Object.keys(dict)
// Maybe an efficient way to do this with a Lambda?
for (let i in allKeys) {
if (!allKeys[i].startsWith('_')) {
return true
}
}
return false
}
function count_network_children(network, subnetTree, ancestryList) {
// TODO: This might be able to be optimized. Ultimately it needs to count the number of keys underneath
// the current key are unsplit networks (IE rows in the table, IE keys with a value of {}).
let childCount = 0
for (let mapKey in subnetTree) {
if (mapKey.startsWith('_')) { continue; }
if (has_network_sub_keys(subnetTree[mapKey])) {
childCount += count_network_children(network, subnetTree[mapKey], ancestryList.concat([mapKey]))
} else {
if (ancestryList.includes(network)) {
childCount += 1
}
}
}
return childCount
}
function get_network_children(network, subnetTree) {
// TODO: This might be able to be optimized. Ultimately it needs to count the number of keys underneath
// the current key are unsplit networks (IE rows in the table, IE keys with a value of {}).
let subnetList = []
for (let mapKey in subnetTree) {
if (mapKey.startsWith('_')) { continue; }
if (has_network_sub_keys(subnetTree[mapKey])) {
subnetList.push.apply(subnetList, get_network_children(network, subnetTree[mapKey]))
} else {
subnetList.push(mapKey)
}
}
return subnetList
}
function get_matching_network_list(network, subnetTree) {
let subnetList = []
for (let mapKey in subnetTree) {
if (mapKey.startsWith('_')) { continue; }
if (has_network_sub_keys(subnetTree[mapKey])) {
subnetList.push.apply(subnetList, get_matching_network_list(network, subnetTree[mapKey]))
}
if (mapKey.split('/')[0] === network) {
subnetList.push(mapKey)
}
}
return subnetList
}
function get_consolidated_property(subnetTree, property) {
let allValues = get_property_values(subnetTree, property)
// https://stackoverflow.com/questions/14832603/check-if-all-values-of-array-are-equal
let allValuesMatch = allValues.every( (val, i, arr) => val === arr[0] )
if (allValuesMatch) {
return allValues[0]
} else {
return ''
}
}
function get_property_values(subnetTree, property) {
let propValues = []
for (let mapKey in subnetTree) {
if (has_network_sub_keys(subnetTree[mapKey])) {
propValues.push.apply(propValues, get_property_values(subnetTree[mapKey], property))
} else {
// The "else" above is a bit different because it will start tracking values for subnets which are
// in the hierarchy, but not displayed. Those are always blank so it messes up the value list
propValues.push(subnetTree[mapKey][property] || '')
}
}
return propValues
}
function get_network(networkInput, netSize) {
let ipInt = ip2int(networkInput)
netSize = parseInt(netSize)
for (let i=31-netSize; i>=0; i--) {
ipInt &= ~ 1< More Information: Amazon Virtual Private Cloud > User Guide > Subnet CIDR Blocks > Subnet Sizing for IPv4'
break;
case 'AZURE':
var modal_error_message = 'The minimum IPv4 subnet size for Azure is /' + minSubnetSizes[operatingMode] + '.
')
}
} else if (verb === 'join') {
// Options:
// [ Selected ] Keep note if all the notes are the same, blank them out if they differ. Most intuitive
// [ Possible ] Lose note data for all deleted subnets.
// [ Possible ] Keep note from first subnet in the join scope. Reasonable but I think rarely will the note be kept by the user
// [ Possible ] Concatenate all notes. Ugly and won't really be useful for more than two subnets being joined
subnetTree[mapKey] = {
'_note': get_consolidated_property(subnetTree[mapKey], '_note'),
'_color': get_consolidated_property(subnetTree[mapKey], '_color')
}
} else if (verb === 'note') {
subnetTree[mapKey]['_note'] = propValue
} else if (verb === 'color') {
subnetTree[mapKey]['_color'] = propValue
} else {
// How did you get here?
}
}
}
}
function switchMode(operatingMode) {
let isSwitched = true;
if (subnetMap !== null) {
if (validateSubnetSizes(subnetMap, minSubnetSizes[operatingMode])) {
renderTable(operatingMode);
set_usable_ips_title(operatingMode);
$('#netsize').attr('pattern', netsizePatterns[operatingMode]);
$('#input_form').removeClass('was-validated');
$('#input_form').rules('remove', 'netsize');
switch (operatingMode) {
case 'AWS':
var validate_error_message = 'AWS Mode - Smallest size is /' + minSubnetSizes[operatingMode]
break;
case 'AZURE':
var validate_error_message = 'Azure Mode - Smallest size is /' + minSubnetSizes[operatingMode]
break;
default:
var validate_error_message = 'Smallest size is /' + minSubnetSizes[operatingMode]
break;
}
// Modify jquery validation rule
$('#input_form #netsize').rules('add', {
required: true,
pattern: netsizePatterns[operatingMode],
messages: {
required: 'Please enter a network size',
pattern: validate_error_message
}
});
// Remove active class from all buttons if needed
$('#dropdown_standard, #dropdown_azure, #dropdown_aws').removeClass('active');
$('#dropdown_' + operatingMode.toLowerCase()).addClass('active');
isSwitched = true;
} else {
switch (operatingMode) {
case 'AWS':
var modal_error_message = 'One or more subnets are smaller than the minimum allowed for AWS. The smallest size allowed is /' + minSubnetSizes[operatingMode] + '. See: Amazon Virtual Private Cloud > User Guide > Subnet CIDR Blocks > Subnet Sizing for IPv4'
break;
case 'AZURE':
var modal_error_message = 'One or more subnets are smaller than the minimum allowed for Azure. The smallest size allowed is /' + minSubnetSizes[operatingMode] + '. See: Azure Virtual Network FAQ > How small and how large can virtual networks and subnets be?'
break;
default:
var validate_error_message = 'Unknown Error'
break;
}
show_warning_modal('
' + modal_error_message + '
');
isSwitched = false;
}
} else {
//unlikely to get here.
reset();
}
return isSwitched;
}
function validateSubnetSizes(subnetMap, minSubnetSize) {
let isValid = true;
const validate = (subnetTree) => {
for (let key in subnetTree) {
if (key.startsWith('_')) continue; // Skip special keys
let [_, size] = key.split('/');
if (parseInt(size) > minSubnetSize) {
isValid = false;
return; // Early exit if any subnet is invalid
}
if (typeof subnetTree[key] === 'object') {
validate(subnetTree[key]); // Recursively validate subnets
}
}
};
validate(subnetMap);
return isValid;
}
function set_usable_ips_title(operatingMode) {
switch (operatingMode) {
case 'AWS':
$('#useableHeader').html('Usable IPs (AWS)')
break;
case 'AZURE':
$('#useableHeader').html('Usable IPs (Azure)')
break;
default:
$('#useableHeader').html('Usable IPs')
break;
}
$('[data-bs-toggle="tooltip"]').tooltip()
}
function show_warning_modal(message) {
var notifyModal = new bootstrap.Modal(document.getElementById('notifyModal'), {});
$('#notifyModal .modal-body').html(message)
notifyModal.show()
}
$( document ).ready(function() {
// Initialize the jQuery Validation on the form
var validator = $('#input_form').validate({
onfocusout: function (element) {
$(element).valid();
},
rules: {
network: {
required: true,
pattern: '^(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]?)$'
},
netsize: {
required: true,
pattern: '^([0-9]|[12][0-9]|3[0-2])$'
}
},
messages: {
network: {
required: 'Please enter a network',
pattern: 'Must be a valid IPv4 Address'
},
netsize: {
required: 'Please enter a network size',
pattern: 'Smallest size is /32'
}
},
errorPlacement: function(error, element) {
console.log(error);
console.log(element);
if (error[0].innerHTML !== '') {
console.log('Error Placement - Text')
if (!element.data('errorIsVisible')) {
bootstrap.Tooltip.getInstance(element).setContent({'.tooltip-inner': error[0].innerHTML})
element.tooltip('show');
element.data('errorIsVisible', true)
}
} else {
console.log('Error Placement - Empty')
console.log(element);
if (element.data('errorIsVisible')) {
element.tooltip('hide');
element.data('errorIsVisible', false)
}
}
console.log(element);
},
// This success function appears to be required as errorPlacement() does not fire without the success function
// being defined.
success: function(label, element) { },
// When the form is valid, add the 'was-validated' class
submitHandler: function(form) {
form.classList.add('was-validated');
form.submit(); // Submit the form
}
});
let autoConfigResult = processConfigUrl();
if (!autoConfigResult) {
reset();
}
//importConfig('{"config_version":"1","subnets":{"10.0.0.0/16":{"10.0.0.0/17":{"10.0.0.0/18":{},"10.0.64.0/18":{}},"10.0.128.0/17":{"10.0.128.0/18":{"10.0.128.0/19":{},"10.0.160.0/19":{"10.0.160.0/20":{"10.0.160.0/21":{"10.0.160.0/22":{},"10.0.164.0/22":{}},"10.0.168.0/21":{}},"10.0.176.0/20":{"10.0.176.0/21":{"10.0.176.0/22":{"10.0.176.0/23":{},"10.0.178.0/23":{}},"10.0.180.0/22":{}},"10.0.184.0/21":{}}}},"10.0.192.0/18":{"10.0.192.0/19":{},"10.0.224.0/19":{}}}}},"notes":{}}')
//importConfig('{"config_version":"1","subnets":{"10.0.0.0/16":{"10.0.0.0/17":{"10.0.0.0/18":{"_note":"Note 1"},"10.0.64.0/18":{"_note":"Note 2"}},"10.0.128.0/17":{"10.0.128.0/18":{"10.0.128.0/19":{"_note":"Note 3"},"10.0.160.0/19":{"10.0.160.0/20":{"10.0.160.0/21":{"10.0.160.0/22":{"_note":"Note 4"},"10.0.164.0/22":{"_note":"Note 5"}},"10.0.168.0/21":{"_note":"Note 6"}},"10.0.176.0/20":{"10.0.176.0/21":{"10.0.176.0/22":{"10.0.176.0/23":{"_note":"Note 7"},"10.0.178.0/23":{"_note":"Note 8"}},"10.0.180.0/22":{"_note":"Note 9"}},"10.0.184.0/21":{"_note":"Note 10"}}}},"10.0.192.0/18":{"10.0.192.0/19":{"_note":"Note 11"},"10.0.224.0/19":{"_note":"Note 12"}}}}},"notes":{}}')
});
function exportConfig() {
if (operatingMode !== 'Standard') {
return {
'config_version': configVersion,
'operating_mode': operatingMode,
'subnets': subnetMap,
}
} else {
return {
'config_version': configVersion,
'subnets': subnetMap,
}
}
}
function getConfigUrl() {
let defaultExport = JSON.parse(JSON.stringify(exportConfig()));
renameKey(defaultExport, 'config_version', 'v')
if (defaultExport.hasOwnProperty('operating_mode')) {
renameKey(defaultExport, 'operating_mode', 'm')
}
renameKey(defaultExport, 'subnets', 's')
shortenKeys(defaultExport['s'])
return '/index.html?c=' + urlVersion + LZString.compressToEncodedURIComponent(JSON.stringify(defaultExport))
}
function processConfigUrl() {
const params = new Proxy(new URLSearchParams(window.location.search), {
get: (searchParams, prop) => searchParams.get(prop),
});
if (params['c'] !== null) {
// First character is the version of the URL string, in case the mechanism of encoding changes
let urlVersion = params['c'].substring(0, 1)
let urlData = params['c'].substring(1)
let urlConfig = JSON.parse(LZString.decompressFromEncodedURIComponent(params['c'].substring(1)))
renameKey(urlConfig, 'v', 'config_version')
if (urlConfig.hasOwnProperty('m')) {
renameKey(urlConfig, 'm', 'operating_mode')
}
renameKey(urlConfig, 's', 'subnets')
expandKeys(urlConfig['subnets'])
importConfig(urlConfig)
return true
}
}
function shortenKeys(subnetTree) {
for (let mapKey in subnetTree) {
if (mapKey.startsWith('_')) {
continue;
}
if (has_network_sub_keys(subnetTree[mapKey])) {
shortenKeys(subnetTree[mapKey])
} else {
if (subnetTree[mapKey].hasOwnProperty('_note')) {
renameKey(subnetTree[mapKey], '_note', '_n')
}
if (subnetTree[mapKey].hasOwnProperty('_color')) {
renameKey(subnetTree[mapKey], '_color', '_c')
}
}
}
}
function expandKeys(subnetTree) {
for (let mapKey in subnetTree) {
if (mapKey.startsWith('_')) {
continue;
}
if (has_network_sub_keys(subnetTree[mapKey])) {
expandKeys(subnetTree[mapKey])
} else {
if (subnetTree[mapKey].hasOwnProperty('_n')) {
renameKey(subnetTree[mapKey], '_n', '_note')
}
if (subnetTree[mapKey].hasOwnProperty('_c')) {
renameKey(subnetTree[mapKey], '_c', '_color')
}
}
}
}
function renameKey(obj, oldKey, newKey) {
if (oldKey !== newKey) {
Object.defineProperty(obj, newKey,
Object.getOwnPropertyDescriptor(obj, oldKey));
delete obj[oldKey];
}
}
function importConfig(text) {
if (text['config_version'] === '1') {
let subnet_split = Object.keys(text['subnets'])[0].split('/')
$('#network').val(subnet_split[0])
$('#netsize').val(subnet_split[1])
subnetMap = text['subnets'];
operatingMode = text['operating_mode'] || 'Standard'
switchMode(operatingMode);
}
}
const rgba2hex = (rgba) => `#${rgba.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+\.{0,1}\d*))?\)$/).slice(1).map((n, i) => (i === 3 ? Math.round(parseFloat(n) * 255) : parseFloat(n)).toString(16).padStart(2, '0').replace('NaN', '')).join('')}`