// yacyPeerActions.java // ------------------------------------- // (C) by Michael Peter Christen; mc@yacy.net // first published on http://yacy.net // Frankfurt, Germany, 2005 // // $LastChangedDate$ // $LastChangedRevision$ // $LastChangedBy$ // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA package net.yacy.peers; import java.util.Map; import net.yacy.cora.document.encoding.ASCII; import net.yacy.cora.document.feed.RSSMessage; import net.yacy.cora.storage.ConcurrentARC; import net.yacy.kelondro.util.MapTools; public class PeerActions { private final SeedDB seedDB; private Map<String, String> userAgents; private final NewsPool newsPool; public PeerActions(final SeedDB seedDB, final NewsPool newsPool) { this.seedDB = seedDB; this.newsPool = newsPool; this.userAgents = new ConcurrentARC<String, String>(10000, Runtime.getRuntime().availableProcessors() + 1); } public void close() { // the seedDB and newsPool should be cleared elsewhere if (this.userAgents != null) this.userAgents.clear(); this.userAgents = null; } public boolean connectPeer(final Seed seed, final boolean direct) { // store a remote peer's seed // returns true if the peer is new and previously unknown if (seed == null) { Network.log.severe("connect: WRONG seed (NULL)"); return false; } final String error = seed.isProper(false); if (error != null) { Network.log.severe("connect: WRONG seed (" + seed.getName() + "/" + seed.hash + "): " + error); return false; } if ((this.seedDB.mySeedIsDefined()) && (seed.hash.equals(this.seedDB.mySeed().hash))) { Network.log.info("connect: SELF reference " + seed.getIPs()); return false; } final String peerType = seed.get(Seed.PEERTYPE, Seed.PEERTYPE_VIRGIN); if ((peerType.equals(Seed.PEERTYPE_VIRGIN)) || (peerType.equals(Seed.PEERTYPE_JUNIOR))) { // reject unqualified seeds if (Network.log.isFine()) Network.log.fine("connect: rejecting NOT QUALIFIED " + peerType + " seed " + seed.getName()); return false; } if (!(peerType.equals(Seed.PEERTYPE_SENIOR) || peerType.equals(Seed.PEERTYPE_PRINCIPAL))) { // reject unqualified seeds if (Network.log.isFine()) Network.log.fine("connect: rejecting NOT QUALIFIED " + peerType + " seed " + seed.getName()); return false; } final Seed doubleSeed = this.seedDB.lookupByIPs(seed.getIPs(), seed.getPort(), true, false, false); if ((doubleSeed != null) && (doubleSeed.getPort() == seed.getPort()) && (!(doubleSeed.hash.equals(seed.hash)))) { // a user frauds with his peer different peer hashes if (Network.log.isFine()) Network.log.fine("connect: rejecting FRAUD (double hashes " + doubleSeed.hash + "/" + seed.hash + " on same port " + seed.getPort() + ") peer " + seed.getName()); return false; } if (seed.get(Seed.LASTSEEN, "").length() != 14) { // hack for peers that do not have a LastSeen date seed.setLastSeenUTC(); if (Network.log.isFine()) Network.log.fine("connect: reset wrong date (" + seed.getName() + "/" + seed.hash + ")"); } // connection time final long nowUTC0Time = System.currentTimeMillis(); // is better to have this value in a variable for debugging long ctimeUTC0 = seed.getLastSeenUTC(); if (ctimeUTC0 > nowUTC0Time) { // the peer is future-dated, correct it seed.setLastSeenUTC(); ctimeUTC0 = nowUTC0Time; assert (seed.getLastSeenUTC() - ctimeUTC0 < 100); } if (Math.abs(nowUTC0Time - ctimeUTC0) / 1000 / 60 > 1440 ) { // the new connection is out-of-age, we reject the connection if (Network.log.isFine()) Network.log.info("connect: rejecting out-dated peer '" + seed.getName() + "' from " + seed.getIPs() + "; nowUTC0=" + nowUTC0Time + ", seedUTC0=" + ctimeUTC0 + ", TimeDiff=" + formatInterval(Math.abs(nowUTC0Time - ctimeUTC0))); return false; } final Seed disconnectedSeed = this.seedDB.getDisconnected(seed.hash); if (direct) { // remember the moment // Date applies the local UTC offset, which is wrong // we correct that by subtracting the local offset and adding // the remote offset. seed.setLastSeenUTC(); seed.setFlagDirectConnect(true); } else { // set connection flag if (Math.abs(nowUTC0Time - ctimeUTC0) > 120000) seed.setFlagDirectConnect(false); // 2 minutes } // prepare to update if (disconnectedSeed != null) { // if the indirect connect aims to announce a peer that we know // has been disconnected then we compare the dates: // if the new peer has a LastSeen date, and that date is before // the disconnection date, then we ignore the new peer /* if (!direct) { if (ctimeUTC0 < dtimeUTC0) { // the disconnection was later, we reject the connection if (Network.log.isFine()) Network.log.fine("connect: rejecting disconnected peer '" + seed.getName() + "' from " + seed.getIPs()); return false; } } */ // this is a return of a lost peer if (Network.log.isFine()) Network.log.fine("connect: returned KNOWN " + peerType + " peer '" + seed.getName() + "' from " + seed.getIPs()); this.seedDB.addConnected(seed); return true; } final Seed connectedSeed = this.seedDB.getConnected(seed.hash); if (connectedSeed != null) { // the seed is known: this is an update try { // if the old LastSeen date is later then the other // info, then we reject the info if ((ctimeUTC0 < (connectedSeed.getLastSeenUTC())) && (!direct)) { if (Network.log.isFine()) Network.log.fine("connect: rejecting old info about peer '" + seed.getName() + "'"); return false; } /*if (connectedSeed.getName() != seed.getName()) { // TODO: update seed name lookup cache }*/ } catch (final NumberFormatException e) { if (Network.log.isFine()) Network.log.fine("connect: rejecting wrong peer '" + seed.getName() + "' from " + seed.getIPs() + ". Cause: " + e.getMessage()); return false; } if (Network.log.isFine()) Network.log.fine("connect: updated KNOWN " + ((direct) ? "direct " : "") + peerType + " peer '" + seed.getName() + "' from " + seed.getIPs()); this.seedDB.addConnected(seed); return true; } // the seed is new if ((this.seedDB.mySeedIsDefined()) && (seed.clash(this.seedDB.mySeed().getIPs()))) { // seed from the same IP as the calling client: can be // the case if there runs another one over a NAT if (Network.log.isFine()) Network.log.fine("connect: saved NEW seed (myself IP) " + seed.getIPs()); } else { // completely new seed if (Network.log.isFine()) Network.log.fine("connect: saved NEW " + peerType + " peer '" + seed.getName() + "' from " + seed.getIPs()); } this.seedDB.addConnected(seed); return true; } public boolean peerArrival(final Seed peer, final boolean direct) { if (peer == null) return false; final boolean res = connectPeer(peer, direct); if (res) { // perform all actions if peer is effective new processPeerArrival(peer); EventChannel.channels(EventChannel.PEERNEWS).addMessage(new RSSMessage(peer.getName() + " joined the network", "", "")); } return res; } /** * If any of the peer2peer communication attempts fail, then remove the tested IP from the peer by calling this method. * if the given IP is the only one which is remaining, then the IP is NOT removed from the peer but the peer is removed from the * active list of peers instead. That means when a peer arrives in the deactivated peer list, then it has at least one IP left * which should be actually the latest IP where the peer was accessible. * @param peer * @param ip */ public void interfaceDeparture(final Seed peer, String ip) { if (peer == null) return; if (Network.log.isFine()) Network.log.fine("connect: no contact to a interface from " + peer.get(Seed.PEERTYPE, Seed.PEERTYPE_VIRGIN) + " peer '" + peer.getName() + "' at " + ip); synchronized (this.seedDB) { if (this.seedDB.hasConnected(ASCII.getBytes(peer.hash))) { if (peer.countIPs() > 1) { if (peer.removeIP(ip)) { this.seedDB.updateConnected(peer); } else { // this is bad because the IP does not appear at all in the seed. We consider the seed as poisoned and remove it from the active peers this.seedDB.addDisconnected(peer); } } else { // disconnect the peer anyway peer.put(Seed.DCT, Long.toString(System.currentTimeMillis())); this.seedDB.addDisconnected(peer); } } } EventChannel.channels(EventChannel.PEERNEWS).addMessage(new RSSMessage(peer.getName() + " interface not available: " + ip, "", "")); } /** * PeerDeparture marks a peers as not available. Because with IPv6 we have more than one IP, we first mark single IPs as not available instead of marking the whole peer. * Therefore this method is deprecated. Please use interfaceDeparture instead. * @param peer * @param cause */ @Deprecated public void peerDeparture(final Seed peer, final String cause) { if (peer == null) return; // we do this if we did not get contact with the other peer if (Network.log.isFine()) Network.log.fine("connect: no contact to a " + peer.get(Seed.PEERTYPE, Seed.PEERTYPE_VIRGIN) + " peer '" + peer.getName() + "' at " + peer.getIPs() + ". Cause: " + cause); synchronized (this.seedDB) { peer.put(Seed.DCT, Long.toString(System.currentTimeMillis())); this.seedDB.addDisconnected(peer); // update info } EventChannel.channels(EventChannel.PEERNEWS).addMessage(new RSSMessage(peer.getName() + " left the network", "", "")); } public void peerPing(final Seed peer) { if (peer == null) return; // this is called only if the peer has junior status this.seedDB.addPotential(peer); // perform all actions processPeerArrival(peer); EventChannel.channels(EventChannel.PEERNEWS).addMessage(new RSSMessage(peer.getName() + " sent me a ping", "", "")); } private void processPeerArrival(final Seed peer) { final String recordString = peer.get(Seed.NEWS, null); //System.out.println("### triggered news arrival from peer " + peer.getName() + ", news " + ((recordString == null) ? "empty" : "attached")); if ((recordString == null) || (recordString.isEmpty())) return; final String decodedString = net.yacy.utils.crypt.simpleDecode(recordString); final NewsDB.Record record = this.newsPool.parseExternal(decodedString); if (record != null) { //System.out.println("### news arrival from peer " + peer.getName() + ", decoded=" + decodedString + ", record=" + recordString + ", news=" + record.toString()); final String cre1 = MapTools.string2map(decodedString, ",").get("cre"); final String cre2 = MapTools.string2map(record.toString(), ",").get("cre"); if ((cre1 == null) || (cre2 == null) || (!(cre1.equals(cre2)))) { Network.log.warn("processPeerArrival: ### ERROR - message creation date verification not equal: cre1=" + cre1 + ", cre2=" + cre2); return; } try { synchronized (this.newsPool) {this.newsPool.enqueueIncomingNews(record);} } catch (final Exception e) { Network.log.severe("processPeerArrival", e); } } } public int sizeConnected() { return this.seedDB.sizeConnected(); } public void setUserAgent(final String IP, final String userAgent) { if (this.userAgents == null) return; // case can happen during shutdown this.userAgents.put(IP, userAgent); } public String getUserAgent(final String IP) { final String userAgent = this.userAgents.get(IP); return (userAgent == null) ? "" : userAgent; } /** * Format a time inteval in milliseconds into a String of the form * X 'day'['s'] HH':'mm */ public static String formatInterval(final long millis) { try { final long mins = millis / 60000; final StringBuilder uptime = new StringBuilder(40); final int uptimeDays = (int) (Math.floor(mins/1440.0)); final int uptimeHours = (int) (Math.floor(mins/60.0)%24); final int uptimeMins = (int) mins%60; uptime.append(uptimeDays) .append(((uptimeDays == 1)?" day ":" days ")) .append((uptimeHours < 10)?"0":"") .append(uptimeHours) .append(':') .append((uptimeMins < 10)?"0":"") .append(uptimeMins); return uptime.toString(); } catch (final Exception e) { return "unknown"; } } }