commit 5c1381bdb1ad8f1b5e922a3ce03d83ecb1e6e3f3 Author: Ben van Hartingsveldt Date: Sun Nov 24 19:21:31 2024 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ef2c5c8 --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..611dcd3 --- /dev/null +++ b/Dockerfile @@ -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"] \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..6bd3776 --- /dev/null +++ b/LICENSE.md @@ -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. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..449f3bd --- /dev/null +++ b/README.md @@ -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). \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..3ee4f8b --- /dev/null +++ b/pom.xml @@ -0,0 +1,75 @@ + + + + 4.0.0 + + org.lbry + lbry-globe + 1.0.0-SNAPSHOT + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.3.0 + + + + com.lbry.globe.Main + + + true + + + + jar-with-dependencies + + + + + package + + single + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 8 + 8 + + + + + + + + com.dampcake + bencode + 1.4.1 + + + io.netty + netty-all + 4.1.115.Final + + + org.json + json + 20240303 + + + + + 21 + 21 + UTF-8 + + + \ No newline at end of file diff --git a/src/main/java/com/lbry/globe/Main.java b/src/main/java/com/lbry/globe/Main.java new file mode 100644 index 0000000..569d737 --- /dev/null +++ b/src/main/java/com/lbry/globe/Main.java @@ -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(){ + 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(); + } + +} \ No newline at end of file diff --git a/src/main/java/com/lbry/globe/api/API.java b/src/main/java/com/lbry/globe/api/API.java new file mode 100644 index 0000000..29641b0 --- /dev/null +++ b/src/main/java/com/lbry/globe/api/API.java @@ -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 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); + } + } + } + +} diff --git a/src/main/java/com/lbry/globe/handler/HTTPHandler.java b/src/main/java/com/lbry/globe/handler/HTTPHandler.java new file mode 100644 index 0000000..c1a65a4 --- /dev/null +++ b/src/main/java/com/lbry/globe/handler/HTTPHandler.java @@ -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 ATTR_REQUEST = AttributeKey.newInstance("request"); + public static final AttributeKey> 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 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); + } + +} \ No newline at end of file diff --git a/src/main/java/com/lbry/globe/logging/LogLevel.java b/src/main/java/com/lbry/globe/logging/LogLevel.java new file mode 100644 index 0000000..5ccfd90 --- /dev/null +++ b/src/main/java/com/lbry/globe/logging/LogLevel.java @@ -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); + } + +} diff --git a/src/main/java/com/lbry/globe/object/Node.java b/src/main/java/com/lbry/globe/object/Node.java new file mode 100644 index 0000000..ceff014 --- /dev/null +++ b/src/main/java/com/lbry/globe/object/Node.java @@ -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 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 getServices(){ + return this.services; + } + +} \ No newline at end of file diff --git a/src/main/java/com/lbry/globe/object/Service.java b/src/main/java/com/lbry/globe/object/Service.java new file mode 100644 index 0000000..1736132 --- /dev/null +++ b/src/main/java/com/lbry/globe/object/Service.java @@ -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; + } + +} \ No newline at end of file diff --git a/src/main/java/com/lbry/globe/thread/BlockchainNodeFinderThread.java b/src/main/java/com/lbry/globe/thread/BlockchainNodeFinderThread.java new file mode 100644 index 0000000..1cb0a08 --- /dev/null +++ b/src/main/java/com/lbry/globe/thread/BlockchainNodeFinderThread.java @@ -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 data = new TreeMap<>(Comparator.comparing(InetAddress::getHostAddress)); + for(int i=0;i60_000){ + s.markedForRemoval = true; + } + } + } + + for(Map.Entry 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 removedService = new ArrayList<>(); + for(Service s : n.getServices()){ + if(s.markedForRemoval && "blockchain".equals(s.getType())){ + removedService.add(s); + } + } + n.getServices().removeAll(removedService); + } + } + +} diff --git a/src/main/java/com/lbry/globe/thread/DHTNodeFinderThread.java b/src/main/java/com/lbry/globe/thread/DHTNodeFinderThread.java new file mode 100644 index 0000000..f14134c --- /dev/null +++ b/src/main/java/com/lbry/globe/thread/DHTNodeFinderThread.java @@ -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 pingableDHTs = new ConcurrentHashMap<>(); + + private final Queue 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 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> nodes = (List>) receivingDictionary.get("3"); + for(List 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 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 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 map){ + return DHTNodeFinderThread.BENCODE.encode(map); + } + + private static Map decodePacket(byte[] bytes){ + // Fix invalid B-encoding + if(bytes[0]=='d'){ + bytes[0] = 'l'; + } + List list = DHTNodeFinderThread.BENCODE.decode(bytes,Type.LIST); + for(int i=0;i 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 removeIPs = new ArrayList<>(); + for(Map.Entry 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 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 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); + } + } + } + +} diff --git a/src/main/java/com/lbry/globe/util/GeoIP.java b/src/main/java/com/lbry/globe/util/GeoIP.java new file mode 100644 index 0000000..9e29a3b --- /dev/null +++ b/src/main/java/com/lbry/globe/util/GeoIP.java @@ -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 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 entry : GeoIP.CACHE.entrySet()){ + obj.put(entry.getKey().getHostAddress(),entry.getValue()); + } + fos.write(obj.toString().getBytes()); + fos.close(); + }catch(Exception e){ + e.printStackTrace(); + } + } + +} \ No newline at end of file diff --git a/src/main/resources/earth.jpg b/src/main/resources/earth.jpg new file mode 100644 index 0000000..264d686 Binary files /dev/null and b/src/main/resources/earth.jpg differ diff --git a/src/main/resources/favicon.ico b/src/main/resources/favicon.ico new file mode 100644 index 0000000..91cdf39 Binary files /dev/null and b/src/main/resources/favicon.ico differ diff --git a/src/main/resources/index.html b/src/main/resources/index.html new file mode 100644 index 0000000..e6a5167 --- /dev/null +++ b/src/main/resources/index.html @@ -0,0 +1,148 @@ + + + + + + + LBRY Globe + + +
+
+
+
+
+
+
+ + + \ No newline at end of file