From 3999a62f54c1b7b551620fe7fb49444ce6720b8c Mon Sep 17 00:00:00 2001 From: Mihail Slobodyanuk Date: Mon, 19 Dec 2022 12:05:01 +0200 Subject: [PATCH] IE11 support, markup fixes, caching fixes --- contrib/msi/build-msi.sh | 12 + contrib/ui/mesh-ui/ui/assets/mesh-ui-es5.js | 420 +++++++++++ contrib/ui/mesh-ui/ui/assets/mesh-ui.css | 12 +- contrib/ui/mesh-ui/ui/assets/mesh-ui.js | 6 +- contrib/ui/mesh-ui/ui/assets/polyfills.js | 798 ++++++++++++++++++++ contrib/ui/mesh-ui/ui/country.json | 4 +- contrib/ui/mesh-ui/ui/index.html | 39 +- src/admin/admin.go | 12 +- 8 files changed, 1278 insertions(+), 25 deletions(-) create mode 100644 contrib/ui/mesh-ui/ui/assets/mesh-ui-es5.js create mode 100644 contrib/ui/mesh-ui/ui/assets/polyfills.js diff --git a/contrib/msi/build-msi.sh b/contrib/msi/build-msi.sh index c913c9c4..84ef8164 100644 --- a/contrib/msi/build-msi.sh +++ b/contrib/msi/build-msi.sh @@ -98,6 +98,18 @@ else PKGDISPLAYNAME="RiV-mesh Network" fi +cat > mesh-ui-ie.js << EOF +var ie = new ActiveXObject("InternetExplorer.Application"); +ie.AddressBar = false; +ie.MenuBar = false; +ie.ToolBar = false; +ie.height = 960 +ie.width = 706 +ie.resizable = false +ie.Visible = true; +ie.Navigate("http://localhost:19019"); +EOF + # Generate the wix.xml file cat > wix.xml << EOF diff --git a/contrib/ui/mesh-ui/ui/assets/mesh-ui-es5.js b/contrib/ui/mesh-ui/ui/assets/mesh-ui-es5.js new file mode 100644 index 00000000..f931f0d7 --- /dev/null +++ b/contrib/ui/mesh-ui/ui/assets/mesh-ui-es5.js @@ -0,0 +1,420 @@ +"use strict"; + +var $ = function $(id) { + return document.getElementById(id); +}; +var $$ = function $$(clazz) { + return document.getElementsByClassName(clazz); +}; + +function setPingValue(peer, value) { + var cellText; + var peerCell = $(peer); + if (!peerCell) return; + var peerTable = $("peer_list"); + if (value === "-1") { + var peerAddress = $("label_" + peer); + peerAddress.style.color = "rgba(250,250,250,.5)"; + } else { + + cellText = document.createTextNode(value); + peerCell.appendChild(cellText); + + var peerCellTime = $("time_" + peer); + var cellTextTime = document.createTextNode("ms"); + peerCellTime.appendChild(cellTextTime); + } + peerCell.parentNode.classList.remove("is-hidden"); + //sort table + moveRowToOrderPos(peerTable, 2, peerCell.parentNode); +} + +function cmpTime(a, b) { + return a.textContent.trim() === "" ? 1 : a.textContent.trim() // using `.textContent.trim()` for test + .localeCompare(b.textContent.trim(), 'en', { numeric: true }); +} + +function moveRowToOrderPos(table, col, row) { + var tb = table.tBodies[0], + tr = tb.rows; + var i = 0; + for (; i < tr.length && cmpTime(row.cells[col], tr[i].cells[col]) >= 0; ++i) {} + if (i < tr.length && i != row.rowIndex) { + tb.deleteRow(row.rowIndex); + tb.insertBefore(row, tr[i]); + } +} + +function openTab(element, tabName) { + // Declare all variables + var i, tabContent, tabLinks; + + // Get all elements with class="content" and hide them + tabContent = $$("tab here"); + for (i = 0; i < tabContent.length; i++) { + tabContent[i].className = "tab here is-hidden"; + } + + // Get all elements with class="tab" and remove the class "is-active" + tabLinks = $$("tab is-active"); + for (i = 0; i < tabLinks.length; i++) { + tabLinks[i].className = "tab"; + } + + // Show the current tab, and add an "is-active" class to the button that opened the tab + $(tabName).className = "tab here"; + element.parentElement.className = "tab is-active"; + //refreshRecordsList(); +} + +function copy2clipboard(text) { + var textArea = document.createElement("textarea"); + textArea.style.position = 'fixed'; + textArea.style.top = 0; + textArea.style.left = 0; + + // Ensure it has a small width and height. Setting to 1px / 1em + // doesn't work as this gives a negative w/h on some browsers. + textArea.style.width = '2em'; + textArea.style.height = '2em'; + + // We don't need padding, reducing the size if it does flash render. + textArea.style.padding = 0; + + // Clean up any borders. + textArea.style.border = 'none'; + textArea.style.outline = 'none'; + textArea.style.boxShadow = 'none'; + + // Avoid flash of the white box if rendered for any reason. + textArea.style.background = 'transparent'; + textArea.value = text; + + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + try { + var successful = document.execCommand('copy'); + var msg = successful ? 'successful' : 'unsuccessful'; + console.log('Copying text command was ' + msg); + } catch (err) { + console.log('Oops, unable to copy'); + } + document.body.removeChild(textArea); + showInfo('value copied successfully!'); +} + +function showInfo(text) { + var info = $("notification_info"); + var message = $("info_text"); + message.innerHTML = text; + + info.className = "notification is-primary"; + var button = $("info_close"); + button.onclick = function () { + message.value = ""; + info.className = "notification is-primary is-hidden"; + }; + setTimeout(button.onclick, 2000); +} + +function showWindow(text) { + var info = $("notification_window"); + var message = $("info_window"); + message.innerHTML = text; + + info.classList.remove("is-hidden"); + var button_info_close = $("info_win_close"); + button_info_close.onclick = function () { + message.value = ""; + info.classList.add("is-hidden"); + $("peer_list").remove(); + }; + var button_window_close = $("window_close"); + button_window_close.onclick = function () { + message.value = ""; + info.classList.add("is-hidden"); + $("peer_list").remove(); + }; + var button_window_save = $("window_save"); + button_window_save.onclick = function () { + message.value = ""; + info.classList.add("is-hidden"); + //todo save peers + var peers = document.querySelectorAll('*[id^="peer-"]'); + var peer_list = []; + for (i = 0; i < peers.length; ++i) { + var p = peers[i]; + if (p.checked) { + var peerURL = p.parentElement.parentElement.children[1].innerText; + peer_list.push(peerURL); + } + } + fetch('api/peers', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Riv-Save-Config': 'true' + }, + body: JSON.stringify(peer_list) + }).catch(function (error) { + console.error('Error:', error); + }); + $("peer_list").remove(); + }; +} + +function add_table(peerList) { + + var peers = []; + //const countries = Object.keys(peerList); + // get the reference for the body + var body = document.createElement("div"); + // creates a element and a element + var tbl = document.createElement("table"); + tbl.setAttribute('id', "peer_list"); + //tbl.setAttribute('cellpadding', '10'); + var tblBody = document.createElement("tbody"); + + // creating all cells + for (var c in peerList) { + var counter = 1; + for (var peer in peerList[c]) { + peers.push(peer); + // creates a table row + var row = document.createElement("tr"); + row.className = "is-hidden"; + var imgElement = document.createElement("td"); + imgElement.className = "big-flag fi fi-" + ui.lookupCountryCodeByAddress(peer); + var peerAddress = document.createElement("td"); + var cellText = document.createTextNode(peer); + peerAddress.appendChild(cellText); + peerAddress.setAttribute('id', "label_" + peer); + var peerPing = document.createElement("td"); + peerPing.setAttribute('id', peer); + var peerPingTime = document.createElement("td"); + peerPingTime.setAttribute('id', "time_" + peer); + var peerSelect = document.createElement("td"); + var chk = document.createElement('input'); + chk.setAttribute('type', 'checkbox'); + chk.setAttribute('id', "peer-" + counter); + peerSelect.appendChild(chk); + + row.appendChild(imgElement); + row.appendChild(peerAddress); + row.appendChild(peerPing); + row.appendChild(peerPingTime); + row.appendChild(peerSelect); + tblBody.appendChild(row); + } + } + // put the in the
+ tbl.appendChild(tblBody); + // appends
into + body.appendChild(tbl); + // sets the border attribute of tbl to 2; + //tbl.setAttribute("border", "0"); + showWindow(body.innerHTML); + return peers; +} + +function togglePrivKeyVisibility() { + if (this.classList.contains("fa-eye")) { + this.classList.remove("fa-eye"); + this.classList.add("fa-eye-slash"); + $("priv_key_visible").innerHTML = $("priv_key").innerHTML; + } else { + this.classList.remove("fa-eye-slash"); + this.classList.add("fa-eye"); + $("priv_key_visible").innerHTML = "••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••"; + } +} + +function humanReadableSpeed(speed) { + var i = speed == 0 ? 0 : Math.floor(Math.log(speed) / Math.log(1024)); + return (speed / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['Bps', 'kBps', 'MBps', 'GBps', 'TBps'][i]; +} + +var ui = ui || { + countries: [] +}; + +ui.getAllPeers = function () { + if (!("_allPeersPromise" in ui)) { + ui._allPeersPromise = new Promise(function (resolve, reject) { + if ("_allPeers" in ui) resolve(ui._allPeers);else fetch('https://map.rivchain.org/rest/peers.json').then(function (response) { + return response.json(); + }).then(function (data) { + var _loop = function _loop() { + var country = c.slice(0, -3); + var filtered = ui.countries.find(function (entry) { + return entry.name.toLowerCase() == country; + }); + //let flagFile = filtered ? filtered["flag_4x3"] : ""; + var flagCode = filtered ? filtered["code"] : ""; + for (var peer in data[c]) { + data[c][peer].countryCode = flagCode; + } + }; + + // add country code to each peer + for (var c in data) { + _loop(); + } + ui._allPeers = data; + resolve(ui._allPeers); + }).catch(reject); + }).finally(function () { + return delete ui._allPeersPromise; + }); + } + return ui._allPeersPromise; +}; + +ui.showAllPeers = function () { + return ui.getAllPeers().then(function (peerList) { + var peers = add_table(peerList); + //start peers test + fetch('api/ping', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(peers) + }).catch(function (error) { + console.error('Error:', error); + }); + }).catch(function (error) { + console.error(error); + }); +}; + +ui.getConnectedPeers = function () { + return fetch('api/peers').then(function (response) { + return response.json(); + }); +}; + +var regexMulticast = /:\/\/\[fe80::/; +ui.updateConnectedPeersHandler = function (peers) { + $("peers").innerText = ""; + var regexStrip = /%[^\]]*/gm; + var sorted = peers.map(function (peer) { + return { "url": peer["remote"], "isMulticast": peer["remote"].match(regexMulticast) }; + }).sort(function (a, b) { + return a.isMulticast > b.isMulticast; + }); + sorted.forEach(function (peer) { + var row = $("peers").appendChild(document.createElement('div')); + row.className = "overflow-ellipsis"; + var flag = row.appendChild(document.createElement("span")); + if (peer.isMulticast) flag.className = "fa fa-thin fa-share-nodes peer-connected-fl";else flag.className = "fi fi-" + ui.lookupCountryCodeByAddress(peer.url) + " peer-connected-fl"; + row.append(peer.url.replace(regexStrip, "")); + }); +}; + +ui.updateStatus = function (peers) { + var status = "st-error"; + if (peers) { + if (peers.length) { + var isNonMulticastExists = peers.filter(function (peer) { + return !peer["remote"].match(regexMulticast); + }).length; + status = isNonMulticastExists ? "st-multicast" : "st-connected"; + } else { + status = "st-connecting"; + } + } + Array.from($$("status")).forEach(function (node) { + return node.classList.add("is-hidden"); + }); + $(status).classList.remove("is-hidden"); +}; + +ui.updateSpeed = function (peers) { + if (peers) { + var rsbytes = { "bytes_recvd": peers.reduce(function (acc, peer) { + return acc + peer.bytes_recvd; + }, 0), + "bytes_sent": peers.reduce(function (acc, peer) { + return acc + peer.bytes_sent; + }, 0), + "timestamp": Date.now() }; + if ("_rsbytes" in ui) { + $("dn_speed").innerText = humanReadableSpeed((rsbytes.bytes_recvd - ui._rsbytes.bytes_recvd) * 1000 / (rsbytes.timestamp - ui._rsbytes.timestamp)); + $("up_speed").innerText = humanReadableSpeed((rsbytes.bytes_sent - ui._rsbytes.bytes_sent) * 1000 / (rsbytes.timestamp - ui._rsbytes.timestamp)); + } + ui._rsbytes = rsbytes; + } else { + delete ui._rsbytes; + $("dn_speed").innerText = "? Bs"; + $("up_speed").innerText = "? Bs"; + } +}; + +ui.updateConnectedPeers = function () { + return ui.getConnectedPeers().then(function (peers) { + ui.updateConnectedPeersHandler(peers); + ui.updateStatus(peers); + ui.updateSpeed(peers); + }).catch(function (error) { + $("peers").innerText = error.message; + ui.updateStatus(); + ui.updateSpeed(); + }); +}; + +ui.lookupCountryCodeByAddress = function (address) { + for (var c in ui._allPeers) { + for (var peer in ui._allPeers[c]) { + if (peer == address) return ui._allPeers[c][peer].countryCode; + } + } +}; + +ui.getSelfInfo = function () { + return fetch('api/self').then(function (response) { + return response.json(); + }); +}; + +ui.updateSelfInfo = function () { + return ui.getSelfInfo().then(function (info) { + $("ipv6").innerText = info.address; + $("subnet").innerText = info.subnet; + $("pub_key").innerText = info.key; + $("priv_key").innerText = info.private_key; + $("ipv6").innerText = info.address; + $("version").innerText = info.build_version; + }).catch(function (error) { + $("ipv6").innerText = error.message; + }); +}; + +ui.sse = new EventSource('/api/sse'); + +function main() { + + window.addEventListener("load", function () { + $("showAllPeersBtn").addEventListener("click", ui.showAllPeers); + + ui.getAllPeers().then(function () { + return ui.updateConnectedPeers(); + }); + setInterval(ui.updateConnectedPeers, 5000); + + ui.updateSelfInfo(); + //setInterval(ui.updateSelfInfo, 5000); + + ui.sse.addEventListener("ping", function (e) { + var data = JSON.parse(e.data); + setPingValue(data.peer, data.value); + }); + + ui.sse.addEventListener("peers", function (e) { + ui.updateConnectedPeersHandler(JSON.parse(e.data)); + }); + }); +} + +main(); diff --git a/contrib/ui/mesh-ui/ui/assets/mesh-ui.css b/contrib/ui/mesh-ui/ui/assets/mesh-ui.css index 056251a6..2d981ddc 100644 --- a/contrib/ui/mesh-ui/ui/assets/mesh-ui.css +++ b/contrib/ui/mesh-ui/ui/assets/mesh-ui.css @@ -68,12 +68,9 @@ img { display: block; } -html, body { - height: 100vh; -} - body { max-width: 690px; + min-width: 690px; margin: auto; } @@ -86,6 +83,13 @@ td.fi { background-position: 0; } +footer { + position:fixed; + bottom:0; + max-width: 690px; + margin-top: auto; +} + #footer-row td { padding: 10px; white-space: nowrap; diff --git a/contrib/ui/mesh-ui/ui/assets/mesh-ui.js b/contrib/ui/mesh-ui/ui/assets/mesh-ui.js index cc836c49..294b4f3d 100644 --- a/contrib/ui/mesh-ui/ui/assets/mesh-ui.js +++ b/contrib/ui/mesh-ui/ui/assets/mesh-ui.js @@ -229,7 +229,7 @@ function humanReadableSpeed(speed) { return (speed / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['Bps', 'kBps', 'MBps', 'GBps', 'TBps'][i]; } -var ui = { +var ui = ui || { countries: [] }; @@ -372,10 +372,6 @@ ui.sse = new EventSource('/api/sse'); function main() { - fetch('country.json') - .then((response) => response.json()) - .then((data) => { ui.countries = data }); - window.addEventListener("load", () => { $("showAllPeersBtn").addEventListener("click", ui.showAllPeers); diff --git a/contrib/ui/mesh-ui/ui/assets/polyfills.js b/contrib/ui/mesh-ui/ui/assets/polyfills.js new file mode 100644 index 00000000..54c6c2b2 --- /dev/null +++ b/contrib/ui/mesh-ui/ui/assets/polyfills.js @@ -0,0 +1,798 @@ +/** + * https://github.com/taylorhakes/promise-polyfill + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t():"function"==typeof define&&define.amd?define(t):t()}(0,function(){"use strict";function e(e){var t=this.constructor;return this.then(function(n){return t.resolve(e()).then(function(){return n})},function(n){return t.resolve(e()).then(function(){return t.reject(n)})})}function t(e){return new this(function(t,n){function o(e,n){if(n&&("object"==typeof n||"function"==typeof n)){var f=n.then;if("function"==typeof f)return void f.call(n,function(t){o(e,t)},function(n){r[e]={status:"rejected",reason:n},0==--i&&t(r)})}r[e]={status:"fulfilled",value:n},0==--i&&t(r)}if(!e||"undefined"==typeof e.length)return n(new TypeError(typeof e+" "+e+" is not iterable(cannot read property Symbol(Symbol.iterator))"));var r=Array.prototype.slice.call(e);if(0===r.length)return t([]);for(var i=r.length,f=0;r.length>f;f++)o(f,r[f])})}function n(e){return!(!e||"undefined"==typeof e.length)}function o(){}function r(e){if(!(this instanceof r))throw new TypeError("Promises must be constructed via new");if("function"!=typeof e)throw new TypeError("not a function");this._state=0,this._handled=!1,this._value=undefined,this._deferreds=[],l(e,this)}function i(e,t){for(;3===e._state;)e=e._value;0!==e._state?(e._handled=!0,r._immediateFn(function(){var n=1===e._state?t.onFulfilled:t.onRejected;if(null!==n){var o;try{o=n(e._value)}catch(r){return void u(t.promise,r)}f(t.promise,o)}else(1===e._state?f:u)(t.promise,e._value)})):e._deferreds.push(t)}function f(e,t){try{if(t===e)throw new TypeError("A promise cannot be resolved with itself.");if(t&&("object"==typeof t||"function"==typeof t)){var n=t.then;if(t instanceof r)return e._state=3,e._value=t,void c(e);if("function"==typeof n)return void l(function(e,t){return function(){e.apply(t,arguments)}}(n,t),e)}e._state=1,e._value=t,c(e)}catch(o){u(e,o)}}function u(e,t){e._state=2,e._value=t,c(e)}function c(e){2===e._state&&0===e._deferreds.length&&r._immediateFn(function(){e._handled||r._unhandledRejectionFn(e._value)});for(var t=0,n=e._deferreds.length;n>t;t++)i(e,e._deferreds[t]);e._deferreds=null}function l(e,t){var n=!1;try{e(function(e){n||(n=!0,f(t,e))},function(e){n||(n=!0,u(t,e))})}catch(o){if(n)return;n=!0,u(t,o)}}var a=setTimeout;r.prototype["catch"]=function(e){return this.then(null,e)},r.prototype.then=function(e,t){var n=new this.constructor(o);return i(this,new function(e,t,n){this.onFulfilled="function"==typeof e?e:null,this.onRejected="function"==typeof t?t:null,this.promise=n}(e,t,n)),n},r.prototype["finally"]=e,r.all=function(e){return new r(function(t,o){function r(e,n){try{if(n&&("object"==typeof n||"function"==typeof n)){var u=n.then;if("function"==typeof u)return void u.call(n,function(t){r(e,t)},o)}i[e]=n,0==--f&&t(i)}catch(c){o(c)}}if(!n(e))return o(new TypeError("Promise.all accepts an array"));var i=Array.prototype.slice.call(e);if(0===i.length)return t([]);for(var f=i.length,u=0;i.length>u;u++)r(u,i[u])})},r.allSettled=t,r.resolve=function(e){return e&&"object"==typeof e&&e.constructor===r?e:new r(function(t){t(e)})},r.reject=function(e){return new r(function(t,n){n(e)})},r.race=function(e){return new r(function(t,o){if(!n(e))return o(new TypeError("Promise.race accepts an array"));for(var i=0,f=e.length;f>i;i++)r.resolve(e[i]).then(t,o)})},r._immediateFn="function"==typeof setImmediate&&function(e){setImmediate(e)}||function(e){a(e,0)},r._unhandledRejectionFn=function(e){void 0!==console&&console&&console.warn("Possible Unhandled Promise Rejection:",e)};var s=function(){if("undefined"!=typeof self)return self;if("undefined"!=typeof window)return window;if("undefined"!=typeof global)return global;throw Error("unable to locate global object")}();"function"!=typeof s.Promise?s.Promise=r:(s.Promise.prototype["finally"]||(s.Promise.prototype["finally"]=e),s.Promise.allSettled||(s.Promise.allSettled=t))}); +/** @license + * eventsource.js + * Available under MIT License (MIT) + * https://github.com/Yaffle/EventSource/ + */ +!function(e){"use strict";var r,H=e.setTimeout,N=e.clearTimeout,j=e.XMLHttpRequest,o=e.XDomainRequest,t=e.ActiveXObject,n=e.EventSource,i=e.document,w=e.Promise,d=e.fetch,a=e.Response,h=e.TextDecoder,s=e.TextEncoder,p=e.AbortController;function c(){this.bitsNeeded=0,this.codePoint=0}"undefined"==typeof window||void 0===i||"readyState"in i||null!=i.body||(i.readyState="loading",window.addEventListener("load",function(e){i.readyState="complete"},!1)),null==j&&null!=t&&(j=function(){return new t("Microsoft.XMLHTTP")}),null==Object.create&&(Object.create=function(e){function t(){}return t.prototype=e,new t}),Date.now||(Date.now=function(){return(new Date).getTime()}),null==p&&(r=d,d=function(e,t){var n=t.signal;return r(e,{headers:t.headers,credentials:t.credentials,cache:t.cache}).then(function(e){var t=e.body.getReader();return n._reader=t,n._aborted&&n._reader.cancel(),{status:e.status,statusText:e.statusText,headers:e.headers,body:{getReader:function(){return t}}}})},p=function(){this.signal={_reader:null,_aborted:!1},this.abort=function(){null!=this.signal._reader&&this.signal._reader.cancel(),this.signal._aborted=!0}}),c.prototype.decode=function(e){function t(e,t,n){if(1===n)return 128>>t<=e&&e<>t<=e&&e<>t<=e&&e<>t<=e&&e<>6?3:31>10)))+String.fromCharCode(56320+(i-65535-1&1023)))}return this.bitsNeeded=o,this.codePoint=i,r};function u(){}null!=h&&null!=s&&function(){try{return"test"===(new h).decode((new s).encode("test"),{stream:!0})}catch(e){}return!1}()||(h=c);function I(e){this.withCredentials=!1,this.readyState=0,this.status=0,this.statusText="",this.responseText="",this.onprogress=u,this.onload=u,this.onerror=u,this.onreadystatechange=u,this._contentType="",this._xhr=e,this._sendTimeout=0,this._abort=u}function l(e){return e.replace(/[A-Z]/g,function(e){return String.fromCharCode(e.charCodeAt(0)+32)})}function f(e){for(var t=Object.create(null),n=e.split("\r\n"),r=0;r -1 + }; + } + + function normalizeName(name) { + if (typeof name !== 'string') { + name = String(name); + } + if (/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(name) || name === '') { + throw new TypeError('Invalid character in header field name') + } + return name.toLowerCase() + } + + function normalizeValue(value) { + if (typeof value !== 'string') { + value = String(value); + } + return value + } + + // Build a destructive iterator for the value list + function iteratorFor(items) { + var iterator = { + next: function() { + var value = items.shift(); + return {done: value === undefined, value: value} + } + }; + + if (support.iterable) { + iterator[Symbol.iterator] = function() { + return iterator + }; + } + + return iterator + } + + function Headers(headers) { + this.map = {}; + + if (headers instanceof Headers) { + headers.forEach(function(value, name) { + this.append(name, value); + }, this); + } else if (Array.isArray(headers)) { + headers.forEach(function(header) { + this.append(header[0], header[1]); + }, this); + } else if (headers) { + Object.getOwnPropertyNames(headers).forEach(function(name) { + this.append(name, headers[name]); + }, this); + } + } + + Headers.prototype.append = function(name, value) { + name = normalizeName(name); + value = normalizeValue(value); + var oldValue = this.map[name]; + this.map[name] = oldValue ? oldValue + ', ' + value : value; + }; + + Headers.prototype['delete'] = function(name) { + delete this.map[normalizeName(name)]; + }; + + Headers.prototype.get = function(name) { + name = normalizeName(name); + return this.has(name) ? this.map[name] : null + }; + + Headers.prototype.has = function(name) { + return this.map.hasOwnProperty(normalizeName(name)) + }; + + Headers.prototype.set = function(name, value) { + this.map[normalizeName(name)] = normalizeValue(value); + }; + + Headers.prototype.forEach = function(callback, thisArg) { + for (var name in this.map) { + if (this.map.hasOwnProperty(name)) { + callback.call(thisArg, this.map[name], name, this); + } + } + }; + + Headers.prototype.keys = function() { + var items = []; + this.forEach(function(value, name) { + items.push(name); + }); + return iteratorFor(items) + }; + + Headers.prototype.values = function() { + var items = []; + this.forEach(function(value) { + items.push(value); + }); + return iteratorFor(items) + }; + + Headers.prototype.entries = function() { + var items = []; + this.forEach(function(value, name) { + items.push([name, value]); + }); + return iteratorFor(items) + }; + + if (support.iterable) { + Headers.prototype[Symbol.iterator] = Headers.prototype.entries; + } + + function consumed(body) { + if (body.bodyUsed) { + return Promise.reject(new TypeError('Already read')) + } + body.bodyUsed = true; + } + + function fileReaderReady(reader) { + return new Promise(function(resolve, reject) { + reader.onload = function() { + resolve(reader.result); + }; + reader.onerror = function() { + reject(reader.error); + }; + }) + } + + function readBlobAsArrayBuffer(blob) { + var reader = new FileReader(); + var promise = fileReaderReady(reader); + reader.readAsArrayBuffer(blob); + return promise + } + + function readBlobAsText(blob) { + var reader = new FileReader(); + var promise = fileReaderReady(reader); + reader.readAsText(blob); + return promise + } + + function readArrayBufferAsText(buf) { + var view = new Uint8Array(buf); + var chars = new Array(view.length); + + for (var i = 0; i < view.length; i++) { + chars[i] = String.fromCharCode(view[i]); + } + return chars.join('') + } + + function bufferClone(buf) { + if (buf.slice) { + return buf.slice(0) + } else { + var view = new Uint8Array(buf.byteLength); + view.set(new Uint8Array(buf)); + return view.buffer + } + } + + function Body() { + this.bodyUsed = false; + + this._initBody = function(body) { + /* + fetch-mock wraps the Response object in an ES6 Proxy to + provide useful test harness features such as flush. However, on + ES5 browsers without fetch or Proxy support pollyfills must be used; + the proxy-pollyfill is unable to proxy an attribute unless it exists + on the object before the Proxy is created. This change ensures + Response.bodyUsed exists on the instance, while maintaining the + semantic of setting Request.bodyUsed in the constructor before + _initBody is called. + */ + this.bodyUsed = this.bodyUsed; + this._bodyInit = body; + if (!body) { + this._bodyText = ''; + } else if (typeof body === 'string') { + this._bodyText = body; + } else if (support.blob && Blob.prototype.isPrototypeOf(body)) { + this._bodyBlob = body; + } else if (support.formData && FormData.prototype.isPrototypeOf(body)) { + this._bodyFormData = body; + } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) { + this._bodyText = body.toString(); + } else if (support.arrayBuffer && support.blob && isDataView(body)) { + this._bodyArrayBuffer = bufferClone(body.buffer); + // IE 10-11 can't handle a DataView body. + this._bodyInit = new Blob([this._bodyArrayBuffer]); + } else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) { + this._bodyArrayBuffer = bufferClone(body); + } else { + this._bodyText = body = Object.prototype.toString.call(body); + } + + if (!this.headers.get('content-type')) { + if (typeof body === 'string') { + this.headers.set('content-type', 'text/plain;charset=UTF-8'); + } else if (this._bodyBlob && this._bodyBlob.type) { + this.headers.set('content-type', this._bodyBlob.type); + } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) { + this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8'); + } + } + }; + + if (support.blob) { + this.blob = function() { + var rejected = consumed(this); + if (rejected) { + return rejected + } + + if (this._bodyBlob) { + return Promise.resolve(this._bodyBlob) + } else if (this._bodyArrayBuffer) { + return Promise.resolve(new Blob([this._bodyArrayBuffer])) + } else if (this._bodyFormData) { + throw new Error('could not read FormData body as blob') + } else { + return Promise.resolve(new Blob([this._bodyText])) + } + }; + + this.arrayBuffer = function() { + if (this._bodyArrayBuffer) { + var isConsumed = consumed(this); + if (isConsumed) { + return isConsumed + } + if (ArrayBuffer.isView(this._bodyArrayBuffer)) { + return Promise.resolve( + this._bodyArrayBuffer.buffer.slice( + this._bodyArrayBuffer.byteOffset, + this._bodyArrayBuffer.byteOffset + this._bodyArrayBuffer.byteLength + ) + ) + } else { + return Promise.resolve(this._bodyArrayBuffer) + } + } else { + return this.blob().then(readBlobAsArrayBuffer) + } + }; + } + + this.text = function() { + var rejected = consumed(this); + if (rejected) { + return rejected + } + + if (this._bodyBlob) { + return readBlobAsText(this._bodyBlob) + } else if (this._bodyArrayBuffer) { + return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer)) + } else if (this._bodyFormData) { + throw new Error('could not read FormData body as text') + } else { + return Promise.resolve(this._bodyText) + } + }; + + if (support.formData) { + this.formData = function() { + return this.text().then(decode) + }; + } + + this.json = function() { + return this.text().then(JSON.parse) + }; + + return this + } + + // HTTP methods whose capitalization should be normalized + var methods = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT']; + + function normalizeMethod(method) { + var upcased = method.toUpperCase(); + return methods.indexOf(upcased) > -1 ? upcased : method + } + + function Request(input, options) { + if (!(this instanceof Request)) { + throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.') + } + + options = options || {}; + var body = options.body; + + if (input instanceof Request) { + if (input.bodyUsed) { + throw new TypeError('Already read') + } + this.url = input.url; + this.credentials = input.credentials; + if (!options.headers) { + this.headers = new Headers(input.headers); + } + this.method = input.method; + this.mode = input.mode; + this.signal = input.signal; + if (!body && input._bodyInit != null) { + body = input._bodyInit; + input.bodyUsed = true; + } + } else { + this.url = String(input); + } + + this.credentials = options.credentials || this.credentials || 'same-origin'; + if (options.headers || !this.headers) { + this.headers = new Headers(options.headers); + } + this.method = normalizeMethod(options.method || this.method || 'GET'); + this.mode = options.mode || this.mode || null; + this.signal = options.signal || this.signal; + this.referrer = null; + + if ((this.method === 'GET' || this.method === 'HEAD') && body) { + throw new TypeError('Body not allowed for GET or HEAD requests') + } + this._initBody(body); + + if (this.method === 'GET' || this.method === 'HEAD') { + if (options.cache === 'no-store' || options.cache === 'no-cache') { + // Search for a '_' parameter in the query string + var reParamSearch = /([?&])_=[^&]*/; + if (reParamSearch.test(this.url)) { + // If it already exists then set the value with the current time + this.url = this.url.replace(reParamSearch, '$1_=' + new Date().getTime()); + } else { + // Otherwise add a new '_' parameter to the end with the current time + var reQueryString = /\?/; + this.url += (reQueryString.test(this.url) ? '&' : '?') + '_=' + new Date().getTime(); + } + } + } + } + + Request.prototype.clone = function() { + return new Request(this, {body: this._bodyInit}) + }; + + function decode(body) { + var form = new FormData(); + body + .trim() + .split('&') + .forEach(function(bytes) { + if (bytes) { + var split = bytes.split('='); + var name = split.shift().replace(/\+/g, ' '); + var value = split.join('=').replace(/\+/g, ' '); + form.append(decodeURIComponent(name), decodeURIComponent(value)); + } + }); + return form + } + + function parseHeaders(rawHeaders) { + var headers = new Headers(); + // Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space + // https://tools.ietf.org/html/rfc7230#section-3.2 + var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, ' '); + preProcessedHeaders.split(/\r?\n/).forEach(function(line) { + var parts = line.split(':'); + var key = parts.shift().trim(); + if (key) { + var value = parts.join(':').trim(); + headers.append(key, value); + } + }); + return headers + } + + Body.call(Request.prototype); + + function Response(bodyInit, options) { + if (!(this instanceof Response)) { + throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.') + } + if (!options) { + options = {}; + } + + this.type = 'default'; + this.status = options.status === undefined ? 200 : options.status; + this.ok = this.status >= 200 && this.status < 300; + this.statusText = 'statusText' in options ? options.statusText : ''; + this.headers = new Headers(options.headers); + this.url = options.url || ''; + this._initBody(bodyInit); + } + + Body.call(Response.prototype); + + Response.prototype.clone = function() { + return new Response(this._bodyInit, { + status: this.status, + statusText: this.statusText, + headers: new Headers(this.headers), + url: this.url + }) + }; + + Response.error = function() { + var response = new Response(null, {status: 0, statusText: ''}); + response.type = 'error'; + return response + }; + + var redirectStatuses = [301, 302, 303, 307, 308]; + + Response.redirect = function(url, status) { + if (redirectStatuses.indexOf(status) === -1) { + throw new RangeError('Invalid status code') + } + + return new Response(null, {status: status, headers: {location: url}}) + }; + + exports.DOMException = global.DOMException; + try { + new exports.DOMException(); + } catch (err) { + exports.DOMException = function(message, name) { + this.message = message; + this.name = name; + var error = Error(message); + this.stack = error.stack; + }; + exports.DOMException.prototype = Object.create(Error.prototype); + exports.DOMException.prototype.constructor = exports.DOMException; + } + + function fetch(input, init) { + return new Promise(function(resolve, reject) { + var request = new Request(input, init); + + if (request.signal && request.signal.aborted) { + return reject(new exports.DOMException('Aborted', 'AbortError')) + } + + var xhr = new XMLHttpRequest(); + + function abortXhr() { + xhr.abort(); + } + + xhr.onload = function() { + var options = { + status: xhr.status, + statusText: xhr.statusText, + headers: parseHeaders(xhr.getAllResponseHeaders() || '') + }; + options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL'); + var body = 'response' in xhr ? xhr.response : xhr.responseText; + setTimeout(function() { + resolve(new Response(body, options)); + }, 0); + }; + + xhr.onerror = function() { + setTimeout(function() { + reject(new TypeError('Network request failed')); + }, 0); + }; + + xhr.ontimeout = function() { + setTimeout(function() { + reject(new TypeError('Network request failed')); + }, 0); + }; + + xhr.onabort = function() { + setTimeout(function() { + reject(new exports.DOMException('Aborted', 'AbortError')); + }, 0); + }; + + function fixUrl(url) { + try { + return url === '' && global.location.href ? global.location.href : url + } catch (e) { + return url + } + } + + xhr.open(request.method, fixUrl(request.url), true); + + if (request.credentials === 'include') { + xhr.withCredentials = true; + } else if (request.credentials === 'omit') { + xhr.withCredentials = false; + } + + if ('responseType' in xhr) { + if (support.blob) { + xhr.responseType = 'blob'; + } else if ( + support.arrayBuffer && + request.headers.get('Content-Type') && + request.headers.get('Content-Type').indexOf('application/octet-stream') !== -1 + ) { + xhr.responseType = 'arraybuffer'; + } + } + + if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers)) { + Object.getOwnPropertyNames(init.headers).forEach(function(name) { + xhr.setRequestHeader(name, normalizeValue(init.headers[name])); + }); + } else { + request.headers.forEach(function(value, name) { + xhr.setRequestHeader(name, value); + }); + } + + if (request.signal) { + request.signal.addEventListener('abort', abortXhr); + + xhr.onreadystatechange = function() { + // DONE (success or failure) + if (xhr.readyState === 4) { + request.signal.removeEventListener('abort', abortXhr); + } + }; + } + + xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit); + }) + } + + fetch.polyfill = true; + + if (!global.fetch) { + global.fetch = fetch; + global.Headers = Headers; + global.Request = Request; + global.Response = Response; + } + + exports.Headers = Headers; + exports.Request = Request; + exports.Response = Response; + exports.fetch = fetch; + + Object.defineProperty(exports, '__esModule', { value: true }); + +}))); + + + + + + + + + + + + + + + +/** + * ChildNode.append() polyfill + * https://gomakethings.com/adding-an-element-to-the-end-of-a-set-of-elements-with-vanilla-javascript/ + * @author Chris Ferdinandi + * @license MIT + */ +(function (elem) { + + // Check if element is a node + // https://github.com/Financial-Times/polyfill-service + var isNode = function (object) { + + // DOM, Level2 + if (typeof Node === 'function') { + return object instanceof Node; + } + + // Older browsers, check if it looks like a Node instance) + return object && + typeof object === "object" && + object.nodeName && + object.nodeType >= 1 && + object.nodeType <= 12; + + }; + + // Add append() method to prototype + for (var i = 0; i < elem.length; i++) { + if (!window[elem[i]] || 'append' in window[elem[i]].prototype) continue; + window[elem[i]].prototype.append = function () { + var argArr = Array.prototype.slice.call(arguments); + var docFrag = document.createDocumentFragment(); + + for (var n = 0; n < argArr.length; n++) { + docFrag.appendChild(isNode(argArr[n]) ? argArr[n] : document.createTextNode(String(argArr[n]))); + } + + this.appendChild(docFrag); + }; + } + +})(['Element', 'CharacterData', 'DocumentType']); + + + + + + + + + +/** + * https://github.com/jsPolyfill/Array.prototype.find/blob/master/find.js + */ + +'use strict'; + +Array.prototype.find = Array.prototype.find || function(callback) { + if (this === null) { + throw new TypeError('Array.prototype.find called on null or undefined'); + } else if (typeof callback !== 'function') { + throw new TypeError('callback must be a function'); + } + var list = Object(this); + // Makes sures is always has an positive integer as length. + var length = list.length >>> 0; + var thisArg = arguments[1]; + for (var i = 0; i < length; i++) { + var element = list[i]; + if ( callback.call(thisArg, element, i, list) ) { + return element; + } + } +}; + + + + +/** + * Array.from() polyfill + */ +// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from +// Production steps of ECMA-262, Edition 6, 22.1.2.1 +if (!Array.from) { + Array.from = (function () { + var toStr = Object.prototype.toString; + var isCallable = function (fn) { + return typeof fn === 'function' || toStr.call(fn) === '[object Function]'; + }; + var toInteger = function (value) { + var number = Number(value); + if (isNaN(number)) { return 0; } + if (number === 0 || !isFinite(number)) { return number; } + return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number)); + }; + var maxSafeInteger = Math.pow(2, 53) - 1; + var toLength = function (value) { + var len = toInteger(value); + return Math.min(Math.max(len, 0), maxSafeInteger); + }; + + // The length property of the from method is 1. + return function from(arrayLike/*, mapFn, thisArg */) { + // 1. Let C be the this value. + var C = this; + + // 2. Let items be ToObject(arrayLike). + var items = Object(arrayLike); + + // 3. ReturnIfAbrupt(items). + if (arrayLike == null) { + throw new TypeError('Array.from requires an array-like object - not null or undefined'); + } + + // 4. If mapfn is undefined, then let mapping be false. + var mapFn = arguments.length > 1 ? arguments[1] : void undefined; + var T; + if (typeof mapFn !== 'undefined') { + // 5. else + // 5. a If IsCallable(mapfn) is false, throw a TypeError exception. + if (!isCallable(mapFn)) { + throw new TypeError('Array.from: when provided, the second argument must be a function'); + } + + // 5. b. If thisArg was supplied, let T be thisArg; else let T be undefined. + if (arguments.length > 2) { + T = arguments[2]; + } + } + + // 10. Let lenValue be Get(items, "length"). + // 11. Let len be ToLength(lenValue). + var len = toLength(items.length); + + // 13. If IsConstructor(C) is true, then + // 13. a. Let A be the result of calling the [[Construct]] internal method + // of C with an argument list containing the single item len. + // 14. a. Else, Let A be ArrayCreate(len). + var A = isCallable(C) ? Object(new C(len)) : new Array(len); + + // 16. Let k be 0. + var k = 0; + // 17. Repeat, while k < len… (also steps a - h) + var kValue; + while (k < len) { + kValue = items[k]; + if (mapFn) { + A[k] = typeof T === 'undefined' ? mapFn(kValue, k) : mapFn.call(T, kValue, k); + } else { + A[k] = kValue; + } + k += 1; + } + // 18. Let putStatus be Put(A, "length", len, true). + A.length = len; + // 20. Return A. + return A; + }; + }()); +} \ No newline at end of file diff --git a/contrib/ui/mesh-ui/ui/country.json b/contrib/ui/mesh-ui/ui/country.json index c528d21d..d7cfc574 100644 --- a/contrib/ui/mesh-ui/ui/country.json +++ b/contrib/ui/mesh-ui/ui/country.json @@ -1,4 +1,6 @@ -[ +var ui = ui || {}; + +ui.countries = [ { "capital": "Georgetown", "code": "ac", diff --git a/contrib/ui/mesh-ui/ui/index.html b/contrib/ui/mesh-ui/ui/index.html index c222530b..95983b3b 100755 --- a/contrib/ui/mesh-ui/ui/index.html +++ b/contrib/ui/mesh-ui/ui/index.html @@ -11,11 +11,27 @@ - + + + - -
+ +
-
+
+

@@ -181,6 +198,4 @@
- - diff --git a/src/admin/admin.go b/src/admin/admin.go index 7db6f4a6..bf895993 100644 --- a/src/admin/admin.go +++ b/src/admin/admin.go @@ -254,10 +254,16 @@ func (a *AdminSocket) StartHttpServer(configFn string, nc *config.NodeConfig) { a.log.Errorln("An error occurred parsing http address:", err) return } + addNoCacheHeaders := func(w http.ResponseWriter) { + w.Header().Add("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Add("Pragma", "no-cache") + w.Header().Add("Expires", "0") + } http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Following methods are allowed: getself, getpeers. litening"+u.Host) }) http.HandleFunc("/api/self", func(w http.ResponseWriter, r *http.Request) { + addNoCacheHeaders(w) switch r.Method { case "GET": w.Header().Add("Content-Type", "application/json") @@ -316,6 +322,7 @@ func (a *AdminSocket) StartHttpServer(configFn string, nc *config.NodeConfig) { return nil } + addNoCacheHeaders(w) switch r.Method { case "GET": w.Header().Add("Content-Type", "application/json") @@ -365,6 +372,7 @@ func (a *AdminSocket) StartHttpServer(configFn string, nc *config.NodeConfig) { }) http.HandleFunc("/api/sse", func(w http.ResponseWriter, r *http.Request) { + addNoCacheHeaders(w) switch r.Method { case "GET": w.Header().Add("Content-Type", "text/event-stream") @@ -399,9 +407,7 @@ func (a *AdminSocket) StartHttpServer(configFn string, nc *config.NodeConfig) { if docFs == "" { var nocache = func(fs http.Handler) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Cache-Control", "no-cache, no-store, must-revalidate") - w.Header().Add("Pragma", "no-cache") - w.Header().Add("Expires", "0") + addNoCacheHeaders(w) fs.ServeHTTP(w, r) } }