/****************************************************************************** * Copyright © 2013-2016 The Nxt Core Developers. * * * * See the AUTHORS.txt, DEVELOPER-AGREEMENT.txt and LICENSE.txt files at * * the top-level directory of this distribution for the individual copyright * * holder information and the developer policies on copyright and licensing. * * * * Unless otherwise agreed in a custom licensing agreement, no part of the * * Nxt software, including this file, may be copied, modified, propagated, * * or distributed except according to the terms contained in the LICENSE.txt * * file. * * * * Removal or modification of this copyright notice is prohibited. * * * ******************************************************************************/ package nxt.peer; import nxt.Account; import nxt.BlockchainProcessor; import nxt.Constants; import nxt.Nxt; import nxt.NxtException; import nxt.util.Convert; import nxt.util.CountingInputReader; import nxt.util.CountingInputStream; import nxt.util.CountingOutputWriter; import nxt.util.JSON; import nxt.util.Logger; import org.json.simple.JSONObject; import org.json.simple.JSONStreamAware; import org.json.simple.JSONValue; import org.json.simple.parser.ParseException; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.StringWriter; import java.io.Writer; import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.SocketException; import java.net.SocketTimeoutException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.UnknownHostException; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import java.util.zip.GZIPInputStream; final class PeerImpl implements Peer { private final String host; private final PeerWebSocket webSocket; private volatile PeerWebSocket inboundSocket; private volatile boolean useWebSocket; private volatile String announcedAddress; private volatile int port; private volatile boolean shareAddress; private volatile Hallmark hallmark; private volatile String platform; private volatile String application; private volatile int apiPort; private volatile int apiSSLPort; private volatile String version; private volatile boolean isOldVersion; private volatile long adjustedWeight; private volatile int blacklistingTime; private volatile String blacklistingCause; private volatile State state; private volatile long downloadedVolume; private volatile long uploadedVolume; private volatile int lastUpdated; private volatile int lastConnectAttempt; private volatile int lastInboundRequest; private volatile long hallmarkBalance = -1; private volatile int hallmarkBalanceHeight; private volatile long services; PeerImpl(String host, String announcedAddress) { this.host = host; this.announcedAddress = announcedAddress; try { this.port = new URI("http://" + announcedAddress).getPort(); } catch (URISyntaxException ignore) {} this.state = State.NON_CONNECTED; this.shareAddress = true; this.webSocket = new PeerWebSocket(); this.useWebSocket = Peers.useWebSockets && !Peers.useProxy; } @Override public String getHost() { return host; } @Override public State getState() { return state; } void setState(State state) { if (state != State.CONNECTED) webSocket.close(); if (this.state == state) { return; } if (this.state == State.NON_CONNECTED) { this.state = state; Peers.notifyListeners(this, Peers.Event.ADDED_ACTIVE_PEER); } else if (state != State.NON_CONNECTED) { this.state = state; Peers.notifyListeners(this, Peers.Event.CHANGED_ACTIVE_PEER); } else { this.state = state; } } @Override public long getDownloadedVolume() { return downloadedVolume; } void updateDownloadedVolume(long volume) { synchronized (this) { downloadedVolume += volume; } Peers.notifyListeners(this, Peers.Event.DOWNLOADED_VOLUME); } @Override public long getUploadedVolume() { return uploadedVolume; } void updateUploadedVolume(long volume) { synchronized (this) { uploadedVolume += volume; } Peers.notifyListeners(this, Peers.Event.UPLOADED_VOLUME); } @Override public String getVersion() { return version; } void setVersion(String version) { if (version != null && version.length() > Peers.MAX_VERSION_LENGTH) { throw new IllegalArgumentException("Invalid version length: " + version.length()); } boolean versionChanged = version == null || !version.equals(this.version); this.version = version; isOldVersion = false; if (Nxt.APPLICATION.equals(application)) { String[] versions; if (version == null || (versions = version.split("\\.")).length < Constants.MIN_VERSION.length) { isOldVersion = true; } else { for (int i = 0; i < Constants.MIN_VERSION.length; i++) { try { int v = Integer.parseInt(versions[i]); if (v > Constants.MIN_VERSION[i]) { isOldVersion = false; break; } else if (v < Constants.MIN_VERSION[i]) { isOldVersion = true; break; } } catch (NumberFormatException e) { isOldVersion = true; break; } } } if (isOldVersion) { if (versionChanged) { Logger.logDebugMessage(String.format("Blacklisting %s version %s", host, version)); } blacklistingCause = "Old version: " + version; lastInboundRequest = 0; setState(State.NON_CONNECTED); Peers.notifyListeners(this, Peers.Event.BLACKLIST); } } } @Override public String getApplication() { return application; } void setApplication(String application) { if (application == null || application.length() > Peers.MAX_APPLICATION_LENGTH) { throw new IllegalArgumentException("Invalid application"); } this.application = application; } @Override public String getPlatform() { return platform; } void setPlatform(String platform) { if (platform != null && platform.length() > Peers.MAX_PLATFORM_LENGTH) { throw new IllegalArgumentException("Invalid platform length: " + platform.length()); } this.platform = platform; } @Override public String getSoftware() { return Convert.truncate(application, "?", 10, false) + " (" + Convert.truncate(version, "?", 10, false) + ")" + " @ " + Convert.truncate(platform, "?", 10, false); } @Override public int getApiPort() { return apiPort; } void setApiPort(Object apiPortValue) { if (apiPortValue != null) { try { apiPort = ((Long)apiPortValue).intValue(); } catch (RuntimeException e) { throw new IllegalArgumentException("Invalid peer apiPort " + apiPortValue); } } } public int getApiSSLPort() { return apiSSLPort; } void setApiSSLPort(Object apiSSLPortValue) { if (apiSSLPortValue != null) { try { apiSSLPort = ((Long)apiSSLPortValue).intValue(); } catch (RuntimeException e) { throw new IllegalArgumentException("Invalid peer apiSSLPort " + apiSSLPortValue); } } } @Override public boolean shareAddress() { return shareAddress; } void setShareAddress(boolean shareAddress) { this.shareAddress = shareAddress; } @Override public String getAnnouncedAddress() { return announcedAddress; } void setAnnouncedAddress(String announcedAddress) { if (announcedAddress != null && announcedAddress.length() > Peers.MAX_ANNOUNCED_ADDRESS_LENGTH) { throw new IllegalArgumentException("Announced address too long: " + announcedAddress.length()); } this.announcedAddress = announcedAddress; if (announcedAddress != null) { try { this.port = new URI("http://" + announcedAddress).getPort(); } catch (URISyntaxException e) { this.port = -1; } } else { this.port = -1; } } @Override public int getPort() { return port <= 0 ? Peers.getDefaultPeerPort() : port; } @Override public Hallmark getHallmark() { return hallmark; } @Override public int getWeight() { if (hallmark == null) { return 0; } if (hallmarkBalance == -1 || hallmarkBalanceHeight < Nxt.getBlockchain().getHeight() - 60) { long accountId = hallmark.getAccountId(); Account account = Account.getAccount(accountId); hallmarkBalance = account == null ? 0 : account.getBalanceNQT(); hallmarkBalanceHeight = Nxt.getBlockchain().getHeight(); } return (int)(adjustedWeight * (hallmarkBalance / Constants.ONE_NXT) / Constants.MAX_BALANCE_NXT); } @Override public boolean isBlacklisted() { return blacklistingTime > 0 || isOldVersion || Peers.knownBlacklistedPeers.contains(host) || (announcedAddress != null && Peers.knownBlacklistedPeers.contains(announcedAddress)); } @Override public void blacklist(Exception cause) { if (cause instanceof NxtException.NotCurrentlyValidException || cause instanceof BlockchainProcessor.BlockOutOfOrderException || cause instanceof SQLException || cause.getCause() instanceof SQLException) { // don't blacklist peers just because a feature is not yet enabled, or because of database timeouts // prevents erroneous blacklisting during loading of blockchain from scratch return; } if (cause instanceof ParseException && Errors.END_OF_FILE.equals(cause.toString())) { return; } if (! isBlacklisted()) { if (cause instanceof IOException || cause instanceof ParseException || cause instanceof IllegalArgumentException) { Logger.logDebugMessage("Blacklisting " + host + " because of: " + cause.toString()); } else { Logger.logDebugMessage("Blacklisting " + host + " because of: " + cause.toString(), cause); } } blacklist(cause.toString() == null || Peers.hideErrorDetails ? cause.getClass().getName() : cause.toString()); } @Override public void blacklist(String cause) { blacklistingTime = Nxt.getEpochTime(); blacklistingCause = cause; setState(State.NON_CONNECTED); lastInboundRequest = 0; Peers.notifyListeners(this, Peers.Event.BLACKLIST); } @Override public void unBlacklist() { if (blacklistingTime == 0 ) { return; } Logger.logDebugMessage("Unblacklisting " + host); setState(State.NON_CONNECTED); blacklistingTime = 0; blacklistingCause = null; Peers.notifyListeners(this, Peers.Event.UNBLACKLIST); } void updateBlacklistedStatus(int curTime) { if (blacklistingTime > 0 && blacklistingTime + Peers.blacklistingPeriod <= curTime) { unBlacklist(); } if (isOldVersion && lastUpdated < curTime - 3600) { isOldVersion = false; } } @Override public void deactivate() { if (state == State.CONNECTED) { setState(State.DISCONNECTED); } else { setState(State.NON_CONNECTED); } Peers.notifyListeners(this, Peers.Event.DEACTIVATE); } @Override public void remove() { webSocket.close(); Peers.removePeer(this); Peers.notifyListeners(this, Peers.Event.REMOVE); } @Override public int getLastUpdated() { return lastUpdated; } void setLastUpdated(int lastUpdated) { this.lastUpdated = lastUpdated; } @Override public boolean isInbound() { return lastInboundRequest != 0; } int getLastInboundRequest() { return lastInboundRequest; } void setLastInboundRequest(int now) { lastInboundRequest = now; } void setInboundWebSocket(PeerWebSocket inboundSocket) { this.inboundSocket = inboundSocket; } @Override public boolean isInboundWebSocket() { PeerWebSocket s; return ((s=inboundSocket) != null && s.isOpen()); } @Override public boolean isOutboundWebSocket() { return webSocket.isOpen(); } @Override public String getBlacklistingCause() { return blacklistingCause == null ? "unknown" : blacklistingCause; } @Override public int getLastConnectAttempt() { return lastConnectAttempt; } @Override public JSONObject send(final JSONStreamAware request) { return send(request, Peers.MAX_RESPONSE_SIZE); } @Override public JSONObject send(final JSONStreamAware request, int maxResponseSize) { JSONObject response = null; String log = null; boolean showLog = false; HttpURLConnection connection = null; int communicationLoggingMask = Peers.communicationLoggingMask; try { // // Create a new WebSocket session if we don't have one // if (useWebSocket && !webSocket.isOpen()) useWebSocket = webSocket.startClient(URI.create("ws://" + host + ":" + getPort() + "/nxt")); // // Send the request and process the response // if (useWebSocket) { // // Send the request using the WebSocket session // StringWriter wsWriter = new StringWriter(1000); request.writeJSONString(wsWriter); String wsRequest = wsWriter.toString(); if (communicationLoggingMask != 0) log = "WebSocket " + host + ": " + wsRequest; String wsResponse = webSocket.doPost(wsRequest); updateUploadedVolume(wsRequest.length()); if (maxResponseSize > 0) { if ((communicationLoggingMask & Peers.LOGGING_MASK_200_RESPONSES) != 0) { log += " >>> " + wsResponse; showLog = true; } if (wsResponse.length() > maxResponseSize) throw new NxtException.NxtIOException("Maximum size exceeded: " + wsResponse.length()); response = (JSONObject)JSONValue.parseWithException(wsResponse); updateDownloadedVolume(wsResponse.length()); } } else { // // Send the request using HTTP // URL url = new URL("http://" + host + ":" + getPort() + "/nxt"); if (communicationLoggingMask != 0) log = "\"" + url.toString() + "\": " + JSON.toString(request); connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("POST"); connection.setDoOutput(true); connection.setConnectTimeout(Peers.connectTimeout); connection.setReadTimeout(Peers.readTimeout); connection.setRequestProperty("Accept-Encoding", "gzip"); connection.setRequestProperty("Content-Type", "text/plain; charset=UTF-8"); try (Writer writer = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream(), "UTF-8"))) { CountingOutputWriter cow = new CountingOutputWriter(writer); request.writeJSONString(cow); updateUploadedVolume(cow.getCount()); } if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { if (maxResponseSize > 0) { if ((communicationLoggingMask & Peers.LOGGING_MASK_200_RESPONSES) != 0) { CountingInputStream cis = new CountingInputStream(connection.getInputStream(), maxResponseSize); InputStream responseStream = cis; if ("gzip".equals(connection.getHeaderField("Content-Encoding"))) responseStream = new GZIPInputStream(cis); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int numberOfBytes; try (InputStream inputStream = responseStream) { while ((numberOfBytes = inputStream.read(buffer, 0, buffer.length)) > 0) byteArrayOutputStream.write(buffer, 0, numberOfBytes); } String responseValue = byteArrayOutputStream.toString("UTF-8"); if (responseValue.length() > 0 && responseStream instanceof GZIPInputStream) log += String.format("[length: %d, compression ratio: %.2f]", cis.getCount(), (double)cis.getCount()/(double) responseValue.length()); log += " >>> " + responseValue; showLog = true; response = (JSONObject) JSONValue.parseWithException(responseValue); updateDownloadedVolume(responseValue.length()); } else { InputStream responseStream = connection.getInputStream(); if ("gzip".equals(connection.getHeaderField("Content-Encoding"))) responseStream = new GZIPInputStream(responseStream); try (Reader reader = new BufferedReader(new InputStreamReader(responseStream, "UTF-8"))) { CountingInputReader cir = new CountingInputReader(reader, maxResponseSize); response = (JSONObject)JSONValue.parseWithException(cir); updateDownloadedVolume(cir.getCount()); } } } } else { if ((communicationLoggingMask & Peers.LOGGING_MASK_NON200_RESPONSES) != 0) { log += " >>> Peer responded with HTTP " + connection.getResponseCode() + " code!"; showLog = true; } Logger.logDebugMessage("Peer " + host + " responded with HTTP " + connection.getResponseCode()); deactivate(); connection.disconnect(); } } // // Check for an error response // if (response != null && response.get("error") != null) { deactivate(); if (Errors.SEQUENCE_ERROR.equals(response.get("error")) && request != Peers.myPeerInfoRequest) { Logger.logDebugMessage("Sequence error, reconnecting to " + host); connect(); } else { Logger.logDebugMessage("Peer " + host + " version " + version + " returned error: " + response.toJSONString() + ", request was: " + JSON.toString(request) + ", disconnecting"); if (connection != null) { connection.disconnect(); } } } } catch (NxtException.NxtIOException e) { blacklist(e); if (connection != null) { connection.disconnect(); } } catch (RuntimeException|ParseException|IOException e) { if (!(e instanceof UnknownHostException || e instanceof SocketTimeoutException || e instanceof SocketException || Errors.END_OF_FILE.equals(e.getMessage()))) { Logger.logDebugMessage(String.format("Error sending request to peer %s: %s", host, e.getMessage()!=null ? e.getMessage() : e.toString())); } if ((communicationLoggingMask & Peers.LOGGING_MASK_EXCEPTIONS) != 0) { log += " >>> " + e.toString(); showLog = true; } deactivate(); if (connection != null) { connection.disconnect(); } } if (showLog) { Logger.logMessage(log + "\n"); } return response; } @Override public int compareTo(Peer o) { if (getWeight() > o.getWeight()) { return -1; } else if (getWeight() < o.getWeight()) { return 1; } return getHost().compareTo(o.getHost()); } void connect() { lastConnectAttempt = Nxt.getEpochTime(); try { if (!Peers.ignorePeerAnnouncedAddress && announcedAddress != null) { try { URI uri = new URI("http://" + announcedAddress); InetAddress inetAddress = InetAddress.getByName(uri.getHost()); if (!inetAddress.equals(InetAddress.getByName(host))) { Logger.logDebugMessage("Connect: announced address " + announcedAddress + " now points to " + inetAddress.getHostAddress() + ", replacing peer " + host); Peers.removePeer(this); PeerImpl newPeer = Peers.findOrCreatePeer(inetAddress, announcedAddress, true); if (newPeer != null) { Peers.addPeer(newPeer); newPeer.connect(); } return; } } catch (URISyntaxException | UnknownHostException e) { blacklist(e); return; } } JSONObject response = send(Peers.myPeerInfoRequest); if (response != null) { if (response.get("error") != null) { setState(State.NON_CONNECTED); return; } String servicesString = (String)response.get("services"); long origServices = services; services = (servicesString != null ? Long.parseUnsignedLong(servicesString) : 0); setApplication((String)response.get("application")); setApiPort(response.get("apiPort")); setApiSSLPort(response.get("apiSSLPort")); lastUpdated = lastConnectAttempt; setVersion((String) response.get("version")); setPlatform((String) response.get("platform")); shareAddress = Boolean.TRUE.equals(response.get("shareAddress")); analyzeHallmark((String) response.get("hallmark")); if (!Peers.ignorePeerAnnouncedAddress) { String newAnnouncedAddress = Convert.emptyToNull((String) response.get("announcedAddress")); if (newAnnouncedAddress != null) { newAnnouncedAddress = Peers.addressWithPort(newAnnouncedAddress.toLowerCase()); if (newAnnouncedAddress != null) { if (!verifyAnnouncedAddress(newAnnouncedAddress)) { Logger.logDebugMessage("Connect: new announced address for " + host + " not accepted"); if (!verifyAnnouncedAddress(announcedAddress)) { Logger.logDebugMessage("Connect: old announced address for " + host + " no longer valid"); Peers.setAnnouncedAddress(this, host); } setState(State.NON_CONNECTED); return; } if (!newAnnouncedAddress.equals(announcedAddress)) { Logger.logDebugMessage("Connect: peer " + host + " has new announced address " + newAnnouncedAddress + ", old is " + announcedAddress); int oldPort = getPort(); Peers.setAnnouncedAddress(this, newAnnouncedAddress); if (getPort() != oldPort) { // force checking connectivity to new announced port setState(State.NON_CONNECTED); return; } } } } else { Peers.setAnnouncedAddress(this, host); } } if (announcedAddress == null) { if (hallmark == null || hallmark.getPort() == Peers.getDefaultPeerPort()) { Peers.setAnnouncedAddress(this, host); Logger.logDebugMessage("Connected to peer without announced address, setting to " + host); } else { setState(State.NON_CONNECTED); return; } } if (!isOldVersion) { setState(State.CONNECTED); if (services != origServices) { Peers.notifyListeners(this, Peers.Event.CHANGED_SERVICES); } } else if (!isBlacklisted()) { blacklist("Old version: " + version); } } else { //Logger.logDebugMessage("Failed to connect to peer " + peerAddress); setState(State.NON_CONNECTED); } } catch (RuntimeException e) { blacklist(e); } } boolean verifyAnnouncedAddress(String newAnnouncedAddress) { if (newAnnouncedAddress == null) { return true; } try { URI uri = new URI("http://" + newAnnouncedAddress); int announcedPort = uri.getPort() == -1 ? Peers.getDefaultPeerPort() : uri.getPort(); if (hallmark != null && announcedPort != hallmark.getPort()) { Logger.logDebugMessage("Announced port " + announcedPort + " does not match hallmark " + hallmark.getPort() + ", ignoring hallmark for " + host); unsetHallmark(); } InetAddress address = InetAddress.getByName(host); for (InetAddress inetAddress : InetAddress.getAllByName(uri.getHost())) { if (inetAddress.equals(address)) { return true; } } Logger.logDebugMessage("Announced address " + newAnnouncedAddress + " does not resolve to " + host); } catch (UnknownHostException|URISyntaxException e) { Logger.logDebugMessage(e.toString()); blacklist(e); } return false; } boolean analyzeHallmark(final String hallmarkString) { if (hallmarkString == null && this.hallmark == null) { return true; } if (this.hallmark != null && this.hallmark.getHallmarkString().equals(hallmarkString)) { return true; } if (hallmarkString == null) { unsetHallmark(); return true; } try { Hallmark hallmark = Hallmark.parseHallmark(hallmarkString); if (!hallmark.isValid()) { Logger.logDebugMessage("Invalid hallmark " + hallmarkString + " for " + host); unsetHallmark(); return false; } if (!hallmark.getHost().equals(host)) { InetAddress hostAddress = InetAddress.getByName(host); boolean validHost = false; for (InetAddress nextHallmark : InetAddress.getAllByName(hallmark.getHost())) { if (hostAddress.equals(nextHallmark)) { validHost = true; break; } } if (!validHost) { Logger.logDebugMessage("Hallmark host " + hallmark.getHost() + " doesn't match " + host); unsetHallmark(); return false; } } setHallmark(hallmark); long accountId = Account.getId(hallmark.getPublicKey()); List<PeerImpl> groupedPeers = new ArrayList<>(); int mostRecentDate = 0; long totalWeight = 0; for (PeerImpl peer : Peers.allPeers) { if (peer.hallmark == null) { continue; } if (accountId == peer.hallmark.getAccountId()) { groupedPeers.add(peer); if (peer.hallmark.getDate() > mostRecentDate) { mostRecentDate = peer.hallmark.getDate(); totalWeight = peer.getHallmarkWeight(mostRecentDate); } else { totalWeight += peer.getHallmarkWeight(mostRecentDate); } } } for (PeerImpl peer : groupedPeers) { peer.adjustedWeight = Constants.MAX_BALANCE_NXT * peer.getHallmarkWeight(mostRecentDate) / totalWeight; Peers.notifyListeners(peer, Peers.Event.WEIGHT); } return true; } catch (UnknownHostException ignore) { } catch (RuntimeException e) { Logger.logDebugMessage("Failed to analyze hallmark for peer " + host + ", " + e.toString(), e); } unsetHallmark(); return false; } private int getHallmarkWeight(int date) { if (hallmark == null || ! hallmark.isValid() || hallmark.getDate() != date) { return 0; } return hallmark.getWeight(); } private void unsetHallmark() { removeService(Service.HALLMARK, false); this.hallmark = null; } private void setHallmark(Hallmark hallmark) { this.hallmark = hallmark; addService(Service.HALLMARK, false); } void addService(Service service, boolean doNotify) { boolean notifyListeners; synchronized (this) { notifyListeners = ((services & service.getCode()) == 0); services |= service.getCode(); } if (notifyListeners && doNotify) { Peers.notifyListeners(this, Peers.Event.CHANGED_SERVICES); } } void removeService(Service service, boolean doNotify) { boolean notifyListeners; synchronized (this) { notifyListeners = ((services & service.getCode()) != 0); services &= (~service.getCode()); } if (notifyListeners && doNotify) { Peers.notifyListeners(this, Peers.Event.CHANGED_SERVICES); } } long getServices() { synchronized (this) { return services; } } void setServices(long services) { synchronized (this) { this.services = services; } } @Override public boolean providesService(Service service) { boolean isProvided; synchronized (this) { isProvided = ((services & service.getCode()) != 0); } return isProvided; } @Override public boolean providesServices(long services) { boolean isProvided; synchronized (this) { isProvided = (services & this.services) == services; } return isProvided; } }