mirror of
https://github.com/r4sas/Niflheim-api
synced 2025-02-11 14:34:14 +00:00
update files, add map and few more scripts
Signed-off-by: r4sas <r4sas@i2pmail.org>
This commit is contained in:
parent
87f5936e83
commit
8897b48fc9
@ -1,6 +1,6 @@
|
||||
# niflheim-api.py
|
||||
# Yggdrasil-monitor
|
||||
|
||||
Niflheim-api provides both a web interface and an api. The web interface is used to see some basic stats on the data vserv.py has collected and the API provides raw data in JSON format.
|
||||
Yggdrasil-monitor provides both a web interface and an api. The web interface is used to see some basic stats on the data has collected by crawler and the API provides raw data in JSON format.
|
||||
|
||||
## Install & Setup
|
||||
|
||||
|
14
api/addresses.py
Normal file
14
api/addresses.py
Normal file
@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import json
|
||||
from pk2addr import keyTo128BitAddress
|
||||
|
||||
with open('api/result.json', 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
addlist = open("api/addresses.txt", "w")
|
||||
|
||||
for key, _ in data['yggnodes'].items():
|
||||
addlist.write(keyTo128BitAddress(key) + "\n")
|
||||
|
||||
addlist.close()
|
24
api/config.py.example
Normal file
24
api/config.py.example
Normal file
@ -0,0 +1,24 @@
|
||||
### Configuration file ###
|
||||
|
||||
### Database configuration ###
|
||||
DB_PASS = "password"
|
||||
DB_USER = "yggindex"
|
||||
DB_NAME = "yggindex"
|
||||
DB_HOST = "localhost"
|
||||
|
||||
DB_RETRIES = 3
|
||||
DB_RECONNIDLE = 2
|
||||
|
||||
# count peer alive if it was available not more that amount of seconds ago
|
||||
# I'm using 1 hour beause of running crawler every 30 minutes
|
||||
ALIVE_SECONDS = 3600 # 1 hour
|
||||
|
||||
### Built-in crawler configuration ###
|
||||
# Configuration to use TCP connection or unix domain socket for admin connection to yggdrasil
|
||||
useAdminSock = True
|
||||
yggAdminTCP = ('localhost', 9001)
|
||||
yggAdminSock = ('/var/run/yggdrasil.sock')
|
||||
|
||||
# Save in database node info fields like buildname, buildarch, etc. (True/False)?
|
||||
saveDefaultNodeInfo = False
|
||||
removableFileds = ['buildname', 'buildarch', 'buildplatform', 'buildversion', 'board_name', 'kernel', 'model', 'system']
|
@ -12,22 +12,7 @@ import ipaddress
|
||||
import traceback
|
||||
from threading import Lock, Thread
|
||||
from queue import Queue
|
||||
|
||||
#####
|
||||
|
||||
# Configuration to use TCP connection or unix domain socket for admin connection to yggdrasil
|
||||
useAdminSock = True
|
||||
yggAdminTCP = ('localhost', 9001)
|
||||
yggAdminSock = ('/var/run/yggdrasil.sock')
|
||||
|
||||
DB_PASS = "password"
|
||||
DB_USER = "yggindex"
|
||||
DB_NAME = "yggindex"
|
||||
DB_HOST = "localhost"
|
||||
|
||||
## Save in database node info fields like buildname, buildarch, etc. (True/False)?
|
||||
saveDefaultNodeInfo = False
|
||||
removableFileds = ['buildname', 'buildarch', 'buildplatform', 'buildversion', 'board_name', 'kernel', 'model', 'system']
|
||||
from config import DB_PASS, DB_USER, DB_NAME, DB_HOST, useAdminSock, yggAdminTCP, yggAdminSock, saveDefaultNodeInfo, removableFileds
|
||||
|
||||
#####
|
||||
|
||||
@ -210,11 +195,11 @@ for k,v in selfInfo['response']['self'].items():
|
||||
# Loop over rumored nodes and ping them, adding to visited if they respond
|
||||
while len(rumored) > 0:
|
||||
for k,v in rumored.items():
|
||||
#print("Processing", v['coords'])
|
||||
# print("Processing", v['coords'])
|
||||
handleResponse(k, v, doRequest(getDHTPingRequest(v['box_pub_key'], v['coords'])))
|
||||
break
|
||||
del rumored[k]
|
||||
#End
|
||||
# End
|
||||
|
||||
nodeinfopool.wait()
|
||||
|
||||
|
112
api/graphPlotter.py
Normal file
112
api/graphPlotter.py
Normal file
@ -0,0 +1,112 @@
|
||||
import pygraphviz as pgv
|
||||
import time
|
||||
import json
|
||||
import networkx as nx
|
||||
from networkx.algorithms import centrality
|
||||
import urllib.request
|
||||
|
||||
def position_nodes(nodes, edges):
|
||||
G = pgv.AGraph(strict=True, directed=False, size='10!')
|
||||
|
||||
for n in nodes.values():
|
||||
G.add_node(n.ip, label=n.label, coords=n.coords)
|
||||
|
||||
for e in edges:
|
||||
G.add_edge(e.a.ip, e.b.ip, len=1.0)
|
||||
|
||||
G.layout(prog='neato', args='-Gepsilon=0.0001 -Gmaxiter=100000')
|
||||
|
||||
return G
|
||||
|
||||
def compute_betweenness(G):
|
||||
ng = nx.Graph()
|
||||
for start in G.iternodes():
|
||||
others = G.neighbors(start)
|
||||
for other in others:
|
||||
ng.add_edge(start, other)
|
||||
|
||||
c = centrality.betweenness_centrality(ng)
|
||||
|
||||
for k, v in c.items():
|
||||
c[k] = v
|
||||
|
||||
return c
|
||||
|
||||
def canonalize_ip(ip):
|
||||
return ':'.join( i.rjust(4, '0') for i in ip.split(':') )
|
||||
|
||||
def load_db():
|
||||
url = "http://[316:c51a:62a3:8b9::2]/result.json"
|
||||
f = urllib.request.urlopen(url)
|
||||
return dict(
|
||||
[
|
||||
(canonalize_ip(v[0]), v[1]) for v in
|
||||
[
|
||||
l.split(None)[:2] for l in
|
||||
json.loads(f.read())["yggnodes"].keys()
|
||||
]
|
||||
if len(v) > 1
|
||||
]
|
||||
)
|
||||
|
||||
def get_graph_json(G):
|
||||
max_neighbors = 1
|
||||
for n in G.iternodes():
|
||||
neighbors = len(G.neighbors(n))
|
||||
if neighbors > max_neighbors:
|
||||
max_neighbors = neighbors
|
||||
print('Max neighbors: %d' % max_neighbors)
|
||||
|
||||
out_data = {
|
||||
'created': int(time.time()),
|
||||
'nodes': [],
|
||||
'edges': []
|
||||
}
|
||||
|
||||
centralities = compute_betweenness(G)
|
||||
db = load_db()
|
||||
|
||||
for n in G.iternodes():
|
||||
neighbor_ratio = len(G.neighbors(n)) / float(max_neighbors)
|
||||
pos = n.attr['pos'].split(',', 1)
|
||||
centrality = centralities.get(n.name, 0)
|
||||
size = 5*(1 + 1*centrality)
|
||||
name = db.get(canonalize_ip(n.name))
|
||||
# If label isn't the default value, set name to that instead
|
||||
if n.attr['label'] != n.name.split(':')[-1]: name = n.attr['label']
|
||||
|
||||
out_data['nodes'].append({
|
||||
'id': n.name,
|
||||
'label': name if name else n.attr['label'],
|
||||
'name': name,
|
||||
'coords': n.attr['coords'],
|
||||
'x': float(pos[0]),
|
||||
'y': float(pos[1]),
|
||||
'color': _gradient_color(neighbor_ratio, [(100, 100, 100), (0, 0, 0)]),
|
||||
'size': size,
|
||||
'centrality': '%.4f' % centrality
|
||||
})
|
||||
|
||||
for e in G.iteredges():
|
||||
out_data['edges'].append({
|
||||
'sourceID': e[0],
|
||||
'targetID': e[1]
|
||||
})
|
||||
|
||||
return json.dumps(out_data)
|
||||
|
||||
|
||||
def _gradient_color(ratio, colors):
|
||||
jump = 1.0 / (len(colors) - 1)
|
||||
gap_num = int(ratio / (jump + 0.0000001))
|
||||
|
||||
a = colors[gap_num]
|
||||
b = colors[gap_num + 1]
|
||||
|
||||
ratio = (ratio - gap_num * jump) * (len(colors) - 1)
|
||||
|
||||
r = int(a[0] + (b[0] - a[0]) * ratio)
|
||||
g = int(a[1] + (b[1] - a[1]) * ratio)
|
||||
b = int(a[2] + (b[2] - a[2]) * ratio)
|
||||
|
||||
return '#%02x%02x%02x' % (r, g, b)
|
@ -1,39 +1,37 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import psycopg2, json, traceback
|
||||
from config import DB_PASS, DB_USER, DB_NAME, DB_HOST, saveDefaultNodeInfo, removableFileds
|
||||
from pk2addr import keyTo128BitAddress
|
||||
|
||||
#####
|
||||
|
||||
# Configuration to use TCP connection or unix domain socket for admin connection to yggdrasil
|
||||
DB_PASS = "password"
|
||||
DB_USER = "yggindex"
|
||||
DB_NAME = "yggindex"
|
||||
DB_HOST = "localhost"
|
||||
|
||||
## Save in database node info fields like buildname, buildarch, etc. (True/False)?
|
||||
saveDefaultNodeInfo = False
|
||||
removableFileds = ['buildname', 'buildarch', 'buildplatform', 'buildversion', 'board_name', 'kernel', 'model', 'system']
|
||||
|
||||
#####
|
||||
|
||||
with open('api/results.json', 'r') as f:
|
||||
with open('api/result.json', 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
timestamp = data['meta']['generated_at_utc']
|
||||
|
||||
# connect to database
|
||||
dbconn = psycopg2.connect(host=DB_HOST, database=DB_NAME, user=DB_USER, password=DB_PASS)
|
||||
cur = dbconn.cursor()
|
||||
|
||||
# start importing
|
||||
for node in data['topology']:
|
||||
for key, node in data['yggnodes'].items():
|
||||
nodename = ""
|
||||
nodeinfo = {}
|
||||
ipv6 = data['topology'][node]['ipv6_addr']
|
||||
coords = '[%s]' % (' '.join(str(e) for e in data['topology'][node]['coords']))
|
||||
|
||||
if node in data['nodeinfo']:
|
||||
nodeinfo = data['nodeinfo'][node]
|
||||
if "address" in node:
|
||||
ipv6 = keyTo128BitAddress(node['address']) if len(node['address']) == 64 else node['address']
|
||||
else:
|
||||
ipv6 = keyTo128BitAddress(key)
|
||||
|
||||
if "coords" in node:
|
||||
coords = node['coords']
|
||||
else:
|
||||
continue
|
||||
|
||||
timestamp = node['time']
|
||||
|
||||
if "nodeinfo" in node:
|
||||
nodeinfo = node['nodeinfo']
|
||||
|
||||
if not saveDefaultNodeInfo:
|
||||
# remove default Node info fields
|
||||
@ -42,10 +40,8 @@ for node in data['topology']:
|
||||
|
||||
if "name" in nodeinfo:
|
||||
nodename = nodeinfo['name']
|
||||
elif data['topology'][node]['found'] == False:
|
||||
nodename = '? %s' % coords
|
||||
else:
|
||||
nodename = ipv6
|
||||
nodename = '? %s' % coords
|
||||
|
||||
nodeinfo = json.dumps(nodeinfo)
|
||||
|
||||
@ -66,4 +62,3 @@ for node in data['topology']:
|
||||
dbconn.commit()
|
||||
cur.close()
|
||||
dbconn.close()
|
||||
|
||||
|
@ -1,21 +1,12 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
#max/min for the day with nodes
|
||||
# max/min for the day with nodes
|
||||
|
||||
import psycopg2
|
||||
import time
|
||||
from config import DB_PASS, DB_USER, DB_NAME, DB_HOST, ALIVE_SECONDS
|
||||
|
||||
#run every hour
|
||||
|
||||
DB_PASS = "password"
|
||||
DB_USER = "yggindex"
|
||||
DB_NAME = "yggindex"
|
||||
DB_HOST = "localhost"
|
||||
|
||||
# count peer alive if it was available not more that amount of seconds ago
|
||||
# I'm using 1 hour beause of running crawler every 15 minutes
|
||||
ALIVE_SECONDS = 3600 # 1 hour
|
||||
|
||||
# run every hour
|
||||
|
||||
def age_calc(ustamp):
|
||||
if (time.time() - ustamp) <= ALIVE_SECONDS :
|
||||
|
32
api/pk2addr.py
Normal file
32
api/pk2addr.py
Normal file
@ -0,0 +1,32 @@
|
||||
# Author: silentfamiliar@matrix
|
||||
|
||||
def keyTo128BitAddress(key):
|
||||
key260bits = int("1" + key, 16) # "1" to avoid trimming leading 0s
|
||||
source_cursor = 4 # skip the "1"
|
||||
|
||||
# loop over each bit while NOT(bit) is 1
|
||||
while (1 & ~(key260bits >> (260 - source_cursor - 1))) == 1:
|
||||
source_cursor = source_cursor + 1
|
||||
|
||||
ones_count = source_cursor - 4 # 1s to count minus 4 which was our initial offset
|
||||
source_cursor = source_cursor + 1 # skipping trailing 0
|
||||
|
||||
dest = (0x2 << 8) | ones_count # set header
|
||||
bitsToAdd = 128 - 16 # header was 2 bytes which is 16 bit
|
||||
|
||||
# append needed amount of NOT key starting from source_cursor
|
||||
dest = (dest << bitsToAdd) | ((2**bitsToAdd - 1) & ~(key260bits >> (260 - source_cursor - bitsToAdd)))
|
||||
|
||||
# the long addr
|
||||
dest_hex = "0" + hex(dest)[2:]
|
||||
# format ipv6 128bit addr
|
||||
|
||||
addr = ""
|
||||
for i in range(8):
|
||||
piece = int(dest_hex[i*4:i*4+4], 16)
|
||||
if (len(addr) != 0) and not (addr[len(addr)-2:len(addr)] == "::"):
|
||||
addr += ":"
|
||||
if (piece != 0):
|
||||
addr += hex(piece)[2:]
|
||||
|
||||
return addr
|
6
api/static/map/jquery-2.0.3.min.js
vendored
Normal file
6
api/static/map/jquery-2.0.3.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
29
api/static/map/jquery.autocomplete.min.js
vendored
Normal file
29
api/static/map/jquery.autocomplete.min.js
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Ajax Autocomplete for jQuery, version 1.2.9
|
||||
* (c) 2013 Tomas Kirda
|
||||
*
|
||||
* Ajax Autocomplete for jQuery is freely distributable under the terms of an MIT-style license.
|
||||
* For details, see the web site: https://github.com/devbridge/jQuery-Autocomplete
|
||||
*
|
||||
*/
|
||||
(function(d){"function"===typeof define&&define.amd?define(["jquery"],d):d(jQuery)})(function(d){function g(a,b){var c=function(){},c={autoSelectFirst:!1,appendTo:"body",serviceUrl:null,lookup:null,onSelect:null,width:"auto",minChars:1,maxHeight:300,deferRequestBy:0,params:{},formatResult:g.formatResult,delimiter:null,zIndex:9999,type:"GET",noCache:!1,onSearchStart:c,onSearchComplete:c,onSearchError:c,containerClass:"autocomplete-suggestions",tabDisabled:!1,dataType:"text",currentRequest:null,triggerSelectOnValidInput:!0,
|
||||
lookupFilter:function(a,b,c){return-1!==a.value.toLowerCase().indexOf(c)},paramName:"query",transformResult:function(a){return"string"===typeof a?d.parseJSON(a):a}};this.element=a;this.el=d(a);this.suggestions=[];this.badQueries=[];this.selectedIndex=-1;this.currentValue=this.element.value;this.intervalId=0;this.cachedResponse={};this.onChange=this.onChangeInterval=null;this.isLocal=!1;this.suggestionsContainer=null;this.options=d.extend({},c,b);this.classes={selected:"autocomplete-selected",suggestion:"autocomplete-suggestion"};
|
||||
this.hint=null;this.hintValue="";this.selection=null;this.initialize();this.setOptions(b)}var k=function(){return{escapeRegExChars:function(a){return a.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")},createNode:function(a){var b=document.createElement("div");b.className=a;b.style.position="absolute";b.style.display="none";return b}}}();g.utils=k;d.Autocomplete=g;g.formatResult=function(a,b){var c="("+k.escapeRegExChars(b)+")";return a.value.replace(RegExp(c,"gi"),"<strong>$1</strong>")};g.prototype=
|
||||
{killerFn:null,initialize:function(){var a=this,b="."+a.classes.suggestion,c=a.classes.selected,e=a.options,f;a.element.setAttribute("autocomplete","off");a.killerFn=function(b){0===d(b.target).closest("."+a.options.containerClass).length&&(a.killSuggestions(),a.disableKillerFn())};a.suggestionsContainer=g.utils.createNode(e.containerClass);f=d(a.suggestionsContainer);f.appendTo(e.appendTo);"auto"!==e.width&&f.width(e.width);f.on("mouseover.autocomplete",b,function(){a.activate(d(this).data("index"))});
|
||||
f.on("mouseout.autocomplete",function(){a.selectedIndex=-1;f.children("."+c).removeClass(c)});f.on("click.autocomplete",b,function(){a.select(d(this).data("index"))});a.fixPosition();a.fixPositionCapture=function(){a.visible&&a.fixPosition()};d(window).on("resize.autocomplete",a.fixPositionCapture);a.el.on("keydown.autocomplete",function(b){a.onKeyPress(b)});a.el.on("keyup.autocomplete",function(b){a.onKeyUp(b)});a.el.on("blur.autocomplete",function(){a.onBlur()});a.el.on("focus.autocomplete",function(){a.onFocus()});
|
||||
a.el.on("change.autocomplete",function(b){a.onKeyUp(b)})},onFocus:function(){this.fixPosition();if(this.options.minChars<=this.el.val().length)this.onValueChange()},onBlur:function(){this.enableKillerFn()},setOptions:function(a){var b=this.options;d.extend(b,a);if(this.isLocal=d.isArray(b.lookup))b.lookup=this.verifySuggestionsFormat(b.lookup);d(this.suggestionsContainer).css({"max-height":b.maxHeight+"px",width:b.width+"px","z-index":b.zIndex})},clearCache:function(){this.cachedResponse={};this.badQueries=
|
||||
[]},clear:function(){this.clearCache();this.currentValue="";this.suggestions=[]},disable:function(){this.disabled=!0;this.currentRequest&&this.currentRequest.abort()},enable:function(){this.disabled=!1},fixPosition:function(){var a;"body"===this.options.appendTo&&(a=this.el.offset(),a={top:a.top+this.el.outerHeight()+"px",left:a.left+"px"},"auto"===this.options.width&&(a.width=this.el.outerWidth()-2+"px"),d(this.suggestionsContainer).css(a))},enableKillerFn:function(){d(document).on("click.autocomplete",
|
||||
this.killerFn)},disableKillerFn:function(){d(document).off("click.autocomplete",this.killerFn)},killSuggestions:function(){var a=this;a.stopKillSuggestions();a.intervalId=window.setInterval(function(){a.hide();a.stopKillSuggestions()},50)},stopKillSuggestions:function(){window.clearInterval(this.intervalId)},isCursorAtEnd:function(){var a=this.el.val().length,b=this.element.selectionStart;return"number"===typeof b?b===a:document.selection?(b=document.selection.createRange(),b.moveStart("character",
|
||||
-a),a===b.text.length):!0},onKeyPress:function(a){if(!this.disabled&&!this.visible&&40===a.which&&this.currentValue)this.suggest();else if(!this.disabled&&this.visible){switch(a.which){case 27:this.el.val(this.currentValue);this.hide();break;case 39:if(this.hint&&this.options.onHint&&this.isCursorAtEnd()){this.selectHint();break}return;case 9:if(this.hint&&this.options.onHint){this.selectHint();return}case 13:if(-1===this.selectedIndex){this.hide();return}this.select(this.selectedIndex);if(9===a.which&&
|
||||
!1===this.options.tabDisabled)return;break;case 38:this.moveUp();break;case 40:this.moveDown();break;default:return}a.stopImmediatePropagation();a.preventDefault()}},onKeyUp:function(a){var b=this;if(!b.disabled){switch(a.which){case 38:case 40:return}clearInterval(b.onChangeInterval);if(b.currentValue!==b.el.val())if(b.findBestHint(),0<b.options.deferRequestBy)b.onChangeInterval=setInterval(function(){b.onValueChange()},b.options.deferRequestBy);else b.onValueChange()}},onValueChange:function(){var a=
|
||||
this.options,b=this.el.val(),c=this.getQuery(b);this.selection&&(this.selection=null,(a.onInvalidateSelection||d.noop).call(this.element));clearInterval(this.onChangeInterval);this.currentValue=b;this.selectedIndex=-1;if(a.triggerSelectOnValidInput&&(b=this.findSuggestionIndex(c),-1!==b)){this.select(b);return}c.length<a.minChars?this.hide():this.getSuggestions(c)},findSuggestionIndex:function(a){var b=-1,c=a.toLowerCase();d.each(this.suggestions,function(a,d){if(d.value.toLowerCase()===c)return b=
|
||||
a,!1});return b},getQuery:function(a){var b=this.options.delimiter;if(!b)return a;a=a.split(b);return d.trim(a[a.length-1])},getSuggestionsLocal:function(a){var b=this.options,c=a.toLowerCase(),e=b.lookupFilter,f=parseInt(b.lookupLimit,10),b={suggestions:d.grep(b.lookup,function(b){return e(b,a,c)})};f&&b.suggestions.length>f&&(b.suggestions=b.suggestions.slice(0,f));return b},getSuggestions:function(a){var b,c=this,e=c.options,f=e.serviceUrl,l,g;e.params[e.paramName]=a;l=e.ignoreParams?null:e.params;
|
||||
c.isLocal?b=c.getSuggestionsLocal(a):(d.isFunction(f)&&(f=f.call(c.element,a)),g=f+"?"+d.param(l||{}),b=c.cachedResponse[g]);b&&d.isArray(b.suggestions)?(c.suggestions=b.suggestions,c.suggest()):c.isBadQuery(a)||!1===e.onSearchStart.call(c.element,e.params)||(c.currentRequest&&c.currentRequest.abort(),c.currentRequest=d.ajax({url:f,data:l,type:e.type,dataType:e.dataType}).done(function(b){c.currentRequest=null;c.processResponse(b,a,g);e.onSearchComplete.call(c.element,a)}).fail(function(b,d,f){e.onSearchError.call(c.element,
|
||||
a,b,d,f)}))},isBadQuery:function(a){for(var b=this.badQueries,c=b.length;c--;)if(0===a.indexOf(b[c]))return!0;return!1},hide:function(){this.visible=!1;this.selectedIndex=-1;d(this.suggestionsContainer).hide();this.signalHint(null)},suggest:function(){if(0===this.suggestions.length)this.hide();else{var a=this.options,b=a.formatResult,c=this.getQuery(this.currentValue),e=this.classes.suggestion,f=this.classes.selected,g=d(this.suggestionsContainer),k=a.beforeRender,m="",h;if(a.triggerSelectOnValidInput&&
|
||||
(h=this.findSuggestionIndex(c),-1!==h)){this.select(h);return}d.each(this.suggestions,function(a,d){m+='<div class="'+e+'" data-index="'+a+'">'+b(d,c)+"</div>"});"auto"===a.width&&(h=this.el.outerWidth()-2,g.width(0<h?h:300));g.html(m);a.autoSelectFirst&&(this.selectedIndex=0,g.children().first().addClass(f));d.isFunction(k)&&k.call(this.element,g);g.show();this.visible=!0;this.findBestHint()}},findBestHint:function(){var a=this.el.val().toLowerCase(),b=null;a&&(d.each(this.suggestions,function(c,
|
||||
d){var f=0===d.value.toLowerCase().indexOf(a);f&&(b=d);return!f}),this.signalHint(b))},signalHint:function(a){var b="";a&&(b=this.currentValue+a.value.substr(this.currentValue.length));this.hintValue!==b&&(this.hintValue=b,this.hint=a,(this.options.onHint||d.noop)(b))},verifySuggestionsFormat:function(a){return a.length&&"string"===typeof a[0]?d.map(a,function(a){return{value:a,data:null}}):a},processResponse:function(a,b,c){var d=this.options;a=d.transformResult(a,b);a.suggestions=this.verifySuggestionsFormat(a.suggestions);
|
||||
d.noCache||(this.cachedResponse[c]=a,0===a.suggestions.length&&this.badQueries.push(c));b===this.getQuery(this.currentValue)&&(this.suggestions=a.suggestions,this.suggest())},activate:function(a){var b=this.classes.selected,c=d(this.suggestionsContainer),e=c.children();c.children("."+b).removeClass(b);this.selectedIndex=a;return-1!==this.selectedIndex&&e.length>this.selectedIndex?(a=e.get(this.selectedIndex),d(a).addClass(b),a):null},selectHint:function(){var a=d.inArray(this.hint,this.suggestions);
|
||||
this.select(a)},select:function(a){this.hide();this.onSelect(a)},moveUp:function(){-1!==this.selectedIndex&&(0===this.selectedIndex?(d(this.suggestionsContainer).children().first().removeClass(this.classes.selected),this.selectedIndex=-1,this.el.val(this.currentValue),this.findBestHint()):this.adjustScroll(this.selectedIndex-1))},moveDown:function(){this.selectedIndex!==this.suggestions.length-1&&this.adjustScroll(this.selectedIndex+1)},adjustScroll:function(a){var b=this.activate(a),c,e;b&&(b=b.offsetTop,
|
||||
c=d(this.suggestionsContainer).scrollTop(),e=c+this.options.maxHeight-25,b<c?d(this.suggestionsContainer).scrollTop(b):b>e&&d(this.suggestionsContainer).scrollTop(b-this.options.maxHeight+25),this.el.val(this.getValue(this.suggestions[a].value)),this.signalHint(null))},onSelect:function(a){var b=this.options.onSelect;a=this.suggestions[a];this.currentValue=this.getValue(a.value);this.el.val(this.currentValue);this.signalHint(null);this.suggestions=[];this.selection=a;d.isFunction(b)&&b.call(this.element,
|
||||
a)},getValue:function(a){var b=this.options.delimiter,c;if(!b)return a;c=this.currentValue;b=c.split(b);return 1===b.length?a:c.substr(0,c.length-b[b.length-1].length)+a},dispose:function(){this.el.off(".autocomplete").removeData("autocomplete");this.disableKillerFn();d(window).off("resize.autocomplete",this.fixPositionCapture);d(this.suggestionsContainer).remove()}};d.fn.autocomplete=function(a,b){return 0===arguments.length?this.first().data("autocomplete"):this.each(function(){var c=d(this),e=
|
||||
c.data("autocomplete");if("string"===typeof a){if(e&&"function"===typeof e[a])e[a](b)}else e&&e.dispose&&e.dispose(),e=new g(this,a),c.data("autocomplete",e)})}});
|
426
api/static/map/network.js
Normal file
426
api/static/map/network.js
Normal file
@ -0,0 +1,426 @@
|
||||
"use strict";
|
||||
|
||||
var nodes = [];
|
||||
var edges = [];
|
||||
var canvas = null;
|
||||
var ctx = null;
|
||||
var mapOffset = {x: 0, y: 0};
|
||||
var zoom = 1.0;
|
||||
|
||||
function changeHash(hash) {
|
||||
window.location.replace(('' + window.location).split('#')[0] + '#' + hash);
|
||||
}
|
||||
|
||||
function updateCanvasSize() {
|
||||
$(canvas).attr({height: $(canvas).height(), width: $(canvas).width()});
|
||||
ctx.translate(mapOffset.x, mapOffset.y);
|
||||
}
|
||||
|
||||
function drawCircle(ctx, x, y, radius, color) {
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, radius, 0, Math.PI*2, true);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
function drawLine(ctx, x1, y1, x2, y2, color) {
|
||||
ctx.strokeStyle = color;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, y1);
|
||||
ctx.lineTo(x2, y2);
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawText(ctx, x, y, text, color, font) {
|
||||
// ctx.save();
|
||||
// ctx.translate(x, y);
|
||||
// ctx.rotate(Math.PI/4);
|
||||
ctx.fillStyle = color;
|
||||
ctx.font = font;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(text, x, y);
|
||||
// ctx.restore();
|
||||
}
|
||||
|
||||
function drawNetwork() {
|
||||
ctx.save();
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.restore();
|
||||
|
||||
|
||||
// Draw edges
|
||||
for (var i = 0; i < edges.length; ++i) {
|
||||
var edge = edges[i];
|
||||
var highlight = edge.sourceNode.hover || edge.targetNode.hover;
|
||||
var color = highlight ? 'rgba(0, 0, 0, 0.5)' : 'rgba(0, 0, 0, 0.15)';
|
||||
|
||||
drawLine(ctx,
|
||||
edge.sourceNode.x, edge.sourceNode.y,
|
||||
edge.targetNode.x, edge.targetNode.y,
|
||||
color);
|
||||
}
|
||||
|
||||
// Draw nodes
|
||||
for (var i = 0; i < nodes.length; ++i) {
|
||||
var node = nodes[i];
|
||||
|
||||
drawCircle(ctx, node.x, node.y, node.radius, node.color);
|
||||
}
|
||||
|
||||
// Draw labels
|
||||
for (var i = 0; i < nodes.length; ++i) {
|
||||
var node = nodes[i];
|
||||
|
||||
if (node.radius > 4 || node.selected || node.hover) {
|
||||
var fontSize = 4 + node.radius * 0.4;
|
||||
|
||||
drawText(ctx, node.x, node.y - node.radius - 1,
|
||||
node.label, node.textColor, fontSize + 'pt "ubuntu mono"');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getNodeAt(x, y) {
|
||||
x -= mapOffset.x;
|
||||
y -= mapOffset.y;
|
||||
for (var i = nodes.length - 1; i >= 0; --i) {
|
||||
var node = nodes[i];
|
||||
var distPow2 = (node.x - x) * (node.x - x) + (node.y - y) * (node.y - y);
|
||||
|
||||
if (distPow2 <= node.radius * node.radius) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function searchNode(id) {
|
||||
for (var i = 0; i < nodes.length; ++i) {
|
||||
if (nodes[i].id == id)
|
||||
return nodes[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function clearNodes() {
|
||||
changeHash('');
|
||||
$('#node-info').html('');
|
||||
|
||||
for (var i = 0; i < nodes.length; ++i) {
|
||||
var node = nodes[i];
|
||||
node.depth = 0xFFFF;
|
||||
node.color = node.originalColor;
|
||||
node.textColor = node.color;
|
||||
node.selected = false;
|
||||
}
|
||||
}
|
||||
|
||||
function selectNode(node, redraw) {
|
||||
clearNodes();
|
||||
|
||||
changeHash(node.id);
|
||||
|
||||
node.selected = true;
|
||||
showNodeInfo(node);
|
||||
|
||||
markPeers(node, 0);
|
||||
if (redraw)
|
||||
drawNetwork();
|
||||
}
|
||||
|
||||
function markPeers(node, depth) {
|
||||
node.depth = depth;
|
||||
|
||||
// var colors = ['#000000', '#333333', '#555555', '#777777', '#999999', '#BBBBBB', '#DDDDDD'];
|
||||
// var colors = ['#000000', '#29BBFF', '#09E844', '#FFBD0F', '#FF5E14', '#FF3C14', '#FF7357', '#FF9782', '#FFC8BD', '#FFE6E0'];
|
||||
var colors = ['#000000', '#096EE8', '#09E8B8', '#36E809', '#ADE809', '#E8B809', '#E87509', '#E83A09', '#E86946', '#E8AC9B', '#E8C9C1'];
|
||||
var txtCol = ['#000000', '#032247', '#034537', '#0E3D02', '#354703', '#403203', '#3D1F02', '#3B0E02', '#3B0E02', '#3B0E02', '#3B0E02'];
|
||||
// var colors = ['#000000', '#064F8F', '#068F81', '#068F38', '#218F06', '#6F8F06', '#8F7806', '#8F5106'];
|
||||
// var colors = ['#FFFFFF', '#29BBFF', '#17FF54', '#FFBD0F', '#FF3C14', '#590409'];
|
||||
node.color = (depth >= colors.length) ? '#FFFFFF' : colors[depth];
|
||||
node.textColor = (depth >= txtCol.length) ? '#FFFFFF' : txtCol[depth];
|
||||
|
||||
for (var i = 0; i < node.peers.length; ++i) {
|
||||
var n = node.peers[i];
|
||||
if (n.depth > depth + 1)
|
||||
markPeers(n, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
function showNodeInfo(node) {
|
||||
var ip_peers = [];
|
||||
var dns_peers = [];
|
||||
|
||||
for (var i = 0; i < node.peers.length; ++i) {
|
||||
var n = node.peers[i];
|
||||
if (/^[0-9A-F]{4}$/i.test(n.label))
|
||||
ip_peers.push(n);
|
||||
else
|
||||
dns_peers.push(n);
|
||||
}
|
||||
|
||||
var label_compare = function(a, b) {
|
||||
return a.label.localeCompare(b.label);
|
||||
}
|
||||
|
||||
dns_peers.sort(label_compare);
|
||||
ip_peers.sort(label_compare);
|
||||
|
||||
var peers = dns_peers.concat(ip_peers);
|
||||
|
||||
var html =
|
||||
'<h2>' + node.label + '</h2>' +
|
||||
'<span class="tt">' + node.id + '</span><br>' +
|
||||
'<br>' +
|
||||
'<strong>Coords:</strong> ' + node.coords + '<br>' +
|
||||
'<strong>Peers:</strong> ' + node.peers.length + '<br>' +
|
||||
'<strong>Centrality:</strong> ' + node.centrality + '<br>' +
|
||||
'<table>' +
|
||||
// '<tr><td></td><td><strong>Their peers #</strong></td></tr>' +
|
||||
peers.map(function (n) {
|
||||
return '<tr>' +
|
||||
'<td><a href="#' + n.id + '" class="tt">' + n.label + '</a></td>' +
|
||||
'<td>' + n.peers.length + '</td></tr>';
|
||||
}).join('') +
|
||||
'</table>';
|
||||
|
||||
$('#node-info').html(html);
|
||||
}
|
||||
|
||||
function mousePos(e) {
|
||||
var rect = canvas.getBoundingClientRect();
|
||||
return {x: e.clientX - rect.left, y: e.clientY - rect.top};
|
||||
}
|
||||
|
||||
|
||||
|
||||
$(document).ready(function() {
|
||||
canvas = document.getElementById('map');
|
||||
ctx = canvas.getContext('2d');
|
||||
updateCanvasSize();
|
||||
|
||||
|
||||
jQuery.getJSON('/static/graph.json', function(data) {
|
||||
nodes = data.nodes;
|
||||
edges = data.edges;
|
||||
|
||||
// Calculate node radiuses
|
||||
for (var i = 0; i < nodes.length; ++i) {
|
||||
var node = nodes[i];
|
||||
node.x = node.x * 1.2;
|
||||
node.y = node.y * 1.2;
|
||||
node.radius = node.size;
|
||||
node.hover = false;
|
||||
node.selected = false;
|
||||
node.edges = [];
|
||||
node.peers = [];
|
||||
node.depth = 0xFFFF;
|
||||
// node.color = '#000';
|
||||
node.originalColor = node.color;
|
||||
node.textColor = node.color;
|
||||
}
|
||||
|
||||
var newEdges = []
|
||||
// Find node references for edges
|
||||
for (var i = 0; i < edges.length; ++i) {
|
||||
var edge = edges[i];
|
||||
|
||||
for (var n = 0; n < nodes.length; ++n) {
|
||||
if (nodes[n].id == edge.sourceID) {
|
||||
edge.sourceNode = nodes[n];
|
||||
// edge.sourceNode.edges.append(edge);
|
||||
}
|
||||
else if (nodes[n].id == edge.targetID)
|
||||
edge.targetNode = nodes[n];
|
||||
}
|
||||
|
||||
if (!edge.sourceNode || !edge.targetNode)
|
||||
continue;
|
||||
|
||||
edge.sourceNode.edges.push(edge);
|
||||
edge.targetNode.edges.push(edge);
|
||||
edge.sourceNode.peers.push(edge.targetNode);
|
||||
edge.targetNode.peers.push(edge.sourceNode);
|
||||
|
||||
newEdges.push(edge);
|
||||
}
|
||||
edges = newEdges;
|
||||
|
||||
// Set update time
|
||||
var delta = Math.round(new Date().getTime() / 1000) - data.created;
|
||||
var min = Math.floor(delta / 60);
|
||||
var sec = delta % 60;
|
||||
$('#update-time').text(min + ' min, ' + sec + ' s ago');
|
||||
|
||||
// Set stats
|
||||
$('#number-of-nodes').text(nodes.length);
|
||||
$('#number-of-connections').text(edges.length);
|
||||
|
||||
|
||||
if (window.location.hash) {
|
||||
var id = window.location.hash.substring(1);
|
||||
var node = searchNode(id);
|
||||
if (node) selectNode(node, false);
|
||||
}
|
||||
|
||||
drawNetwork();
|
||||
|
||||
$(window).resize(function() {
|
||||
updateCanvasSize();
|
||||
drawNetwork();
|
||||
});
|
||||
|
||||
|
||||
// Initialize search
|
||||
var searchArray = [];
|
||||
for (var i = 0; i < nodes.length; ++i) {
|
||||
var node = nodes[i];
|
||||
|
||||
searchArray.push({
|
||||
value: node.label,
|
||||
data: node
|
||||
});
|
||||
|
||||
searchArray.push({
|
||||
value: node.id,
|
||||
data: node
|
||||
});
|
||||
}
|
||||
|
||||
$('#search-box').autocomplete({
|
||||
lookup: searchArray,
|
||||
autoSelectFirst: true,
|
||||
lookupLimit: 7,
|
||||
onSelect: function(suggestion) {
|
||||
selectNode(suggestion.data, true);
|
||||
}
|
||||
});
|
||||
|
||||
$('#search-box').keypress(function(e) {
|
||||
if (e.which == 13) {
|
||||
selectNode(searchNode($('#search-box').val()), true);
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('click', '#node-info a', function(e) {
|
||||
var id = e.target.hash.substring(1);
|
||||
selectNode(searchNode(id), true);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
var mouseDownPos = null;
|
||||
var mouseLastPos = null;
|
||||
var mouseDownNode = null;
|
||||
var mouseHoverNode = null;
|
||||
|
||||
|
||||
$(canvas).mousemove(function(e) {
|
||||
var mouse = mousePos(e);
|
||||
|
||||
// Dragging
|
||||
if (mouseDownPos != null) {
|
||||
$('body').css('cursor', 'move');
|
||||
var dx = mouse.x - mouseLastPos.x;
|
||||
var dy = mouse.y - mouseLastPos.y;
|
||||
mapOffset.x += dx;
|
||||
mapOffset.y += dy;
|
||||
ctx.translate(dx, dy);
|
||||
mouseLastPos = {x: mouse.x, y: mouse.y};
|
||||
drawNetwork();
|
||||
}
|
||||
// Hovering
|
||||
else {
|
||||
var node = getNodeAt(mouse.x, mouse.y);
|
||||
|
||||
if (node == mouseHoverNode)
|
||||
return;
|
||||
|
||||
if (node == null) {
|
||||
nodeMouseOut(mouseHoverNode);
|
||||
}
|
||||
else {
|
||||
if (mouseHoverNode != null)
|
||||
nodeMouseOut(mouseHoverNode);
|
||||
|
||||
nodeMouseIn(node);
|
||||
}
|
||||
mouseHoverNode = node;
|
||||
|
||||
drawNetwork();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
$(canvas).mousedown(function(e) {
|
||||
var mouse = mousePos(e);
|
||||
mouseLastPos = mouseDownPos = {x: mouse.x, y: mouse.y};
|
||||
mouseDownNode = getNodeAt(mouse.x, mouse.y);
|
||||
return false;
|
||||
});
|
||||
|
||||
$(canvas).mouseup(function(e) {
|
||||
var mouse = mousePos(e);
|
||||
var mouseMoved =
|
||||
Math.abs(mouse.x - mouseDownPos.x) +
|
||||
Math.abs(mouse.y - mouseDownPos.y) > 3
|
||||
|
||||
if (!mouseMoved) {
|
||||
if (mouseDownNode)
|
||||
selectNode(mouseDownNode, true);
|
||||
else {
|
||||
clearNodes();
|
||||
drawNetwork();
|
||||
}
|
||||
}
|
||||
else {
|
||||
$('body').css('cursor', 'auto');
|
||||
}
|
||||
|
||||
mouseDownPos = null;
|
||||
mouseDownNode = null;
|
||||
return false;
|
||||
});
|
||||
|
||||
|
||||
function handleScroll(e) {
|
||||
var mouse = mousePos(e);
|
||||
var delta = Math.max(-1, Math.min(1, (e.wheelDelta || -e.detail)));
|
||||
|
||||
var ratio = (delta < 0) ? (3 / 4) : 1 + (1 / 3);
|
||||
var mx = mouse.x - mapOffset.x;
|
||||
var my = mouse.y - mapOffset.y;
|
||||
|
||||
zoom *= ratio;
|
||||
|
||||
for (var i = 0; i < nodes.length; ++i) {
|
||||
var node = nodes[i];
|
||||
node.x = (node.x - mx) * ratio + mx;
|
||||
node.y = (node.y - my) * ratio + my;
|
||||
// node.x *= ratio;
|
||||
// node.y *= ratio;
|
||||
// node.radius *= ratio;
|
||||
node.radius = (node.size) * zoom;
|
||||
}
|
||||
|
||||
drawNetwork();
|
||||
}
|
||||
canvas.addEventListener("mousewheel", handleScroll, false);
|
||||
canvas.addEventListener("DOMMouseScroll", handleScroll, false);
|
||||
|
||||
});
|
||||
|
||||
function nodeMouseIn(node) {
|
||||
node.hover = true;
|
||||
$('body').css('cursor', 'pointer');
|
||||
}
|
||||
|
||||
function nodeMouseOut(node) {
|
||||
node.hover = false;
|
||||
$('body').css('cursor', 'auto');
|
||||
}
|
202
api/static/map/style.css
Normal file
202
api/static/map/style.css
Normal file
@ -0,0 +1,202 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
html, body {
|
||||
background: #F5F5F5;
|
||||
font-family: 'sans serif';
|
||||
}
|
||||
|
||||
#header {
|
||||
background: #FFF;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: 'Inconsolata', 'Consolas', 'Ubuntu Mono', monospace;
|
||||
font-size: 32px;
|
||||
float: left;
|
||||
padding: 0 40px;
|
||||
font-weight: 100;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.grey {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
height: 100%;
|
||||
}
|
||||
li {
|
||||
float: left;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#header a {
|
||||
color: #777;
|
||||
padding: 0 20px;
|
||||
font-family: 'Open Sans', 'sans-serif';
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
height: 100%;
|
||||
display: block;
|
||||
|
||||
}
|
||||
|
||||
#header a.selected {
|
||||
background: #DDD;
|
||||
}
|
||||
|
||||
#header a:hover {
|
||||
background: #EEE;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#general-info {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
font-size: 10px;
|
||||
padding: 5px;
|
||||
line-height: 150%;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
|
||||
#sidebar {
|
||||
padding: 20px 20px;
|
||||
background: rgba(220, 220, 220, 0.8);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
/*bottom: 0;*/
|
||||
max-height: calc(100vh - 40px);
|
||||
min-width: 250px;
|
||||
z-index: 999;
|
||||
overflow-y: scroll;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#search-wrapper {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
/*position: absolute;*/
|
||||
/*top: 0;*/
|
||||
/*right: 10px;*/
|
||||
/*z-index: 5;*/
|
||||
font-size: 10px;
|
||||
}
|
||||
#search-box {
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
outline: none;
|
||||
border: none;
|
||||
/*border: 1px solid #CCC;*/
|
||||
margin: -5px;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
#sidebar a {
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
}
|
||||
#sidebar a:hover {
|
||||
color: #AAA;
|
||||
}
|
||||
|
||||
#sidebar h2 {
|
||||
text-align: center;
|
||||
margin-bottom: 5px;
|
||||
color: #29BBFF;
|
||||
}
|
||||
|
||||
#node-info table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#node-info td + td {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#node-info strong {
|
||||
color: #29BBFF;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.tt {
|
||||
font-family: monospace;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.autocomplete-suggestions {
|
||||
font-family: monospace;
|
||||
font-size: 10px;
|
||||
border: 1px solid #FFF;
|
||||
background: #FFF;
|
||||
overflow: auto;
|
||||
color: #555;
|
||||
}
|
||||
.autocomplete-suggestion {
|
||||
padding: 2px 5px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
.autocomplete-selected { background: #7FD6FF; }
|
||||
.autocomplete-suggestions strong {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
#content-wrapper {
|
||||
position: absolute;
|
||||
top: 48px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#map {
|
||||
position: absolute;
|
||||
width:100%;
|
||||
height:100%;
|
||||
}
|
||||
|
||||
#content {
|
||||
width: 500px;
|
||||
margin: 30px auto;
|
||||
font-family: sans-serif;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
line-height: 28px;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
#content h2 {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
color: #29BBFF;
|
||||
}
|
||||
|
||||
#content h3 {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
color: #29BBFF;
|
||||
}
|
||||
|
||||
#content a {
|
||||
color: #29BBFF;
|
||||
}
|
@ -14,29 +14,31 @@
|
||||
<strong>Current Nodes online<br />
|
||||
<font size="18">{{ nodes }}</font></strong>
|
||||
<br /><br />
|
||||
You can see them in <i>Yggdrasil Interactive World map</i> <sup><a href="http://[21f:dd73:7cdb:773b:a924:7ec0:800b:221e]/">[⇗]</a> <a href="http://map.ygg/" target="_blank">[DNS]</a></sup>
|
||||
You can see them in <a href="/map"><i>Yggdrasil Interactive World map</i></a>
|
||||
<br />
|
||||
Arceliar's map can be found here: <sup><a href="http://[21e:e795:8e82:a9e2:ff48:952d:55f2:f0bb]/" target="_blank">[⇗]</a> <a href="http://map.ygg/" target="_blank">[DNS]</a></sup>
|
||||
</center>
|
||||
<br /><br />
|
||||
|
||||
<div class="wide"></div>
|
||||
<strong>Make an API request</strong><br />
|
||||
<small>note: data updated every 15 minutes, all requests are returned in JSON.</small><br /><br />
|
||||
<small>note: data is updated hourly (due to network growth and instabillity), all requests are returned in JSON.</small><br /><br />
|
||||
|
||||
Get a current list of active and online nodes:<br />
|
||||
<div class="apireq">
|
||||
http://[31a:fb8a:c43e:ca59::2]/current <sup><a href="http://[31a:fb8a:c43e:ca59::2]/current" target="_blank">[⇗]</a> <a href="http://nodelist.ygg/current" target="_blank">[DNS]</a></sup>
|
||||
http://[316:c51a:62a3:8b9::2]/current <sup><a href="http://[316:c51a:62a3:8b9::2]/current" target="_blank">[⇗]</a> <a href="http://nodelist.ygg/current" target="_blank">[DNS]</a></sup>
|
||||
</div>
|
||||
Nodeinfo from all current active nodes:<br />
|
||||
<div class="apireq">
|
||||
http://[31a:fb8a:c43e:ca59::2]/nodeinfo <sup><a href="http://[31a:fb8a:c43e:ca59::2]/nodeinfo" target="_blank">[⇗]</a> <a href="http://nodelist.ygg/nodeinfo" target="_blank">[DNS]</a></sup>
|
||||
http://[316:c51a:62a3:8b9::2]/nodeinfo <sup><a href="http://[316:c51a:62a3:8b9::2]/nodeinfo" target="_blank">[⇗]</a> <a href="http://nodelist.ygg/nodeinfo" target="_blank">[DNS]</a></sup>
|
||||
</div>
|
||||
Active nodes count for last 24 hours:<br />
|
||||
<div class="apireq">
|
||||
http://[31a:fb8a:c43e:ca59::2]/nodes24h <sup><a href="http://[31a:fb8a:c43e:ca59::2]/nodes24h" target="_blank">[⇗]</a> <a href="http://nodelist.ygg/nodes24h" target="_blank">[DNS]</a></sup>
|
||||
http://[316:c51a:62a3:8b9::2]/nodes24h <sup><a href="http://[316:c51a:62a3:8b9::2]/nodes24h" target="_blank">[⇗]</a> <a href="http://nodelist.ygg/nodes24h" target="_blank">[DNS]</a></sup>
|
||||
</div>
|
||||
Active nodes count for last 30 days:<br />
|
||||
<div class="apireq">
|
||||
http://[31a:fb8a:c43e:ca59::2]/nodes30d <sup><a href="http://[31a:fb8a:c43e:ca59::2]/nodes30d" target="_blank">[⇗]</a> <a href="http://nodelist.ygg/nodes30d" target="_blank">[DNS]</a></sup>
|
||||
http://[316:c51a:62a3:8b9::2]/nodes30d <sup><a href="http://[316:c51a:62a3:8b9::2]/nodes30d" target="_blank">[⇗]</a> <a href="http://nodelist.ygg/nodes30d" target="_blank">[DNS]</a></sup>
|
||||
</div>
|
||||
|
||||
<div class="wide"></div>
|
||||
@ -47,7 +49,7 @@
|
||||
</div>
|
||||
|
||||
<div class="wide"></div>
|
||||
<small>Made with <a href="https://github.com/r4sas/Niflheim-api" target="_blank">fork</a> of <a href="https://github.com/yakamok/Niflheim-api" target="_blank">Niflheim-API</a> by yakamok</small>
|
||||
<small>Made with <a href="https://github.com/r4sas/yggdrasil-monitor" target="_blank">fork</a> of <a href="https://github.com/yakamok/Niflheim-api" target="_blank">Niflheim-API</a> by yakamok</small>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
17
api/templates/map/about.html
Normal file
17
api/templates/map/about.html
Normal file
@ -0,0 +1,17 @@
|
||||
{% extends "map/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div id="content-wrapper">
|
||||
<div id="content">
|
||||
<h2>About</h2>
|
||||
<p>This is a project that aims to demystify what the <a href="https://yggdrasil-network.github.io/">Yggdrasil</a> network is like. Currently the only thing we have here is a map of the spanning tree subset of the network. The full source code is at <a href="https://github.com/Arceliar/yggdrasil-map">GitHub</a>.</p>
|
||||
|
||||
<h3>Network map</h3>
|
||||
<p>The network page has a map of Yggdrasil's spanning tree as it is now. The map is not complete since it is hard/impossible to get a full picture of the network, and it only includes the minimum subset of links needed to construct the spanning tree. The known nodes and tree coordinates are taken from <a href="http://y.yakamo.org:3000/">Yakamo's API</a>. Node names can be configured by setting a "name" field in <a href="https://yggdrasil-network.github.io/configuration.html">NodeInfo</a>, or from <a href="https://github.com/yakamok/yggdrasil-nodelist">Yakamo's node list</a> as a fallback.</p>
|
||||
|
||||
<h3>Contact</h3>
|
||||
<p>This project was forked from <em>zielmicha</em>'s fork of <em>Randati</em>'s fc00.
|
||||
The yggdrasil developers can be contacted over matrix or IRC, for more info see: <a href="https://yggdrasil-network.github.io/">yggdrasil-network.github.io</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
25
api/templates/map/base.html
Normal file
25
api/templates/map/base.html
Normal file
@ -0,0 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>0200::/7 – Mapping The Yggdrasil Network</title>
|
||||
<script src="{{ url_for('static', filename='map/jquery-2.0.3.min.js')}}"></script>
|
||||
<script src="{{ url_for('static', filename='map/jquery.autocomplete.min.js')}}"></script>
|
||||
<link href="{{ url_for('static', filename='map/style.css')}}" rel="stylesheet" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
<h1>0200<span class="grey">::/7</span></h1>
|
||||
|
||||
<ul>
|
||||
<li><a href="/">Status</a></li>
|
||||
<li><a href="/map" {% if page == 'network' %} class="selected" {% endif %}>Map</a></li>
|
||||
<li><a href="/map/about"{% if page == 'about' %} class="selected" {% endif %}>About</a></li>
|
||||
<li><a href="https://github.com/r4sas/yggdrasil-monitor">Source</a></li>
|
||||
<li><tt>{% if ip is not none %}{{ ip }}{% endif %}</tt></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
22
api/templates/map/network.html
Normal file
22
api/templates/map/network.html
Normal file
@ -0,0 +1,22 @@
|
||||
{% extends "map/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div id="general-info">
|
||||
Nodes: <span id="number-of-nodes">-</span><br>
|
||||
Links: <span id="number-of-connections">-</span><br>
|
||||
Updated <span id="update-time">-</span><br>
|
||||
</div>
|
||||
|
||||
<div id="sidebar">
|
||||
<div id="search-wrapper">
|
||||
<input id="search-box" class="tt" type="text" placeholder="Search nodes...">
|
||||
</div>
|
||||
<div id="node-info"></div>
|
||||
</div>
|
||||
|
||||
<div id="content-wrapper">
|
||||
<canvas id="map"></canvas>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" src="static/map/network.js"></script>
|
||||
{% endblock %}
|
78
api/updateGraph.py
Executable file
78
api/updateGraph.py
Executable file
@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python
|
||||
import graphPlotter
|
||||
import html
|
||||
|
||||
import urllib.request, json
|
||||
url = "http://[316:c51a:62a3:8b9::2]/result.json"
|
||||
|
||||
# nodes indexed by coords
|
||||
class NodeInfo:
|
||||
def __init__(self, ip, coords):
|
||||
self.ip = str(ip)
|
||||
self.label = str(ip).split(":")[-1]
|
||||
self.coords = str(coords)
|
||||
self.version = "unknown"
|
||||
def getCoordList(self):
|
||||
return self.coords.strip("[]").split(" ")
|
||||
def getParent(self):
|
||||
p = self.getCoordList()
|
||||
if len(p) > 0: p = p[:-1]
|
||||
return "[" + " ".join(p).strip() + "]"
|
||||
def getLink(self):
|
||||
c = self.getCoordList()
|
||||
return int(self.getCoordList()[-1].strip() or "0")
|
||||
|
||||
class LinkInfo:
|
||||
def __init__(self, a, b):
|
||||
self.a = a # NodeInfo
|
||||
self.b = b # NodeInfo
|
||||
|
||||
def generate_graph(time_limit=60*60*3):
|
||||
response = urllib.request.urlopen(url)
|
||||
data = json.loads(response.read())["yggnodes"]
|
||||
|
||||
toAdd = []
|
||||
for key in data:
|
||||
if 'address' not in data[key] or 'coords' not in data[key]: continue
|
||||
ip = data[key]['address']
|
||||
coords = data[key]['coords']
|
||||
info = NodeInfo(ip, coords)
|
||||
try:
|
||||
if 'nodeinfo' in data[key]:
|
||||
if 'name' in data[key]['nodeinfo']:
|
||||
label = str(data[key]['nodeinfo']['name'])
|
||||
if len(label) <= 64:
|
||||
info.label = label
|
||||
except: pass
|
||||
info.label = html.escape(info.label)
|
||||
toAdd.append(info)
|
||||
|
||||
nodes = dict()
|
||||
def addAncestors(info):
|
||||
coords = info.getParent()
|
||||
parent = NodeInfo("{} {}".format("?", coords), coords)
|
||||
parent.label = parent.ip
|
||||
nodes[parent.coords] = parent
|
||||
if parent.coords != parent.getParent(): addAncestors(parent)
|
||||
|
||||
for info in toAdd: addAncestors(info)
|
||||
for info in toAdd: nodes[info.coords] = info
|
||||
|
||||
sortedNodes = sorted(nodes.values(), key=(lambda x: x.getLink()))
|
||||
#for node in sortedNodes: print node.ip, node.coords, node.getParent(), node.getLink()
|
||||
|
||||
edges = []
|
||||
for node in sortedNodes:
|
||||
if node.coords == node.getParent: continue
|
||||
edges.append(LinkInfo(node, nodes[node.getParent()]))
|
||||
|
||||
print('%d nodes, %d edges' % (len(nodes), len(edges)))
|
||||
|
||||
graph = graphPlotter.position_nodes(nodes, edges)
|
||||
js = graphPlotter.get_graph_json(graph)
|
||||
|
||||
with open('api/static/graph.json', 'w') as f:
|
||||
f.write(js)
|
||||
|
||||
if __name__ == '__main__':
|
||||
generate_graph()
|
7
api/web.fcgi
Executable file
7
api/web.fcgi
Executable file
@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from flup.server.fcgi import WSGIServer
|
||||
from web import app
|
||||
|
||||
if __name__ == '__main__':
|
||||
WSGIServer(app).run()
|
@ -1,47 +1,62 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import signal, sys, time
|
||||
from flask import Flask, render_template
|
||||
from functools import wraps
|
||||
from flask import Flask, Response, render_template
|
||||
from flask_restful import Resource, Api
|
||||
import requests
|
||||
import psycopg2
|
||||
import json
|
||||
|
||||
######
|
||||
|
||||
DB_PASS = "password"
|
||||
DB_USER = "yggindex"
|
||||
DB_NAME = "yggindex"
|
||||
DB_HOST = "localhost"
|
||||
|
||||
# count peer alive if it was available not more that amount of seconds ago
|
||||
# I'm using 1 hour beause of running crawler every 15 minutes
|
||||
ALIVE_SECONDS = 3600 # 1 hour
|
||||
from config import DB_PASS, DB_USER, DB_NAME, DB_HOST, DB_RETRIES, DB_RECONNIDLE, ALIVE_SECONDS
|
||||
|
||||
######
|
||||
|
||||
app = Flask(__name__)
|
||||
api = Api(app)
|
||||
|
||||
dbconn = psycopg2.connect(host=DB_HOST,\
|
||||
database=DB_NAME,\
|
||||
user=DB_USER,\
|
||||
password=DB_PASS)
|
||||
def pg_connect():
|
||||
return psycopg2.connect(host=DB_HOST,\
|
||||
database=DB_NAME,\
|
||||
user=DB_USER,\
|
||||
password=DB_PASS)
|
||||
|
||||
|
||||
# dbconn = pg_connect() # initialize connection
|
||||
|
||||
|
||||
def retry(fn):
|
||||
@wraps(fn)
|
||||
def wrapper(*args, **kw):
|
||||
for x in range(DB_RETRIES):
|
||||
try:
|
||||
return fn(*args, **kw)
|
||||
except (psycopg2.InterfaceError, psycopg2.OperationalError) as e:
|
||||
print ("\nDatabase Connection [InterfaceError or OperationalError]")
|
||||
print ("Idle for %s seconds" % (cls._reconnectIdle))
|
||||
time.sleep(DB_RECONNIDLE)
|
||||
dbconn = pg_connect()
|
||||
return wrapper
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
dbconn.close()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def age_calc(ustamp):
|
||||
if (time.time() - ustamp) <= ALIVE_SECONDS:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
# active nodes
|
||||
class nodesCurrent(Resource):
|
||||
@retry
|
||||
def get(self):
|
||||
dbconn = pg_connect()
|
||||
cur = dbconn.cursor()
|
||||
|
||||
nodes = {}
|
||||
cur.execute("select * from yggindex")
|
||||
for i in cur.fetchall():
|
||||
@ -51,15 +66,18 @@ class nodesCurrent(Resource):
|
||||
dbconn.commit()
|
||||
cur.close()
|
||||
|
||||
nodelist = {}
|
||||
nodelist['yggnodes'] = nodes
|
||||
nodeinfo = {}
|
||||
nodeinfo['yggnodes'] = nodes
|
||||
|
||||
return nodelist
|
||||
dbconn.close()
|
||||
return nodeinfo
|
||||
|
||||
|
||||
# nodes info
|
||||
class nodesInfo(Resource):
|
||||
@retry
|
||||
def get(self):
|
||||
dbconn = pg_connect()
|
||||
cur = dbconn.cursor()
|
||||
nodes = {}
|
||||
cur.execute("select * from yggnodeinfo")
|
||||
@ -73,12 +91,15 @@ class nodesInfo(Resource):
|
||||
nodeinfo = {}
|
||||
nodeinfo['yggnodeinfo'] = nodes
|
||||
|
||||
dbconn.close()
|
||||
return nodeinfo
|
||||
|
||||
|
||||
# alive nodes count for latest 24 hours
|
||||
class nodes24h(Resource):
|
||||
@retry
|
||||
def get(self):
|
||||
dbconn = pg_connect()
|
||||
cur = dbconn.cursor()
|
||||
nodes = {}
|
||||
cur.execute("SELECT * FROM timeseries ORDER BY unixtstamp DESC LIMIT 24")
|
||||
@ -91,12 +112,15 @@ class nodes24h(Resource):
|
||||
nodeinfo = {}
|
||||
nodeinfo['nodes24h'] = nodes
|
||||
|
||||
dbconn.close()
|
||||
return nodeinfo
|
||||
|
||||
|
||||
# alive nodes count for latest 30 days
|
||||
class nodes30d(Resource):
|
||||
@retry
|
||||
def get(self):
|
||||
dbconn = pg_connect()
|
||||
cur = dbconn.cursor()
|
||||
nodes = {}
|
||||
cur.execute("SELECT * FROM timeseries ORDER BY unixtstamp DESC LIMIT 24 * 30")
|
||||
@ -109,20 +133,14 @@ class nodes30d(Resource):
|
||||
nodeinfo = {}
|
||||
nodeinfo['nodes30d'] = nodes
|
||||
|
||||
dbconn.close()
|
||||
return nodeinfo
|
||||
|
||||
|
||||
# alive nodes count for latest 24 hours
|
||||
class crawlResult(Resource):
|
||||
def get(self):
|
||||
with open('api/results.json', 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@app.route("/")
|
||||
@retry
|
||||
def fpage():
|
||||
dbconn = pg_connect()
|
||||
cur = dbconn.cursor()
|
||||
nodes = 0
|
||||
cur.execute("select * from yggindex")
|
||||
@ -134,19 +152,32 @@ def fpage():
|
||||
dbconn.commit()
|
||||
cur.close()
|
||||
|
||||
dbconn.close()
|
||||
return render_template('index.html', nodes=nodes)
|
||||
|
||||
@app.route('/map')
|
||||
@app.route('/map/network')
|
||||
def page_network():
|
||||
return render_template('map/network.html', page='network')
|
||||
|
||||
@app.route('/map/about')
|
||||
def page_about():
|
||||
return render_template('map/about.html', page='about')
|
||||
|
||||
@app.after_request
|
||||
def add_header(response):
|
||||
response.cache_control.max_age = 300
|
||||
return response
|
||||
|
||||
# sort out the api request here for the url
|
||||
api.add_resource(nodesCurrent, '/current')
|
||||
api.add_resource(nodesInfo, '/nodeinfo')
|
||||
api.add_resource(nodes24h, '/nodes24h')
|
||||
api.add_resource(nodes30d, '/nodes30d')
|
||||
api.add_resource(crawlResult, '/result.json')
|
||||
|
||||
# regirster signal handler
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='::', port=3000)
|
||||
app.run(host='127.0.0.1', port=3001)
|
36
ygg-crawl.sh
36
ygg-crawl.sh
@ -1,12 +1,32 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
YGGCRAWL="/opt/yggcrawl/yggcrawl" # path to yggcrawl binary
|
||||
YGGAPIPATH="/opt/yggdrasil-api" # path to Niflheim-API directory
|
||||
ulimit -n 4096
|
||||
|
||||
CRAWLPEER="tcp://127.0.0.1:12345" # Yggdrasil peer address
|
||||
CRAWLFILE="api/results.json"
|
||||
CRAWLRETR=3
|
||||
YGGCRAWL="/opt/yggdrasil-crawler/crawler"
|
||||
YGGAPIPATH="/opt/yggdrasil-api"
|
||||
|
||||
TMPFILE="api/current.json"
|
||||
CRAWLFILE="api/result.json"
|
||||
|
||||
# Crawler timeout in minutes. It must be lesser then crontab job period
|
||||
# Increased to 50 minutes and crontab runs hourly due to network instabillity
|
||||
#CRAWLTIMEOUT=50
|
||||
|
||||
##############################################################################
|
||||
|
||||
cd $YGGAPIPATH
|
||||
$YGGCRAWL -peer $CRAWLPEER -retry $CRAWLRETR -file $CRAWLFILE > api/yggcrawl.log 2>&1
|
||||
venv/bin/python api/importer.py >> api/yggcrawl.log 2>&1
|
||||
|
||||
#let "TIMEOUT = $CRAWLTIMEOUT * 60"
|
||||
#timeout $TIMEOUT $YGGCRAWL > $TMPFILE 2>logs/crawler.log
|
||||
|
||||
$YGGCRAWL > $TMPFILE 2>logs/crawler.log
|
||||
|
||||
if [[ $? == 0 ]] # Crawler not triggered error or was killed
|
||||
then
|
||||
# add a little delay...
|
||||
sleep 3
|
||||
mv -f $TMPFILE $CRAWLFILE
|
||||
venv/bin/python api/importer.py > logs/importer.log 2>&1
|
||||
venv/bin/python api/addresses.py > logs/addresses.log 2>&1
|
||||
venv/bin/python api/updateGraph.py > logs/graph.log 2>&1
|
||||
fi
|
||||
|
@ -4,7 +4,7 @@ After=network.target
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/opt/yggdrasil-api
|
||||
ExecStart=/opt/yggdrasil-api/venv/bin/python /opt/yggdrasil-api/api/niflheim-api.py
|
||||
ExecStart=/opt/yggdrasil-api/venv/bin/python /opt/yggdrasil-api/api/web.py
|
||||
Restart=always
|
||||
|
||||
Type=simple
|
||||
|
Loading…
x
Reference in New Issue
Block a user