diff --git a/StaticRoot/free b/StaticRoot/free
new file mode 100644
index 0000000..9026fd4
--- /dev/null
+++ b/StaticRoot/free
@@ -0,0 +1,237 @@
+
+
+
+ DN42 Free Explorer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StaticRoot/free.js b/StaticRoot/free.js
new file mode 100644
index 0000000..c6196eb
--- /dev/null
+++ b/StaticRoot/free.js
@@ -0,0 +1,701 @@
+//////////////////////////////////////////////////////////////////////////
+// DN42 IP Explorer
+//////////////////////////////////////////////////////////////////////////
+
+//////////////////////////////////////////////////////////////////////////
+// root component
+
+Vue.component('app-root', {
+ template: '#app-root-template',
+
+ methods: {
+ }
+
+})
+
+//////////////////////////////////////////////////////////////////////////
+// free IPv4 view
+
+Vue.component('app-free4', {
+ template: '#app-free4-template',
+ data() {
+ return {
+
+ state: 'invalid',
+ error: '',
+ inetnum: null,
+ p4: [ ],
+ free4: [ ],
+ stats: {
+ alloc: 0,
+ addr: 0,
+ nets: 0
+ },
+
+ filter: 27,
+ filtered: [ ],
+ ftotal: 0,
+
+ asn: '4242423999',
+ mntner: 'FOO',
+ person: 'FOO',
+ example: '0.0.0.0/0'
+ }
+ },
+ methods: {
+
+ // not used
+ pretty4(msg, subnet, plen) {
+ var octets = [ ]
+ octets[0] = (subnet >> 24) & 0xFF
+ octets[1] = (subnet >> 16) & 0xFF
+ octets[2] = (subnet >> 8) & 0xFF
+ octets[3] = subnet & 0xFF
+ console.log(msg, ': ', octets.join("."),'/',plen)
+ },
+
+ // reload prefix data
+ reload() {
+
+ // reset current data
+ this.inetnum = null
+ this.p4.splice(0,this.p4.length)
+ this.state = "loading"
+
+ // IPv4 prefixes
+ axios
+ .get('/api/registry/inetnum/*')
+ .then(response => {
+ this.inetnum = response.data
+ this.processIPv4()
+ })
+ .catch(error => {
+ this.error = error
+ this.state = 'error'
+ console.log(error)
+ })
+ },
+
+
+ // parse an IPv4 string in to an object
+ parse4(p) {
+ p = p.replace('_','/')
+
+ // split out prefix
+ var s1 = p.split('/')
+ // and octets
+ var s2 = s1[0].split('.')
+ if (s1.length !=2 || s2.length != 4) {
+ console.log("Failed to IPv4 parse ", p)
+ return null
+ }
+
+ // convert to a 32bit integer
+ var num =
+ ((parseInt(s2[0]) << 24) +
+ (parseInt(s2[1]) << 16) +
+ (parseInt(s2[2]) << 8) +
+ parseInt(s2[3]))>>>0
+
+ var plen = parseInt(s1[1])
+
+ var mask = (0xFFFFFFFF - ((2**(32 - plen))-1))>>>0
+
+ // finally return the ipv4 object
+ return {
+ plen: plen,
+ mask: mask,
+ subnet: num
+ }
+ },
+
+ // process IPv4 prefixes
+ processIPv4() {
+
+ var inets = [ ]
+ Object.values(this.inetnum).forEach(rp => {
+
+ // map the attributes in to a hash
+ // no need to worry about duplicated attribs
+ var attrib = { }
+ rp.Attributes.forEach(a => {
+ attrib[a[0]] = a[1]
+ })
+
+ // convert the cidr to an object
+ obj = this.parse4(attrib.cidr)
+ if (obj != null) {
+ if (attrib.policy != null) {
+ obj.policy = attrib.policy
+ }
+ inets.push(obj)
+ }
+ })
+
+ // sort the prefixes in ascending order
+ var sorted = inets.sort((a,b) => {
+ if (a.subnet == b.subnet) {
+ return a.plen - b.plen
+ }
+ else {
+ return a.subnet - b.subnet
+ }
+ })
+
+ // splice in sorted array
+ this.p4.splice(0, this.p4.length, ...sorted)
+
+ // update the free list
+ this.updateFree4()
+
+ this.updatePrefixLen({ target: { value: this.filter } })
+
+ // all done
+ this.state = 'ready'
+ },
+
+ // update ipv4 stats
+ stats4(subnet, plen) {
+ // check DN42 range 172.20.0.0/14
+ var masked = (subnet & 0xFFFC0000)>>>0
+ if (masked == 0xAC140000) {
+ // add em up
+ this.stats.nets += 1
+ this.stats.addr += 2**(32-plen)
+ }
+ },
+
+ // recursively check a subnet for free blocks
+ scanSubnets(subnet, mask, plen) {
+
+ // check for the last defined prefix
+ if (this.ix >= this.p4.length) {
+ this.stats4(subnet, plen)
+ this.free4.push({
+ subnet: subnet,
+ mask: mask,
+ plen: plen
+ })
+ return
+ }
+
+ var prefix = this.p4[this.ix]
+
+ // does the next prefix match this subnet ?
+ if ((prefix.subnet == subnet) && (prefix.plen == plen)) {
+ var policy = 'assigned'
+ if (prefix.policy != null) {
+ policy = prefix.policy
+ }
+
+ if (policy != 'open') {
+ // optimisation to reduce recursion by
+ // scanning forward for open children
+ this.ix += 1
+
+ while(this.ix < this.p4.length) {
+ prefix = this.p4[this.ix]
+ var masked = (prefix.subnet & mask)>>>0
+ if (subnet != masked) {
+ // no longer a child, we're done
+ return
+ }
+
+ // found a subnet that is open
+ if (prefix.policy == 'open') {
+ this.scanSubnets(prefix.subnet,
+ prefix.mask,
+ prefix.plen,
+ prefix.policy)
+ }
+ else {
+ // don't recurse closed subnets
+ var tmppolicy = 'assigned'
+ if (prefix.policy != null) {
+ tmppolicy = prefix.policy
+ }
+ this.ix += 1
+ }
+ }
+ return
+ }
+
+ // move on to next index
+ this.ix += 1
+ if (this.ix >= this.p4.length) {
+ this.stats4(subnet, plen)
+ this.free4.push({
+ subnet: subnet,
+ mask: mask,
+ plen: plen
+ })
+ return
+ }
+ prefix = this.p4[this.ix]
+
+ }
+
+ // is the next prefix a subnet of the current search ?
+ var masked = (prefix.subnet & mask)>>>0
+ if (subnet == masked) {
+
+ // split the subnet and try again
+ plen += 1
+ var bit = (2**(32 - plen))>>>0
+ mask = (mask | bit)>>>0
+
+ this.scanSubnets((subnet & (~bit))>>>0, mask, plen)
+ this.scanSubnets((subnet | bit)>>>0, mask, plen)
+ }
+ else {
+ // found an open block
+ this.stats4(subnet, plen)
+ this.free4.push({
+ subnet: subnet,
+ mask: mask,
+ plen: plen
+ })
+ }
+ },
+
+ // update the free blocks list
+ updateFree4() {
+ // reset stats
+ this.stats.alloc = this.p4.length
+ this.stats.addr = 0
+ this.stats.nets = 0
+
+ // recursively scan for free nets
+ this.ix = 0
+ this.scanSubnets(0, 0, 0, 'undefined')
+ },
+
+ // filter subnets based on prefix length
+ filterFree() {
+
+ var tlist = [ ]
+ // filter the free list
+ this.free4.forEach(free => {
+ if (free.plen == this.filter) {
+ tlist.push(free)
+ }
+ })
+
+ this.ftotal = tlist.length
+
+ // pick up to ten random prefixes
+ var result = [ ]
+ for(var i = 0; ((tlist.length > 0) && (i < 10)); i++) {
+ var ix = Math.round(Math.random() * tlist.length)
+ var obj = tlist[ix]
+
+ // push to result
+ var octets = [ ]
+ octets[0] = (obj.subnet >> 24) & 0xFF
+ octets[1] = (obj.subnet >> 16) & 0xFF
+ octets[2] = (obj.subnet >> 8) & 0xFF
+ octets[3] = obj.subnet & 0xFF
+ result.push(octets.join(".")+'/'+obj.plen)
+
+ // remove from the list
+ tlist.splice(ix, 1)
+ }
+
+ this.filtered.splice(0, this.filtered.length, ...result)
+ },
+
+
+ // update function when selecting prefixes
+ updatePrefixLen(e) {
+ this.filter = e.target.value
+
+ // update buttons
+ var group = document.getElementById("prefixselect")
+ group.childNodes.forEach(button => {
+ if (button.nodeName == "BUTTON") {
+ button.classList.remove('btn-primary')
+ button.classList.remove('btn-secondary')
+ if (button.value == this.filter) {
+ button.classList.add('btn-primary')
+ }
+ else {
+ button.classList.add('btn-secondary')
+ }
+ }
+ })
+
+ // select available prefixes
+ this.filterFree()
+
+ },
+
+ // update the example templates
+ updateExample(e) {
+ this.example = e.text
+ }
+
+ },
+ mounted() {
+ // reload data if required
+ if (this.p4.length == 0) {
+ this.reload()
+ }
+
+ // always update the prefix selection
+ this.updatePrefixLen({ target: { value: this.filter } })
+ }
+
+})
+
+//////////////////////////////////////////////////////////////////////////
+// free IPv6 view
+
+Vue.component('app-free6', {
+ template: '#app-free6-template',
+ data() {
+ return {
+
+ state: 'invalid',
+ error: '',
+ inet6num: null,
+ p6: [ ],
+ stats: {
+ alloc: 0,
+ nets: 0
+ },
+
+ plist: [ ]
+ }
+ },
+
+ methods: {
+
+ // reload prefix data
+ reload() {
+ // reset current data
+ this.inet6num = null
+ this.p6.splice(0,this.p6.length)
+ this.state = "loading"
+
+
+ // IPv6 prefixes
+ axios
+ .get('/api/registry/inet6num/*')
+ .then(response => {
+ this.inet6num = response.data
+ this.processIPv6()
+ })
+ .catch(error => {
+ this.error = error
+ this.state = 'error'
+ console.log(error)
+ })
+ },
+
+ // parse an IPv6 string in to an object
+ parse6(cidr) {
+ cidr = cidr.replace('_','/')
+
+ // split prefix and length
+ var s1 = cidr.split('/')
+
+ var plen = parseInt(s1[1])
+ // ignore networks longer than /64 as they aren't valid
+ if (plen <= 64) {
+
+ // split out the quads
+ var quads = s1[0].split(':')
+
+ // and canonicalise '::'
+ var ix=quads.indexOf('')
+ if (ix != -1) {
+ while(quads.length < 8) {
+ quads.splice(ix, 0, '0')
+ }
+ }
+
+ // calculate 64bit network part of prefix
+ var num = BigInt(0)
+ var quad
+ for(var i = 0; i < 4; i++) {
+ num *= BigInt(65536)
+ quad = (quads[i] == '' ? 0 : parseInt('0x' + quads[i]))
+ num += BigInt(quad)
+ }
+
+ var plen = parseInt(s1[1])
+ var mask = BigInt(0xFFFFFFFFFFFFFFFF) - ((2n**BigInt(64-plen))-1n)
+
+ // return object
+ return {
+ plen: plen,
+ mask: mask,
+ subnet: num
+ }
+ }
+ },
+
+
+ // process IPv6 prefixes
+ processIPv6() {
+
+ var netspace = BigInt(0)
+
+ var nets = [ ]
+ Object.values(this.inet6num).forEach(rp => {
+ // extract attributes
+ var attrib = { }
+ rp.Attributes.forEach(a => {
+ attrib[a[0]] = a[1]
+ })
+
+ // turn in to object
+ obj = this.parse6(attrib.cidr)
+ if (obj != null) {
+ if (attrib.policy != null) {
+ obj.policy = attrib.policy
+ }
+ // ignore ::/0 and fd00::/8 when calculating netspace
+ if ((obj.plen > 8) && (obj.policy != 'open')) {
+ netspace += 2n**BigInt(64 - obj.plen)
+ }
+ nets.push(obj)
+ }
+ })
+
+ netspace /= 1000000n
+ this.stats.nets = Number(netspace)
+
+ // sort by subnet
+ var sorted = nets.sort((a,b) => {
+ if (a.subnet == b.subnet) {
+ return a.plen - b.plen
+ }
+ else {
+ if (a.subnet > b.subnet) { return 1 }
+ return -1
+ }
+ })
+
+ // update
+ this.p6.splice(0, this.p6.length, ...nets)
+
+ this.stats.alloc = this.p6.length
+
+ this.generate()
+
+ // all done
+ this.state = 'ready'
+ },
+
+ // check if a network is already allocated
+ check6(n) {
+
+ var match = null
+ this.p6.forEach(obj => {
+ // only check longer prefixes than current match
+ if ((match == null) || (obj.plen >= match.plen)) {
+ var masked = n & obj.mask
+ if (obj.subnet == masked) {
+ // new, more precise prefix found
+ match = obj
+ }
+ }
+ })
+
+ if ((match != null) && (match.policy != 'open')) {
+ return false
+ }
+
+ return true
+ },
+
+ // create new random prefixes
+ generate() {
+
+ var nlist = [ ]
+ // generate 10 new prefixes
+ for(var i = 0; i < 10; i++) {
+ var valid = false
+ var prefix = ''
+
+ while(!valid) {
+
+ // create 48bits of random address
+ var quads = [ ]
+ for(var j = 0; j < 3; j++) {
+ quads[j] = Math.round(Math.random()*65536)
+ }
+ // fix the first byte to be in fd00::/8
+ quads[0] = 0xFD00 + (quads[0] & 0xFF)
+
+ // convert to hex
+ var hex = [ ]
+ for (j = 0; j < 3; j++) {
+ hex[j] = quads[j].toString(16)
+ }
+
+ prefix = `${hex[0]}:${hex[1]}:${hex[2]}::/48`
+
+ // convert to a BigInt
+ var num = BigInt(0)
+ for(var j = 0; j < 3; j++) {
+ num *= BigInt(65536)
+ num += BigInt(quads[j])
+ }
+ num *= 65536n
+
+ // now check that the random prefix is not a subnet of another allocation
+ valid = this.check6(num)
+ }
+
+ nlist.push(prefix)
+ }
+
+
+ // swap in the new prefix list
+ this.plist.splice(0, this.plist.length, ...nlist)
+ }
+
+ },
+
+ computed: {
+ freenets() {
+ return Math.round(72057.594 - (this.stats.nets/1000000))
+ }
+ },
+
+ mounted() {
+ if (this.p6.length == 0) {
+ this.reload()
+ }
+ }
+
+})
+
+//////////////////////////////////////////////////////////////////////////
+// free IPv6 view
+
+Vue.component('app-asn', {
+ template: '#app-asn-template',
+ data() {
+ return {
+
+ state: 'invalid',
+ error: '',
+ autnum: null,
+ free: [ ],
+ stats: {
+ alloc: 0,
+ },
+
+ alist: [ ]
+ }
+ },
+
+ methods: {
+
+ // reload prefix data
+ reload() {
+ // reset current data
+ this.asn = null
+ this.free.splice(0,this.free.length)
+ this.state = "loading"
+
+ // fetch ASN list
+ axios
+ .get('/api/registry/aut-num')
+ .then(response => {
+ this.autnum = response.data['aut-num']
+ this.processASN()
+ })
+ .catch(error => {
+ this.error = error
+ this.state = 'error'
+ console.log(error)
+ })
+ },
+
+ // process ASN list
+ processASN() {
+
+ var asn = [ ]
+ // extract used ASN
+ this.autnum.forEach(a => {
+ // only interested in DN42 ASN
+ if (a.startsWith('AS424242')) {
+ var num = parseInt(a.substr(8))
+ asn[num] = true
+ this.stats.alloc += 1
+ }
+ })
+
+ // now work out which ASN are free
+ for(var a = 0; a < 4000; a++) {
+ if (!asn[a]) {
+ var str = a.toString(10)
+ // zero pad
+ while(str.length < 4) {
+ str = '0' + str
+ }
+
+ this.free.push('AS424242' + str)
+ }
+ }
+
+ this.generate()
+
+ // all done
+ this.state = 'ready'
+ },
+
+
+ // create new random prefixes
+ generate() {
+
+ var nlist = [ ]
+ for(var i = 0; i < 10; i++) {
+ // pick a random free ASN
+ var rand = Math.round(Math.random() * this.free.length)
+ nlist.push(this.free[rand])
+ }
+
+ // swap in the new prefix list
+ this.alist.splice(0, this.alist.length, ...nlist)
+ }
+
+ },
+
+ mounted() {
+ if (this.free.length == 0) {
+ this.reload()
+ }
+ }
+
+})
+
+//////////////////////////////////////////////////////////////////////////
+// main vue application starts here
+
+// initialise the Vue Router
+const router = new VueRouter({
+ routes: [
+ { path: '/', component: Vue.component('app-root') },
+ { path: '/4', component: Vue.component('app-free4') },
+ { path: '/6', component: Vue.component('app-free6') },
+ { path: '/asn', component: Vue.component('app-asn') }
+ ]
+})
+
+// and the main app instance
+const vm = new Vue({
+ el: '#free-app',
+ data: {
+
+ },
+ router
+})
+
+
+//////////////////////////////////////////////////////////////////////////
+// end of code
diff --git a/StaticRoot/index.html b/StaticRoot/index.html
index 5a166fe..0a000cb 100644
--- a/StaticRoot/index.html
+++ b/StaticRoot/index.html
@@ -20,8 +20,9 @@ body { box-shadow: inset 0 2em 10em rgba(0,0,0,0.4); min-height: 100vh }