package jelectrum; import java.util.TreeMap; import java.util.TreeSet; import java.util.Map; import java.net.InetAddress; import org.json.JSONObject; import org.json.JSONArray; import java.util.LinkedList; import java.util.Collections; import java.util.Scanner; import javax.net.ssl.SSLSocket; import javax.net.SocketFactory; import java.net.Proxy; import java.net.InetSocketAddress; import java.net.Socket; import java.io.PrintStream; public class PeerManager { private Jelectrum jelly; // protected by synchronized private TreeMap<String, PeerInfo> knownPeers; // immutable private final LinkedList<PeerInfo> selfPeers; // replaced all the damn time private volatile JSONArray lastPeerList; public static long RECHECK_TIME = 8L * 3600L* 1000L; //8 hours //public static long RECHECK_TIME = 3600 * 1000L; //1 hr public static long FORGET_TIME = 7L * 86400L * 1000L; //1 week private final String genesis_hash; public PeerManager(Jelectrum jelly) { this.jelly = jelly; genesis_hash = jelly.getNetworkParameters().getGenesisBlock().getHash().toString(); knownPeers = new TreeMap<>(); selfPeers = new LinkedList<>(); } public void start() { Config config = jelly.getConfig(); tryLoadPeerDB(); addPeer("h.1209k.com",50001,50002); addPeer("b.1209k.com",50001,50002); addPeer("electrum.vom-stausee.de",0,50002); addPeer("mashtk6hmnysevfj.onion",50001,0); addPeer("luggsqxihfzqjnwm.onion",443,0); addPeer("ecdsa.net", 0, 110); addPeer("electrum.hsmiths.com", 50001, 50002); if (config.isSet("advertise_host")) { for(String host : config.getList("advertise_host")) { try { PeerInfo self_info = new PeerInfo(); self_info.self_info=true; self_info.hostname = host; self_info.pruning = 0; self_info.protocol_min = "1.0"; self_info.protocol_max = StratumConnection.PROTO_VERSION; self_info.server_version = StratumConnection.PROTO_VERSION + "/j/" + StratumConnection.JELECTRUM_VERSION; self_info.updateAddress(); if (config.isSet("tcp_port")) { self_info.tcp_port = jelly.getStratumServer().getTcpPort(); } if (config.isSet("ssl_port")) { self_info.ssl_port = jelly.getStratumServer().getSslPort(); } synchronized(knownPeers) { knownPeers.put(self_info.getKey(), self_info); } selfPeers.add(self_info); } catch(Exception e) { jelly.getEventLog().alarm(e); } } } synchronized(knownPeers) { jelly.getEventLog().log("Known discovery peers: " + knownPeers.size()); } new PeerMaintThread().start(); } private void addPeer(String host, int tcp, int ssl) { PeerInfo peer = new PeerInfo(); peer.hostname = host; peer.tcp_port = tcp; peer.ssl_port = ssl; addPeerInternal(peer); } public JSONArray getPeers() { JSONArray peerList = lastPeerList; if (peerList == null) { peerList = updatePeerList(); } return peerList; } private JSONArray updatePeerList() { return lastPeerList = populatePeerSubscribe(); } private JSONArray populatePeerSubscribe() { JSONArray result_array = new JSONArray(); synchronized(knownPeers) { for(PeerInfo peer : knownPeers.values()) { if (peer.include()) { JSONArray peer_array = new JSONArray(); peer_array.put(peer.lastAddress); peer_array.put(peer.hostname); JSONArray info_array = new JSONArray(); info_array.put("v" + peer.protocol_max); if (peer.pruning > 0) { info_array.put("p" + peer.pruning); } if (peer.tcp_port > 0) { info_array.put("t" + peer.tcp_port); } if (peer.ssl_port > 0) { info_array.put("s" + peer.ssl_port); } peer_array.put(info_array); result_array.put(peer_array); } } } return result_array; } public JSONObject getServerFeatures() throws org.json.JSONException { JSONObject result = new JSONObject(); PeerInfo peer = null; for(PeerInfo p : selfPeers) { peer = p; } if (peer != null) { result.put("genesis_hash", genesis_hash); JSONObject hostArray = new JSONObject(); for(PeerInfo p : selfPeers) { JSONObject h = new JSONObject(); JSONObject h2 = new JSONObject(); if (p.tcp_port > 0) { h2.put("tcp_port", p.tcp_port); } if (p.ssl_port > 0) { h2.put("ssl_port", p.ssl_port); } hostArray.put(p.hostname, h2); } result.put("hosts", hostArray); result.put("protocol_max", peer.protocol_max); result.put("protocol_min", peer.protocol_min); result.put("server_version", peer.server_version); } return result; } public void addPeers(JSONArray params) throws org.json.JSONException { for(int i=0; i<params.length(); i++) { JSONObject features = params.getJSONObject(i); JSONObject hosts = features.getJSONObject("hosts"); for(String hostname : JSONObject.getNames(hosts)) { JSONObject host = hosts.getJSONObject(hostname); PeerInfo peer = new PeerInfo(); peer.hostname = hostname; if (host.has("tcp_port")) { peer.tcp_port = host.getInt("tcp_port"); } if (host.has("ssl_port")) { peer.ssl_port = host.getInt("ssl_port"); } addPeerInternal(peer); } } } private void addPeerInternal(PeerInfo peer) { String key = peer.getKey(); synchronized(knownPeers) { if (!knownPeers.containsKey(key)) { knownPeers.put(key, peer); jelly.getEventLog().log("Learned new discovery peer: " + key); } else { knownPeers.get(key).learned_time = System.currentTimeMillis(); } } } private void savePeerDB() { synchronized(knownPeers) { jelly.getDB().getSpecialObjectMap().put("PeerManager_PeerDB", knownPeers); } } private void tryLoadPeerDB() { try { TreeMap<String, PeerInfo> dbPeers = (TreeMap<String, PeerInfo>)jelly.getDB().getSpecialObjectMap().get("PeerManager_PeerDB"); synchronized(knownPeers) { knownPeers.putAll(dbPeers); } } catch(Throwable t) { jelly.getEventLog().log("Error loading peer db. Starting fresh: " + t.toString()); } } public class PeerMaintThread extends Thread { public PeerMaintThread() { setDaemon(true); setName("PeerMaintThread"); } public void run() { while(true) { try { Thread.sleep(10000L); runPass(); } catch(Throwable t) { jelly.getEventLog().alarm(t); } } } private void runPass() { boolean changes=false; TreeSet<String> deleteList=new TreeSet<>(); LinkedList<PeerInfo> checkList = new LinkedList<>(); synchronized(knownPeers) { for(Map.Entry<String, PeerInfo> me : knownPeers.entrySet()) { String key = me.getKey(); PeerInfo peer = me.getValue(); if (peer.shouldDelete()) deleteList.add(key); } for(String key : deleteList) { jelly.getEventLog().log("Removing stale discovery peer " + key); knownPeers.remove(key); changes=true; } for(Map.Entry<String, PeerInfo> me : knownPeers.entrySet()) { String key = me.getKey(); PeerInfo peer = me.getValue(); if (peer.shouldCheck()) { checkList.add(peer); } } } Collections.shuffle(checkList, new java.util.Random()); if (checkList.size() > 0) { jelly.getEventLog().log("Discovery peers that need to be checked: " + checkList.size()); PeerInfo peer = checkList.get(0); checkPeer(peer); changes=true; } if (changes) { savePeerDB(); lastPeerList=null; } } } private void checkPeer(PeerInfo peer) { jelly.getEventLog().log("Checking discovery peer: " + peer.getKey()); try { peer.last_checked = System.currentTimeMillis(); if (checkPeerInternal(peer)) { peer.last_passed = System.currentTimeMillis(); jelly.getEventLog().log("Peer check passed: " + peer.getKey()); return; } } catch(Throwable t) { jelly.getEventLog().log("Exception in peer check: " + t.toString()); } jelly.getEventLog().log("Peer check failed: " + peer.getKey()); } private boolean checkPeerInternal(PeerInfo peer) throws Exception { peer.updateAddress(); Socket sock = openSocket(peer); sock.setTcpNoDelay(true); sock.setSoTimeout(15000); Scanner scan = new Scanner(sock.getInputStream()); PrintStream out = new PrintStream(sock.getOutputStream()); JSONObject serverfeatures = getServerFeatures(out, scan); String hash = serverfeatures.getString("genesis_hash"); if(!genesis_hash.equals(hash)) { throw new Exception("Expected " + genesis_hash + " got " + hash); } peer.protocol_max = serverfeatures.getString("protocol_max"); peer.protocol_min = serverfeatures.getString("protocol_min"); peer.server_version = serverfeatures.getString("server_version"); if (serverfeatures.isNull("pruning")) peer.pruning = 0; else { peer.pruning = serverfeatures.getInt("pruning"); } addRemotePeers(out, scan); addSelfToPeer(out, scan); sock.close(); return true; } private JSONObject getServerFeatures(PrintStream out, Scanner scan) throws org.json.JSONException { JSONObject request = new JSONObject(); request.put("id","server_req"); request.put("method", "server.features"); request.put("params",new JSONArray()); out.println(request.toString(0)); out.flush(); JSONObject reply = new JSONObject(scan.nextLine()); return reply.getJSONObject("result"); } private void addRemotePeers(PrintStream out, Scanner scan) throws org.json.JSONException { JSONObject request = new JSONObject(); request.put("id","get_peer"); request.put("method", "server.peers.subscribe"); request.put("params",new JSONArray()); out.println(request.toString(0)); out.flush(); JSONObject reply = new JSONObject(scan.nextLine()); JSONArray peerList = reply.getJSONArray("result"); for(int i=0; i<peerList.length(); i++) { JSONArray p = peerList.getJSONArray(i); String host = p.getString(1); JSONArray param = p.getJSONArray(2); int tcp_port=0; int ssl_port=0; for(int j=0; j<param.length(); j++) { String str = param.getString(j); if (str.startsWith("s")) { ssl_port = Integer.parseInt(str.substring(1)); } if (str.startsWith("t")) { tcp_port = Integer.parseInt(str.substring(1)); } } if (tcp_port + ssl_port > 0) { addPeer(host, tcp_port, ssl_port); } } } private void addSelfToPeer(PrintStream out, Scanner scan) throws org.json.JSONException { JSONArray peersToAdd = new JSONArray(); peersToAdd.put(getServerFeatures()); JSONObject request = new JSONObject(); request.put("id","add_peer"); request.put("method", "server.add_peer"); request.put("params", peersToAdd); out.println(request.toString(0)); out.flush(); //JSONObject reply = new JSONObject(scan.nextLine()); //jelly.getEventLog().log(reply.toString()); } private Socket openSocket(PeerInfo peer) throws Exception { Socket sock = null; if (peer.hostname.endsWith(".onion")) { int port = peer.tcp_port; if (port == 0) throw new Exception("Can only use TCP with onion"); Proxy p = new Proxy(Proxy.Type.SOCKS, new InetSocketAddress("localhost", 9050)); sock = new Socket(p); sock.connect(new InetSocketAddress(peer.hostname, port)); return sock; } if (peer.ssl_port != 0) { int port = peer.ssl_port; SocketFactory sock_factory = new TrustEraser().getFactory(); SSLSocket ssl_sock = (SSLSocket)sock_factory.createSocket(peer.hostname, port); ssl_sock.startHandshake(); sock = ssl_sock; return sock; } if (peer.tcp_port != 0) { sock = new Socket(peer.hostname, peer.tcp_port); return sock; } throw new Exception("Unable to find connection method"); } }