Initial commit

This commit is contained in:
Ben van Hartingsveldt 2024-11-24 19:21:31 +01:00
commit 5c1381bdb1
No known key found for this signature in database
GPG key ID: 261AA214130CE7AB
18 changed files with 1189 additions and 0 deletions

42
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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>

View 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();
}
}

View 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);
}
}
}
}

View 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);
}
}

View 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);
}
}

View 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;
}
}

View 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;
}
}

View file

@ -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);
}
}
}

View 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);
}
}

View 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);
}
}
}
}

View 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();
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View 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>