Initial commit
This commit is contained in:
commit
5c1381bdb1
18 changed files with 1189 additions and 0 deletions
42
.gitignore
vendored
Normal file
42
.gitignore
vendored
Normal file
|
@ -0,0 +1,42 @@
|
|||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
.idea/modules.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/compiler.xml
|
||||
.idea/libraries/
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### Eclipse ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
|
||||
### Custom ###
|
||||
geoip.json
|
13
Dockerfile
Normal file
13
Dockerfile
Normal file
|
@ -0,0 +1,13 @@
|
|||
FROM maven:3.9.9-eclipse-temurin-21-alpine AS build
|
||||
WORKDIR /tmp
|
||||
COPY ./pom.xml ./pom.xml
|
||||
COPY ./src ./src
|
||||
RUN mvn install
|
||||
|
||||
FROM eclipse-temurin:23-alpine
|
||||
WORKDIR /opt/lbry/globe/
|
||||
EXPOSE 25/tcp
|
||||
EXPOSE 465/tcp
|
||||
EXPOSE 587/tcp
|
||||
COPY --from=build /tmp/target/lbry-globe-latest-jar-with-dependencies.jar ./lbry-globe.jar
|
||||
CMD ["java","-jar","lbry-globe.jar"]
|
26
LICENSE.md
Normal file
26
LICENSE.md
Normal file
|
@ -0,0 +1,26 @@
|
|||
The MIT License (MIT)
|
||||
=====================
|
||||
|
||||
Copyright © 2024 The LBRY Foundation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the “Software”), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
6
README.md
Normal file
6
README.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
# LBRY Globe
|
||||
|
||||
Get insight in the size and distribution of the LBRY network.
|
||||
|
||||
## License
|
||||
This project is MIT licensed. For the full license, see [LICENSE](LICENSE.md).
|
75
pom.xml
Normal file
75
pom.xml
Normal file
|
@ -0,0 +1,75 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project
|
||||
xmlns="http://maven.apache.org/POM/4.0.0">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.lbry</groupId>
|
||||
<artifactId>lbry-globe</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>3.3.0</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<mainClass>com.lbry.globe.Main</mainClass>
|
||||
</manifest>
|
||||
<manifestEntries>
|
||||
<Multi-Release>true</Multi-Release>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<configuration>
|
||||
<source>8</source>
|
||||
<target>8</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.dampcake</groupId>
|
||||
<artifactId>bencode</artifactId>
|
||||
<version>1.4.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-all</artifactId>
|
||||
<version>4.1.115.Final</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.json</groupId>
|
||||
<artifactId>json</artifactId>
|
||||
<version>20240303</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
</project>
|
72
src/main/java/com/lbry/globe/Main.java
Normal file
72
src/main/java/com/lbry/globe/Main.java
Normal file
|
@ -0,0 +1,72 @@
|
|||
package com.lbry.globe;
|
||||
|
||||
import com.lbry.globe.handler.HTTPHandler;
|
||||
import com.lbry.globe.logging.LogLevel;
|
||||
import com.lbry.globe.thread.BlockchainNodeFinderThread;
|
||||
import com.lbry.globe.thread.DHTNodeFinderThread;
|
||||
import com.lbry.globe.thread.HubNodeFinderThread;
|
||||
import com.lbry.globe.util.GeoIP;
|
||||
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||
import io.netty.handler.codec.http.HttpRequestDecoder;
|
||||
import io.netty.handler.codec.http.HttpResponseEncoder;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class Main implements Runnable{
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger("Main");
|
||||
|
||||
static{
|
||||
System.setProperty("java.util.logging.SimpleFormatter.format","%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL [%4$s/%3$s]: %5$s%6$s%n");
|
||||
}
|
||||
|
||||
public Main(String... args){
|
||||
Main.LOGGER.info("Arguments = "+ Arrays.toString(args));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(){
|
||||
EventLoopGroup group = new NioEventLoopGroup();
|
||||
this.runTCPServerHTTP(group);
|
||||
}
|
||||
|
||||
private void runTCPServerHTTP(EventLoopGroup group){
|
||||
ServerBootstrap b = new ServerBootstrap().group(group).channel(NioServerSocketChannel.class);
|
||||
|
||||
b.childHandler(new ChannelInitializer<SocketChannel>(){
|
||||
protected void initChannel(SocketChannel socketChannel){
|
||||
socketChannel.pipeline().addLast(new HttpRequestDecoder());
|
||||
socketChannel.pipeline().addLast(new HttpResponseEncoder());
|
||||
socketChannel.pipeline().addLast("http",new HTTPHandler());
|
||||
}
|
||||
});
|
||||
|
||||
try{
|
||||
b.bind(80).sync();
|
||||
}catch(InterruptedException e){
|
||||
Main.LOGGER.log(LogLevel.ERROR,"Failed starting server",e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String... args){
|
||||
Main.LOGGER.info("Loading GeoIP cache");
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(GeoIP::saveCache));
|
||||
GeoIP.loadCache();
|
||||
Main.LOGGER.info("Starting finder thread for blockchain nodes");
|
||||
new Thread(new BlockchainNodeFinderThread()).start();
|
||||
Main.LOGGER.info("Starting finder thread for DHT nodes");
|
||||
new Thread(new DHTNodeFinderThread()).start();
|
||||
Main.LOGGER.info("Starting finder thread for hub nodes");
|
||||
new Thread(new HubNodeFinderThread()).start();
|
||||
Main.LOGGER.info("Starting server");
|
||||
new Main(args).run();
|
||||
}
|
||||
|
||||
}
|
38
src/main/java/com/lbry/globe/api/API.java
Normal file
38
src/main/java/com/lbry/globe/api/API.java
Normal file
|
@ -0,0 +1,38 @@
|
|||
package com.lbry.globe.api;
|
||||
|
||||
import com.lbry.globe.object.Node;
|
||||
import com.lbry.globe.object.Service;
|
||||
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class API{
|
||||
|
||||
public static final Map<InetAddress, Node> NODES = new TreeMap<>(Comparator.comparing(InetAddress::getHostAddress));
|
||||
|
||||
public static void fillPoints(JSONArray points){
|
||||
for(Node node : API.NODES.values()){
|
||||
for(Service service : node.getServices()){
|
||||
JSONObject obj = new JSONObject();
|
||||
obj.put("id",service.getId().toString());
|
||||
String hostname = node.getAddress().toString().split("/")[0];
|
||||
String address = node.getAddress().getHostAddress();
|
||||
if(node.getAddress() instanceof Inet6Address){
|
||||
address = "["+address+"]";
|
||||
}
|
||||
obj.put("label",(!hostname.isEmpty()?(hostname+":"+service.getPort()+" - "):"")+address+":"+service.getPort()+" ("+service.getType()+")");
|
||||
obj.put("lat",node.getLatitude());
|
||||
obj.put("lng",node.getLongitude());
|
||||
obj.put("type",service.getType());
|
||||
points.put(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
128
src/main/java/com/lbry/globe/handler/HTTPHandler.java
Normal file
128
src/main/java/com/lbry/globe/handler/HTTPHandler.java
Normal file
|
@ -0,0 +1,128 @@
|
|||
package com.lbry.globe.handler;
|
||||
|
||||
import com.lbry.globe.api.API;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.handler.codec.http.*;
|
||||
import io.netty.util.AttributeKey;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class HTTPHandler extends ChannelInboundHandlerAdapter{
|
||||
|
||||
public static final AttributeKey<HttpRequest> ATTR_REQUEST = AttributeKey.newInstance("request");
|
||||
public static final AttributeKey<List<HttpContent>> ATTR_CONTENT = AttributeKey.newInstance("content");
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx,Object msg){
|
||||
if(msg instanceof HttpRequest){
|
||||
HttpRequest request = (HttpRequest) msg;
|
||||
ctx.channel().attr(HTTPHandler.ATTR_REQUEST).set(request);
|
||||
}
|
||||
if(msg instanceof HttpContent){
|
||||
HttpContent content = (HttpContent) msg;
|
||||
ctx.channel().attr(HTTPHandler.ATTR_CONTENT).setIfAbsent(new ArrayList<>());
|
||||
ctx.channel().attr(HTTPHandler.ATTR_CONTENT).get().add(content);
|
||||
|
||||
if(msg instanceof LastHttpContent){
|
||||
this.handleResponse(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleResponse(ChannelHandlerContext ctx){
|
||||
HttpRequest request = ctx.channel().attr(HTTPHandler.ATTR_REQUEST).get();
|
||||
List<HttpContent> content = ctx.channel().attr(HTTPHandler.ATTR_CONTENT).get();
|
||||
ctx.channel().attr(HTTPHandler.ATTR_REQUEST).set(null);
|
||||
ctx.channel().attr(HTTPHandler.ATTR_CONTENT).set(null);
|
||||
|
||||
if(request.method().equals(HttpMethod.GET)){
|
||||
URI uri = URI.create(request.uri());
|
||||
|
||||
if("/".equals(uri.getPath())){
|
||||
int status = 200;
|
||||
byte[] indexData;
|
||||
try{
|
||||
indexData = Files.readAllBytes(Paths.get(HTTPHandler.getResource("index.html").toURI()));
|
||||
}catch(Exception ignored){
|
||||
status = 500;
|
||||
indexData = "Some error occured.".getBytes();
|
||||
}
|
||||
ByteBuf responseContent = Unpooled.copiedBuffer(indexData);
|
||||
FullHttpResponse response = new DefaultFullHttpResponse(request.protocolVersion(),HttpResponseStatus.valueOf(status),responseContent);
|
||||
response.headers().add("Content-Length",responseContent.capacity());
|
||||
response.headers().add("Content-Type","text/html");
|
||||
ctx.write(response);
|
||||
return;
|
||||
}
|
||||
if("/api".equals(uri.getPath())){
|
||||
JSONArray points = new JSONArray();
|
||||
API.fillPoints(points);
|
||||
|
||||
JSONObject json = new JSONObject().put("points",points);//new JSONObject(new String(jsonData));
|
||||
ByteBuf responseContent = Unpooled.copiedBuffer(json.toString().getBytes());
|
||||
FullHttpResponse response = new DefaultFullHttpResponse(request.protocolVersion(),HttpResponseStatus.OK,responseContent);
|
||||
response.headers().add("Content-Length",responseContent.capacity());
|
||||
response.headers().add("Content-Type","application/json");
|
||||
ctx.write(response);
|
||||
return;
|
||||
}
|
||||
byte[] fileData = null;
|
||||
try{
|
||||
fileData = Files.readAllBytes(Paths.get(HTTPHandler.getResource(uri.getPath().substring(1)).toURI()));
|
||||
}catch(Exception ignored){
|
||||
}
|
||||
boolean ok = fileData!=null;
|
||||
|
||||
String contentType = null;
|
||||
if("/earth.jpg".equals(uri.getPath())){
|
||||
contentType = "image/jpg";
|
||||
}
|
||||
if("/earth.png".equals(uri.getPath())){
|
||||
contentType = "image/png";
|
||||
}
|
||||
if("/favicon.ico".equals(uri.getPath())){
|
||||
contentType = "image/vnd.microsoft.icon";
|
||||
}
|
||||
|
||||
ByteBuf responseContent = ok?Unpooled.copiedBuffer(fileData):Unpooled.copiedBuffer("File not found.\r\n".getBytes());
|
||||
FullHttpResponse response = new DefaultFullHttpResponse(request.protocolVersion(),ok?HttpResponseStatus.OK:HttpResponseStatus.NOT_FOUND,responseContent);
|
||||
response.headers().add("Content-Length",responseContent.capacity());
|
||||
response.headers().add("Content-Type",contentType==null?"text/html":contentType);
|
||||
ctx.write(response);
|
||||
return;
|
||||
}
|
||||
|
||||
ByteBuf responseContent = Unpooled.copiedBuffer("Method not allowed.\r\n".getBytes());
|
||||
FullHttpResponse response = new DefaultFullHttpResponse(request.protocolVersion(),HttpResponseStatus.METHOD_NOT_ALLOWED,responseContent);
|
||||
response.headers().add("Content-Length",responseContent.capacity());
|
||||
response.headers().add("Content-Type","text/html");
|
||||
ctx.write(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelReadComplete(ChannelHandlerContext ctx){
|
||||
ctx.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
|
||||
cause.printStackTrace(); //TODO
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
private static URL getResource(String name){
|
||||
return HTTPHandler.class.getClassLoader().getResource(name);
|
||||
}
|
||||
|
||||
}
|
21
src/main/java/com/lbry/globe/logging/LogLevel.java
Normal file
21
src/main/java/com/lbry/globe/logging/LogLevel.java
Normal file
|
@ -0,0 +1,21 @@
|
|||
package com.lbry.globe.logging;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class LogLevel extends Level {
|
||||
|
||||
public static final Level FATAL = new LogLevel("FATAL",5000);
|
||||
|
||||
public static final Level ERROR = new LogLevel("ERROR",1000);
|
||||
|
||||
public static final Level DEBUG = new LogLevel("DEBUG",800);
|
||||
|
||||
protected LogLevel(String name, int value) {
|
||||
super(name, value);
|
||||
}
|
||||
|
||||
protected LogLevel(String name, int value, String resourceBundleName) {
|
||||
super(name, value, resourceBundleName);
|
||||
}
|
||||
|
||||
}
|
36
src/main/java/com/lbry/globe/object/Node.java
Normal file
36
src/main/java/com/lbry/globe/object/Node.java
Normal file
|
@ -0,0 +1,36 @@
|
|||
package com.lbry.globe.object;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class Node{
|
||||
|
||||
private final InetAddress address;
|
||||
private Double latitude;
|
||||
private Double longitude;
|
||||
private List<Service> services = new ArrayList<>();
|
||||
|
||||
public Node(InetAddress address,Double latitude,Double longitude){
|
||||
this.address = address;
|
||||
this.latitude = latitude;
|
||||
this.longitude = longitude;
|
||||
}
|
||||
|
||||
public InetAddress getAddress() {
|
||||
return this.address;
|
||||
}
|
||||
|
||||
public Double getLatitude() {
|
||||
return this.latitude;
|
||||
}
|
||||
|
||||
public Double getLongitude() {
|
||||
return this.longitude;
|
||||
}
|
||||
|
||||
public List<Service> getServices(){
|
||||
return this.services;
|
||||
}
|
||||
|
||||
}
|
44
src/main/java/com/lbry/globe/object/Service.java
Normal file
44
src/main/java/com/lbry/globe/object/Service.java
Normal file
|
@ -0,0 +1,44 @@
|
|||
package com.lbry.globe.object;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class Service{
|
||||
|
||||
private final UUID id;
|
||||
|
||||
private final int port;
|
||||
|
||||
private final String type;
|
||||
|
||||
private long lastSeen;
|
||||
|
||||
public boolean markedForRemoval;
|
||||
|
||||
public Service(UUID id,int port,String type){
|
||||
this.id = id;
|
||||
this.port = port;
|
||||
this.type = type;
|
||||
this.updateLastSeen();
|
||||
}
|
||||
|
||||
public UUID getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return this.port;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
public void updateLastSeen(){
|
||||
this.lastSeen = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public long getLastSeen(){
|
||||
return this.lastSeen;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
package com.lbry.globe.thread;
|
||||
|
||||
import com.lbry.globe.api.API;
|
||||
import com.lbry.globe.object.Node;
|
||||
import com.lbry.globe.object.Service;
|
||||
import com.lbry.globe.util.GeoIP;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class BlockchainNodeFinderThread implements Runnable{
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
while(true){
|
||||
try{
|
||||
HttpURLConnection conn = (HttpURLConnection) new URL(System.getenv("BLOCKCHAIN_RPC_URL")).openConnection();
|
||||
conn.setDoOutput(true);
|
||||
conn.addRequestProperty("Authorization","Basic "+ Base64.getEncoder().encodeToString((System.getenv("BLOCKCHAIN_USERNAME")+":"+System.getenv("BLOCKCHAIN_PASSWORD")).getBytes()));
|
||||
conn.connect();
|
||||
conn.getOutputStream().write(new JSONObject().put("id",new Random().nextInt()).put("method","getnodeaddresses").put("params",new JSONArray().put(2147483647)).toString().getBytes());
|
||||
InputStream in = conn.getInputStream();
|
||||
if(in==null){
|
||||
in = conn.getErrorStream();
|
||||
}
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader(in));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while((line = br.readLine())!=null){
|
||||
sb.append(line);
|
||||
}
|
||||
JSONObject json = new JSONObject(sb.toString());
|
||||
manageBlockchainNodes(json.getJSONArray("result"));
|
||||
}catch(Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
try {
|
||||
Thread.sleep(10_000);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void manageBlockchainNodes(JSONArray nodes){
|
||||
Map<InetAddress,JSONObject> data = new TreeMap<>(Comparator.comparing(InetAddress::getHostAddress));
|
||||
for(int i=0;i<nodes.length();i++){
|
||||
JSONObject node = nodes.getJSONObject(i);
|
||||
String hostname = node.getString("address");
|
||||
try{
|
||||
for(InetAddress ip : InetAddress.getAllByName(hostname)){
|
||||
data.put(ip,node);
|
||||
}
|
||||
}catch(Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
for(Node n : API.NODES.values()){
|
||||
for(Service s : n.getServices()){
|
||||
long difference = System.currentTimeMillis()-s.getLastSeen();
|
||||
if(difference>60_000){
|
||||
s.markedForRemoval = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(Map.Entry<InetAddress,JSONObject> node : data.entrySet()){
|
||||
Node existingNode = API.NODES.get(node.getKey());
|
||||
if(existingNode==null){
|
||||
JSONObject geoData = GeoIP.getCachedGeoIPInformation(node.getKey());
|
||||
Double[] coords = GeoIP.getCoordinateFromLocation(geoData.has("loc")?geoData.getString("loc"):null);
|
||||
existingNode = new Node(node.getKey(),coords[0],coords[1]);
|
||||
API.NODES.put(node.getKey(),existingNode);
|
||||
}
|
||||
|
||||
Service blockchainService = null;
|
||||
for(Service s : existingNode.getServices()){
|
||||
if(node.getValue().getInt("port")==s.getPort() && "blockchain".equals(s.getType())){
|
||||
blockchainService = s;
|
||||
blockchainService.markedForRemoval = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(blockchainService==null){
|
||||
existingNode.getServices().add(new Service(UUID.randomUUID(),node.getValue().getInt("port"),"blockchain"));
|
||||
}else{
|
||||
blockchainService.updateLastSeen();
|
||||
}
|
||||
}
|
||||
|
||||
for(Node n : API.NODES.values()){
|
||||
List<Service> removedService = new ArrayList<>();
|
||||
for(Service s : n.getServices()){
|
||||
if(s.markedForRemoval && "blockchain".equals(s.getType())){
|
||||
removedService.add(s);
|
||||
}
|
||||
}
|
||||
n.getServices().removeAll(removedService);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
216
src/main/java/com/lbry/globe/thread/DHTNodeFinderThread.java
Normal file
216
src/main/java/com/lbry/globe/thread/DHTNodeFinderThread.java
Normal file
|
@ -0,0 +1,216 @@
|
|||
package com.lbry.globe.thread;
|
||||
|
||||
import com.dampcake.bencode.Bencode;
|
||||
import com.dampcake.bencode.Type;
|
||||
import com.lbry.globe.api.API;
|
||||
import com.lbry.globe.object.Node;
|
||||
import com.lbry.globe.object.Service;
|
||||
import com.lbry.globe.util.GeoIP;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class DHTNodeFinderThread implements Runnable{
|
||||
|
||||
private static final Bencode BENCODE = new Bencode();
|
||||
|
||||
public static final String[] BOOTSTRAP = {
|
||||
"dht.lbry.grin.io:4444", // Grin
|
||||
"dht.lbry.madiator.com:4444", // Madiator
|
||||
"dht.lbry.pigg.es:4444", // Pigges
|
||||
"lbrynet1.lbry.com:4444", // US EAST
|
||||
"lbrynet2.lbry.com:4444", // US WEST
|
||||
"lbrynet3.lbry.com:4444", // EU
|
||||
"lbrynet4.lbry.com:4444", // ASIA
|
||||
"dht.lizard.technology:4444", // Jack
|
||||
"s2.lbry.network:4444",
|
||||
};
|
||||
|
||||
private static final DatagramSocket SOCKET;
|
||||
|
||||
static{
|
||||
try{
|
||||
SOCKET = new DatagramSocket();
|
||||
}catch(SocketException e){
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private final Map<InetSocketAddress,Boolean> pingableDHTs = new ConcurrentHashMap<>();
|
||||
|
||||
private final Queue<DatagramPacket> incoming = new ConcurrentLinkedQueue<>();
|
||||
|
||||
@Override
|
||||
public void run(){
|
||||
for(String bootstrap : DHTNodeFinderThread.BOOTSTRAP){
|
||||
URI uri = URI.create("udp://"+bootstrap);
|
||||
this.pingableDHTs.put(new InetSocketAddress(uri.getHost(),uri.getPort()),true);
|
||||
}
|
||||
|
||||
// Ping Sender
|
||||
new Thread(() -> {
|
||||
while(true){
|
||||
for(InetSocketAddress socketAddress : DHTNodeFinderThread.this.pingableDHTs.keySet()){
|
||||
String hostname = socketAddress.getHostName();
|
||||
int port = socketAddress.getPort();
|
||||
try{
|
||||
for(InetAddress ip : InetAddress.getAllByName(hostname)){
|
||||
DHTNodeFinderThread.ping(ip,port);
|
||||
}
|
||||
}catch(Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
try {
|
||||
Thread.sleep(15_000);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
|
||||
// Receiver
|
||||
new Thread(() -> {
|
||||
while(true) {
|
||||
try {
|
||||
byte[] buffer = new byte[1024];
|
||||
DatagramPacket receiverPacket = new DatagramPacket(buffer, buffer.length);
|
||||
DHTNodeFinderThread.SOCKET.receive(receiverPacket);
|
||||
DHTNodeFinderThread.this.incoming.add(receiverPacket);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
|
||||
while(true){
|
||||
|
||||
//TODO: MARKS AS DELETED
|
||||
|
||||
while(this.incoming.peek()!=null){
|
||||
DatagramPacket receiverPacket = this.incoming.poll();
|
||||
byte[] receivingBytes = receiverPacket.getData();
|
||||
Map<String, Object> receivingDictionary = DHTNodeFinderThread.decodePacket(receivingBytes);
|
||||
if(receivingDictionary.get("0").equals(1L)){
|
||||
if(receivingDictionary.get("3").equals("pong")){
|
||||
try{
|
||||
DHTNodeFinderThread.findNode(receiverPacket.getAddress(),receiverPacket.getPort());
|
||||
}catch(Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
//TODO Improve updating pinged nodes.
|
||||
System.out.println("PONG: "+receiverPacket.getSocketAddress());
|
||||
|
||||
Node existingNode = API.NODES.get(receiverPacket.getAddress());
|
||||
if(existingNode==null){
|
||||
JSONObject geoData = GeoIP.getCachedGeoIPInformation(receiverPacket.getAddress());
|
||||
Double[] coords = GeoIP.getCoordinateFromLocation(geoData.has("loc")?geoData.getString("loc"):null);
|
||||
existingNode = new Node(receiverPacket.getAddress(),coords[0],coords[1]);
|
||||
API.NODES.put(receiverPacket.getAddress(),existingNode);
|
||||
}
|
||||
Service dhtService = null;
|
||||
for(Service s : existingNode.getServices()){
|
||||
if(s.getPort()==receiverPacket.getPort() && "dht".equals(s.getType())){
|
||||
dhtService = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(dhtService==null){
|
||||
existingNode.getServices().add(new Service(UUID.randomUUID(),receiverPacket.getPort(),"dht"));
|
||||
}else{
|
||||
dhtService.updateLastSeen();
|
||||
}
|
||||
}else{
|
||||
//TODO Save connections too
|
||||
List<List<Object>> nodes = (List<List<Object>>) receivingDictionary.get("3");
|
||||
for(List<Object> n : nodes){
|
||||
String hostname = (String) n.get(1);
|
||||
int port = (int) ((long) n.get(2));
|
||||
InetSocketAddress existingSocketAddr = null;
|
||||
for(InetSocketAddress addr : this.pingableDHTs.keySet()){
|
||||
if(addr.getHostName().equals(hostname) && addr.getPort()==port){
|
||||
existingSocketAddr = addr;
|
||||
}
|
||||
}
|
||||
if(existingSocketAddr==null){
|
||||
this.pingableDHTs.put(new InetSocketAddress(hostname,port),false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: REMOVE MARKED AS DELETED
|
||||
|
||||
System.out.println("----");
|
||||
try {
|
||||
Thread.sleep(1_000);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ping(InetAddress ip,int port) throws IOException{
|
||||
byte[] rpcID = new byte[20];
|
||||
new Random().nextBytes(rpcID);
|
||||
|
||||
Map<String,Object> ping = new HashMap<>();
|
||||
ping.put("0",0);
|
||||
ping.put("1",rpcID);
|
||||
ping.put("2",new byte[48]);
|
||||
ping.put("3","ping");
|
||||
ping.put("4",Collections.singletonList(Collections.singletonMap("protocolVersion",1)));
|
||||
byte[] pingBytes = DHTNodeFinderThread.encodePacket(ping);
|
||||
|
||||
DatagramPacket sendingDiagram = new DatagramPacket(pingBytes,pingBytes.length,ip,port);
|
||||
DHTNodeFinderThread.SOCKET.send(sendingDiagram);
|
||||
}
|
||||
|
||||
private static void findNode(InetAddress ip,int port) throws IOException{
|
||||
byte[] rpcID = new byte[20];
|
||||
new Random().nextBytes(rpcID);
|
||||
|
||||
Map<String,Object> findNode = new HashMap<>();
|
||||
findNode.put("0",0);
|
||||
findNode.put("1",rpcID);
|
||||
findNode.put("2",new byte[48]);
|
||||
findNode.put("3","findNode");
|
||||
findNode.put("4",Arrays.asList(new byte[48],Collections.singletonMap("protocolVersion",1)));
|
||||
byte[] findNodeBytes = DHTNodeFinderThread.encodePacket(findNode);
|
||||
|
||||
DatagramPacket sendingDiagram = new DatagramPacket(findNodeBytes,findNodeBytes.length,ip,port);
|
||||
DHTNodeFinderThread.SOCKET.send(sendingDiagram);
|
||||
}
|
||||
|
||||
private static byte[] encodePacket(Map<String,Object> map){
|
||||
return DHTNodeFinderThread.BENCODE.encode(map);
|
||||
}
|
||||
|
||||
private static Map<String,Object> decodePacket(byte[] bytes){
|
||||
// Fix invalid B-encoding
|
||||
if(bytes[0]=='d'){
|
||||
bytes[0] = 'l';
|
||||
}
|
||||
List<Object> list = DHTNodeFinderThread.BENCODE.decode(bytes,Type.LIST);
|
||||
for(int i=0;i<list.size();i++){
|
||||
if(i%2==0){
|
||||
list.set(i,String.valueOf(list.get(i)));
|
||||
}
|
||||
}
|
||||
bytes = DHTNodeFinderThread.BENCODE.encode(list);
|
||||
if(bytes[0]=='l'){
|
||||
bytes[0] = 'd';
|
||||
}
|
||||
// Normal B-decoding
|
||||
return DHTNodeFinderThread.BENCODE.decode(bytes,Type.DICTIONARY);
|
||||
}
|
||||
|
||||
}
|
124
src/main/java/com/lbry/globe/thread/HubNodeFinderThread.java
Normal file
124
src/main/java/com/lbry/globe/thread/HubNodeFinderThread.java
Normal file
|
@ -0,0 +1,124 @@
|
|||
package com.lbry.globe.thread;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
import java.util.*;
|
||||
|
||||
import com.lbry.globe.api.API;
|
||||
import com.lbry.globe.object.Node;
|
||||
import com.lbry.globe.object.Service;
|
||||
import com.lbry.globe.util.GeoIP;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class HubNodeFinderThread implements Runnable{
|
||||
|
||||
public static final String[] HUBS = {
|
||||
"spv11.lbry.com",
|
||||
"spv12.lbry.com",
|
||||
"spv13.lbry.com",
|
||||
"spv14.lbry.com",
|
||||
"spv15.lbry.com",
|
||||
"spv16.lbry.com",
|
||||
"spv17.lbry.com",
|
||||
"spv18.lbry.com",
|
||||
"spv19.lbry.com",
|
||||
"hub.lbry.grin.io",
|
||||
"hub.lizard.technology",
|
||||
"s1.lbry.network",
|
||||
|
||||
"hub.lbry.nl",
|
||||
};
|
||||
|
||||
private static final Map<InetAddress,Long> LAST_SEEN = new TreeMap<>(Comparator.comparing(InetAddress::getHostAddress));
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
while(true){
|
||||
for(String hostname : HubNodeFinderThread.HUBS){
|
||||
try{
|
||||
for(InetAddress ip : InetAddress.getAllByName(hostname)){
|
||||
new Thread(() -> {
|
||||
try{
|
||||
Socket s = new Socket(ip,50001);
|
||||
JSONObject obj = new JSONObject();
|
||||
obj.put("id",new Random().nextInt());
|
||||
obj.put("method","server.banner");
|
||||
obj.put("params",new JSONArray());
|
||||
s.getOutputStream().write((obj+"\n").getBytes());
|
||||
s.getOutputStream().flush();
|
||||
InputStream in = s.getInputStream();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int b;
|
||||
while((b = in.read())!='\n'){
|
||||
sb.append(new String(new byte[]{(byte) (b & 0xFF)}));
|
||||
}
|
||||
in.close();
|
||||
JSONObject respObj = new JSONObject(sb.toString());
|
||||
boolean successful = respObj.has("result") && !respObj.isNull("result");
|
||||
if(successful){
|
||||
LAST_SEEN.put(ip,System.currentTimeMillis());
|
||||
}
|
||||
}catch(Exception e){
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}catch(Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
List<InetAddress> removeIPs = new ArrayList<>();
|
||||
for(Map.Entry<InetAddress,Long> entry : HubNodeFinderThread.LAST_SEEN.entrySet()){
|
||||
long difference = System.currentTimeMillis()-entry.getValue();
|
||||
if(difference>60_000){
|
||||
removeIPs.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
for(InetAddress removeIP : removeIPs){
|
||||
HubNodeFinderThread.LAST_SEEN.remove(removeIP);
|
||||
Node n = API.NODES.get(removeIP);
|
||||
if(n!=null){
|
||||
List<Service> removeServices = new ArrayList<>();
|
||||
for(Service s : n.getServices()){
|
||||
if(s.getPort()==50001 && "hub".equals(s.getType())){
|
||||
removeServices.add(s);
|
||||
}
|
||||
}
|
||||
n.getServices().removeAll(removeServices);
|
||||
}
|
||||
}
|
||||
for(Map.Entry<InetAddress,Long> entry : HubNodeFinderThread.LAST_SEEN.entrySet()){
|
||||
Node existingNode = API.NODES.get(entry.getKey());
|
||||
if(existingNode==null){
|
||||
JSONObject geoData = GeoIP.getCachedGeoIPInformation(entry.getKey());
|
||||
Double[] coords = GeoIP.getCoordinateFromLocation(geoData.has("loc")?geoData.getString("loc"):null);
|
||||
existingNode = new Node(entry.getKey(),coords[0],coords[1]);
|
||||
API.NODES.put(entry.getKey(),existingNode);
|
||||
}
|
||||
Service hubService = null;
|
||||
for(Service s : existingNode.getServices()){
|
||||
if(s.getPort()==50001 && "hub".equals(s.getType())){
|
||||
hubService = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(hubService==null){
|
||||
existingNode.getServices().add(new Service(UUID.randomUUID(),50001,"hub"));
|
||||
}else{
|
||||
hubService.updateLastSeen();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Thread.sleep(10_000);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
89
src/main/java/com/lbry/globe/util/GeoIP.java
Normal file
89
src/main/java/com/lbry/globe/util/GeoIP.java
Normal file
|
@ -0,0 +1,89 @@
|
|||
package com.lbry.globe.util;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URL;
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class GeoIP{
|
||||
|
||||
private static final Map<InetAddress,JSONObject> CACHE = new TreeMap<>(Comparator.comparing(InetAddress::getHostAddress));
|
||||
private static final String TOKEN = System.getenv("IPINFO_TOKEN");
|
||||
|
||||
public static JSONObject getCachedGeoIPInformation(InetAddress ip){
|
||||
JSONObject result = CACHE.get(ip);
|
||||
if(result==null){
|
||||
try{
|
||||
result = GeoIP.getGeoIPInformation(ip);
|
||||
GeoIP.CACHE.put(ip,result);
|
||||
GeoIP.saveCache();
|
||||
}catch(Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static JSONObject getGeoIPInformation(InetAddress ip) throws IOException{
|
||||
HttpURLConnection conn = (HttpURLConnection) new URL("https://ipinfo.io/"+ip.getHostAddress()+"?token="+GeoIP.TOKEN).openConnection();
|
||||
conn.connect();
|
||||
InputStream in = conn.getInputStream();
|
||||
if(in==null){
|
||||
in = conn.getErrorStream();
|
||||
}
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader(in));
|
||||
String line;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
while((line = br.readLine())!=null){
|
||||
sb.append(line);
|
||||
}
|
||||
return new JSONObject(sb.toString());
|
||||
|
||||
}
|
||||
|
||||
public static Double[] getCoordinateFromLocation(String location){
|
||||
if(location==null){
|
||||
return new Double[]{null,null};
|
||||
}
|
||||
String[] parts = location.split(",");
|
||||
return new Double[]{Double.parseDouble(parts[0]),Double.parseDouble(parts[1])};
|
||||
}
|
||||
|
||||
public static void loadCache(){
|
||||
try{
|
||||
BufferedReader br = new BufferedReader(new FileReader("geoip.json"));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while((line = br.readLine())!=null){
|
||||
sb.append(line);
|
||||
}
|
||||
JSONObject obj = new JSONObject(sb.toString());
|
||||
for(String key : obj.keySet()){
|
||||
GeoIP.CACHE.put(InetAddress.getByName(key),obj.getJSONObject(key));
|
||||
}
|
||||
br.close();
|
||||
}catch(Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static void saveCache(){
|
||||
try{
|
||||
FileOutputStream fos = new FileOutputStream("geoip.json");
|
||||
JSONObject obj = new JSONObject();
|
||||
for(Map.Entry<InetAddress,JSONObject> entry : GeoIP.CACHE.entrySet()){
|
||||
obj.put(entry.getKey().getHostAddress(),entry.getValue());
|
||||
}
|
||||
fos.write(obj.toString().getBytes());
|
||||
fos.close();
|
||||
}catch(Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
BIN
src/main/resources/earth.jpg
Normal file
BIN
src/main/resources/earth.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 MiB |
BIN
src/main/resources/favicon.ico
Normal file
BIN
src/main/resources/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
148
src/main/resources/index.html
Normal file
148
src/main/resources/index.html
Normal file
|
@ -0,0 +1,148 @@
|
|||
<!DOCTYPE html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta content="initial-scale=1,width=device-width" name="viewport" />
|
||||
<script src="//unpkg.com/globe.gl"></script>
|
||||
<style>
|
||||
body{
|
||||
font-family:Arial,sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
.info{
|
||||
color:white;
|
||||
padding:8px;
|
||||
position:absolute;
|
||||
left:0;
|
||||
top:0;
|
||||
}
|
||||
</style>
|
||||
<title>LBRY Globe</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="globe"></div>
|
||||
<div class="info">
|
||||
<div id="info-nodes-blockchain"></div>
|
||||
<div id="info-nodes-dht"></div>
|
||||
<div id="info-nodes-hub"></div>
|
||||
<div id="info-nodes-total"></div>
|
||||
</div>
|
||||
<script>
|
||||
const globe = Globe();
|
||||
|
||||
globe(document.getElementById('globe'))
|
||||
// .globeImageUrl('gebco_08_rev_elev_21600x10800.png')
|
||||
.globeImageUrl('earth.jpg')
|
||||
// .globeImageUrl('earth.png')
|
||||
// .globeImageUrl('//unpkg.com/three-globe@2.35.2/example/img/earth-night.jpg')
|
||||
// .globeImageUrl('//unpkg.com/three-globe/example/img/earth-dark.jpg')
|
||||
// .globeImageUrl('//unpkg.com/three-globe/example/img/earth-water.png')
|
||||
.bumpImageUrl('//unpkg.com/three-globe/example/img/earth-topology.png')
|
||||
.backgroundImageUrl('//unpkg.com/three-globe/example/img/night-sky.png');
|
||||
|
||||
globe.arcAltitude(0);
|
||||
globe.arcColor(d => {
|
||||
return [`rgba(0, 255, 0, 0.1)`, `rgba(255, 0, 0, 0.1)`];
|
||||
});
|
||||
globe.arcStroke(0.1);
|
||||
|
||||
globe.onArcHover(hoverArc => {
|
||||
globe.arcColor(d => {
|
||||
const op = !hoverArc ? 0.1 : d === hoverArc ? 0.9 : 0.1 / 4;
|
||||
return [`rgba(0, 255, 0, ${op})`, `rgba(255, 0, 0, ${op})`];
|
||||
});
|
||||
});
|
||||
|
||||
const POINT_ALTITUDE = {
|
||||
blockchain: 0.02,
|
||||
dht: 0.030,
|
||||
hub: 0.010,
|
||||
};
|
||||
|
||||
const POINT_COLOR = {
|
||||
blockchain: '#0000FF',
|
||||
dht: '#FFFF00',
|
||||
hub: '#FF0000',
|
||||
};
|
||||
|
||||
const POINT_RADIUS = {
|
||||
blockchain: 0.125,
|
||||
dht: 0.1,
|
||||
hub: 0.15,
|
||||
};
|
||||
|
||||
globe.pointAltitude(point => POINT_ALTITUDE[point.type]); // point.type==='hub'?0.015:(point.bootstrap?1:0.02
|
||||
globe.pointColor(point => POINT_COLOR[point.type]);
|
||||
globe.pointLabel(point => point.label);
|
||||
globe.pointRadius(point => POINT_RADIUS[point.type]); // point.bootstrap
|
||||
|
||||
globe.onZoom((x) => {globe.controls().zoomSpeed = 2;});
|
||||
|
||||
var data = null;
|
||||
|
||||
function shuffleArray(array) {
|
||||
for (var i = array.length - 1; i >= 0; i--) {
|
||||
var j = Math.floor(Math.random() * (i + 1));
|
||||
var temp = array[i];
|
||||
array[i] = array[j];
|
||||
array[j] = temp;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
function updatePointsData(points){
|
||||
var threeCache = {};
|
||||
|
||||
var oldPointsData = globe.pointsData();
|
||||
for(var i=0;i<oldPointsData.length;i++){
|
||||
threeCache[oldPointsData[i].id] = oldPointsData[i];
|
||||
}
|
||||
|
||||
var newPointsData = points;
|
||||
for(var i=0;i<newPointsData.length;i++){
|
||||
if(threeCache[newPointsData[i].id]){
|
||||
var newData = newPointsData[i];
|
||||
newPointsData[i] = threeCache[newPointsData[i].id];
|
||||
var newDataKeys = Object.keys(newData);
|
||||
for(var j=0;j<newDataKeys.length;j++){
|
||||
newPointsData[i][newDataKeys[j]] = newData[newDataKeys[j]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var blockchainCount = 0;
|
||||
var dhtCount = 0;
|
||||
var hubCount = 0;
|
||||
for(var i=0;i<newPointsData.length;i++){
|
||||
if(newPointsData[i].type==='blockchain'){
|
||||
blockchainCount++;
|
||||
}
|
||||
if(newPointsData[i].type==='dht'){
|
||||
dhtCount++;
|
||||
}
|
||||
if(newPointsData[i].type==='hub'){
|
||||
hubCount++;
|
||||
}
|
||||
}
|
||||
document.getElementById('info-nodes-blockchain').innerHTML = '<b>Blockchain Nodes:</b> '+blockchainCount;
|
||||
document.getElementById('info-nodes-dht').innerHTML = '<b>DHT Nodes:</b> '+dhtCount;
|
||||
document.getElementById('info-nodes-hub').innerHTML = '<b>Hub Nodes:</b> '+hubCount;
|
||||
document.getElementById('info-nodes-total').innerHTML = '<b>Total Nodes:</b> '+newPointsData.length;
|
||||
|
||||
globe.pointsData(shuffleArray(newPointsData));
|
||||
}
|
||||
|
||||
function updateGlobe(){
|
||||
fetch('/api')
|
||||
.then(resp => resp.json())
|
||||
.then(json => {
|
||||
data = json;
|
||||
updatePointsData(json.points);
|
||||
globe.arcsData(json.arcs);
|
||||
});
|
||||
}
|
||||
|
||||
setInterval(updateGlobe,1_000);
|
||||
updateGlobe();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
Add table
Reference in a new issue