commit 43b563b5a0fe46cfaa899b67ca1fc6804166b468 Author: Karsten Loesing karsten.loesing@gmx.net Date: Wed Jul 23 17:34:36 2014 +0200
Sort classes into six sub packages. --- build.xml | 2 +- etc/web.xml.template | 4 +- src/org/torproject/onionoo/ApplicationFactory.java | 51 -- src/org/torproject/onionoo/BandwidthDocument.java | 27 - .../onionoo/BandwidthDocumentWriter.java | 190 ----- src/org/torproject/onionoo/BandwidthStatus.java | 78 -- .../torproject/onionoo/BandwidthStatusUpdater.java | 145 ---- src/org/torproject/onionoo/ClientsDocument.java | 22 - .../torproject/onionoo/ClientsDocumentWriter.java | 284 -------- .../torproject/onionoo/ClientsGraphHistory.java | 83 --- src/org/torproject/onionoo/ClientsStatus.java | 211 ------ .../torproject/onionoo/ClientsStatusUpdater.java | 224 ------ src/org/torproject/onionoo/DateTimeHelper.java | 92 --- src/org/torproject/onionoo/DescriptorSource.java | 665 ----------------- src/org/torproject/onionoo/DetailsDocument.java | 365 ---------- .../torproject/onionoo/DetailsDocumentWriter.java | 222 ------ src/org/torproject/onionoo/DetailsStatus.java | 141 ---- src/org/torproject/onionoo/Document.java | 24 - src/org/torproject/onionoo/DocumentStore.java | 744 ------------------- src/org/torproject/onionoo/DocumentWriter.java | 11 - src/org/torproject/onionoo/GraphHistory.java | 56 -- src/org/torproject/onionoo/LockFile.java | 43 -- src/org/torproject/onionoo/Logger.java | 81 --- src/org/torproject/onionoo/LookupService.java | 408 ----------- src/org/torproject/onionoo/Main.java | 119 ---- .../onionoo/NodeDetailsStatusUpdater.java | 620 ---------------- src/org/torproject/onionoo/NodeIndexer.java | 425 ----------- src/org/torproject/onionoo/NodeStatus.java | 581 --------------- src/org/torproject/onionoo/RequestHandler.java | 548 -------------- src/org/torproject/onionoo/ResourceServlet.java | 448 ------------ src/org/torproject/onionoo/ResponseBuilder.java | 311 -------- .../onionoo/ReverseDomainNameResolver.java | 174 ----- src/org/torproject/onionoo/StatusUpdater.java | 11 - src/org/torproject/onionoo/SummaryDocument.java | 201 ------ .../torproject/onionoo/SummaryDocumentWriter.java | 87 --- src/org/torproject/onionoo/Time.java | 14 - src/org/torproject/onionoo/UpdateStatus.java | 7 - src/org/torproject/onionoo/UptimeDocument.java | 23 - .../torproject/onionoo/UptimeDocumentWriter.java | 291 -------- src/org/torproject/onionoo/UptimeStatus.java | 226 ------ .../torproject/onionoo/UptimeStatusUpdater.java | 126 ---- src/org/torproject/onionoo/WeightsDocument.java | 64 -- .../torproject/onionoo/WeightsDocumentWriter.java | 222 ------ src/org/torproject/onionoo/WeightsStatus.java | 97 --- .../torproject/onionoo/WeightsStatusUpdater.java | 328 --------- src/org/torproject/onionoo/cron/Main.java | 140 ++++ .../torproject/onionoo/docs/BandwidthDocument.java | 27 + .../torproject/onionoo/docs/BandwidthStatus.java | 80 +++ .../torproject/onionoo/docs/ClientsDocument.java | 22 + .../onionoo/docs/ClientsGraphHistory.java | 83 +++ .../torproject/onionoo/docs/ClientsHistory.java | 174 +++++ src/org/torproject/onionoo/docs/ClientsStatus.java | 43 ++ .../torproject/onionoo/docs/DetailsDocument.java | 365 ++++++++++ src/org/torproject/onionoo/docs/DetailsStatus.java | 141 ++++ src/org/torproject/onionoo/docs/Document.java | 24 + src/org/torproject/onionoo/docs/DocumentStore.java | 748 ++++++++++++++++++++ src/org/torproject/onionoo/docs/GraphHistory.java | 56 ++ src/org/torproject/onionoo/docs/NodeStatus.java | 582 +++++++++++++++ .../torproject/onionoo/docs/SummaryDocument.java | 202 ++++++ src/org/torproject/onionoo/docs/UpdateStatus.java | 7 + .../torproject/onionoo/docs/UptimeDocument.java | 23 + src/org/torproject/onionoo/docs/UptimeHistory.java | 90 +++ src/org/torproject/onionoo/docs/UptimeStatus.java | 142 ++++ .../torproject/onionoo/docs/WeightsDocument.java | 64 ++ src/org/torproject/onionoo/docs/WeightsStatus.java | 99 +++ src/org/torproject/onionoo/server/NodeIndexer.java | 432 +++++++++++ .../torproject/onionoo/server/RequestHandler.java | 552 +++++++++++++++ .../torproject/onionoo/server/ResourceServlet.java | 451 ++++++++++++ .../torproject/onionoo/server/ResponseBuilder.java | 320 +++++++++ .../onionoo/updater/BandwidthStatusUpdater.java | 149 ++++ .../onionoo/updater/ClientsStatusUpdater.java | 230 ++++++ .../onionoo/updater/DescriptorListener.java | 7 + .../onionoo/updater/DescriptorSource.java | 646 +++++++++++++++++ .../torproject/onionoo/updater/DescriptorType.java | 15 + .../onionoo/updater/FingerprintListener.java | 10 + .../torproject/onionoo/updater/LookupResult.java | 70 ++ .../torproject/onionoo/updater/LookupService.java | 343 +++++++++ .../onionoo/updater/NodeDetailsStatusUpdater.java | 626 ++++++++++++++++ .../onionoo/updater/ReverseDomainNameResolver.java | 179 +++++ .../torproject/onionoo/updater/StatusUpdater.java | 11 + .../onionoo/updater/UptimeStatusUpdater.java | 130 ++++ .../onionoo/updater/WeightsStatusUpdater.java | 332 +++++++++ .../onionoo/util/ApplicationFactory.java | 55 ++ .../torproject/onionoo/util/DateTimeHelper.java | 92 +++ src/org/torproject/onionoo/util/LockFile.java | 43 ++ src/org/torproject/onionoo/util/Logger.java | 81 +++ src/org/torproject/onionoo/util/Time.java | 14 + .../onionoo/writer/BandwidthDocumentWriter.java | 201 ++++++ .../onionoo/writer/ClientsDocumentWriter.java | 296 ++++++++ .../onionoo/writer/DetailsDocumentWriter.java | 233 ++++++ .../torproject/onionoo/writer/DocumentWriter.java | 11 + .../onionoo/writer/SummaryDocumentWriter.java | 94 +++ .../onionoo/writer/UptimeDocumentWriter.java | 303 ++++++++ .../onionoo/writer/WeightsDocumentWriter.java | 233 ++++++ .../torproject/onionoo/DummyDescriptorSource.java | 4 + .../org/torproject/onionoo/DummyDocumentStore.java | 3 + test/org/torproject/onionoo/DummyTime.java | 2 + test/org/torproject/onionoo/LookupServiceTest.java | 2 + .../torproject/onionoo/ResourceServletTest.java | 44 +- .../onionoo/UptimeDocumentWriterTest.java | 7 + test/org/torproject/onionoo/UptimeStatusTest.java | 4 + .../onionoo/UptimeStatusUpdaterTest.java | 6 + 102 files changed, 9327 insertions(+), 9112 deletions(-)
diff --git a/build.xml b/build.xml index dccf374..a69bac2 100644 --- a/build.xml +++ b/build.xml @@ -93,7 +93,7 @@ <target name="run" depends="compile"> <java fork="true" maxmemory="4g" - classname="org.torproject.onionoo.Main" + classname="org.torproject.onionoo.cron.Main" error="errors"> <classpath refid="classpath"/> </java> diff --git a/etc/web.xml.template b/etc/web.xml.template index 988d47a..2c0b280 100644 --- a/etc/web.xml.template +++ b/etc/web.xml.template @@ -9,7 +9,7 @@ <servlet> <servlet-name>Resource</servlet-name> <servlet-class> - org.torproject.onionoo.ResourceServlet + org.torproject.onionoo.server.ResourceServlet </servlet-class> <init-param> <param-name>maintenance</param-name> @@ -48,7 +48,7 @@
<listener> <listener-class> - org.torproject.onionoo.NodeIndexer + org.torproject.onionoo.server.NodeIndexer </listener-class> </listener> </web-app> diff --git a/src/org/torproject/onionoo/ApplicationFactory.java b/src/org/torproject/onionoo/ApplicationFactory.java deleted file mode 100644 index 44f2c17..0000000 --- a/src/org/torproject/onionoo/ApplicationFactory.java +++ /dev/null @@ -1,51 +0,0 @@ -/* Copyright 2014 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -public class ApplicationFactory { - - private static Time timeInstance; - public static void setTime(Time time) { - timeInstance = time; - } - public static Time getTime() { - if (timeInstance == null) { - timeInstance = new Time(); - } - return timeInstance; - } - - private static DescriptorSource descriptorSourceInstance; - public static void setDescriptorSource( - DescriptorSource descriptorSource) { - descriptorSourceInstance = descriptorSource; - } - public static DescriptorSource getDescriptorSource() { - if (descriptorSourceInstance == null) { - descriptorSourceInstance = new DescriptorSource(); - } - return descriptorSourceInstance; - } - - private static DocumentStore documentStoreInstance; - public static void setDocumentStore(DocumentStore documentStore) { - documentStoreInstance = documentStore; - } - public static DocumentStore getDocumentStore() { - if (documentStoreInstance == null) { - documentStoreInstance = new DocumentStore(); - } - return documentStoreInstance; - } - - private static NodeIndexer nodeIndexerInstance; - public static void setNodeIndexer(NodeIndexer nodeIndexer) { - nodeIndexerInstance = nodeIndexer; - } - public static NodeIndexer getNodeIndexer() { - if (nodeIndexerInstance == null) { - nodeIndexerInstance = new NodeIndexer(); - } - return nodeIndexerInstance; - } -} diff --git a/src/org/torproject/onionoo/BandwidthDocument.java b/src/org/torproject/onionoo/BandwidthDocument.java deleted file mode 100644 index 9c7cd4d..0000000 --- a/src/org/torproject/onionoo/BandwidthDocument.java +++ /dev/null @@ -1,27 +0,0 @@ -/* Copyright 2013 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.util.Map; - -public class BandwidthDocument extends Document { - - @SuppressWarnings("unused") - private String fingerprint; - public void setFingerprint(String fingerprint) { - this.fingerprint = fingerprint; - } - - @SuppressWarnings("unused") - private Map<String, GraphHistory> write_history; - public void setWriteHistory(Map<String, GraphHistory> writeHistory) { - this.write_history = writeHistory; - } - - @SuppressWarnings("unused") - private Map<String, GraphHistory> read_history; - public void setReadHistory(Map<String, GraphHistory> readHistory) { - this.read_history = readHistory; - } -} - diff --git a/src/org/torproject/onionoo/BandwidthDocumentWriter.java b/src/org/torproject/onionoo/BandwidthDocumentWriter.java deleted file mode 100644 index 164ab30..0000000 --- a/src/org/torproject/onionoo/BandwidthDocumentWriter.java +++ /dev/null @@ -1,190 +0,0 @@ -/* Copyright 2011--2014 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.SortedMap; -import java.util.SortedSet; - -public class BandwidthDocumentWriter implements FingerprintListener, - DocumentWriter{ - - private DescriptorSource descriptorSource; - - private DocumentStore documentStore; - - private long now; - - public BandwidthDocumentWriter() { - this.descriptorSource = ApplicationFactory.getDescriptorSource(); - this.documentStore = ApplicationFactory.getDocumentStore(); - this.now = ApplicationFactory.getTime().currentTimeMillis(); - this.registerFingerprintListeners(); - } - - private void registerFingerprintListeners() { - this.descriptorSource.registerFingerprintListener(this, - DescriptorType.RELAY_EXTRA_INFOS); - this.descriptorSource.registerFingerprintListener(this, - DescriptorType.BRIDGE_EXTRA_INFOS); - } - - private Set<String> updateBandwidthDocuments = new HashSet<String>(); - - public void processFingerprints(SortedSet<String> fingerprints, - boolean relay) { - this.updateBandwidthDocuments.addAll(fingerprints); - } - - public void writeDocuments() { - for (String fingerprint : this.updateBandwidthDocuments) { - BandwidthStatus bandwidthStatus = this.documentStore.retrieve( - BandwidthStatus.class, true, fingerprint); - if (bandwidthStatus == null) { - continue; - } - BandwidthDocument bandwidthDocument = this.compileBandwidthDocument( - fingerprint, bandwidthStatus); - this.documentStore.store(bandwidthDocument, fingerprint); - } - Logger.printStatusTime("Wrote bandwidth document files"); - } - - - private BandwidthDocument compileBandwidthDocument(String fingerprint, - BandwidthStatus bandwidthStatus) { - BandwidthDocument bandwidthDocument = new BandwidthDocument(); - bandwidthDocument.setFingerprint(fingerprint); - bandwidthDocument.setWriteHistory(this.compileGraphType( - bandwidthStatus.getWriteHistory())); - bandwidthDocument.setReadHistory(this.compileGraphType( - bandwidthStatus.getReadHistory())); - return bandwidthDocument; - } - - private String[] graphNames = new String[] { - "3_days", - "1_week", - "1_month", - "3_months", - "1_year", - "5_years" }; - - private long[] graphIntervals = new long[] { - DateTimeHelper.THREE_DAYS, - DateTimeHelper.ONE_WEEK, - DateTimeHelper.ROUGHLY_ONE_MONTH, - DateTimeHelper.ROUGHLY_THREE_MONTHS, - DateTimeHelper.ROUGHLY_ONE_YEAR, - DateTimeHelper.ROUGHLY_FIVE_YEARS }; - - private long[] dataPointIntervals = new long[] { - DateTimeHelper.FIFTEEN_MINUTES, - DateTimeHelper.ONE_HOUR, - DateTimeHelper.FOUR_HOURS, - DateTimeHelper.TWELVE_HOURS, - DateTimeHelper.TWO_DAYS, - DateTimeHelper.TEN_DAYS }; - - private Map<String, GraphHistory> compileGraphType( - SortedMap<Long, long[]> history) { - Map<String, GraphHistory> graphs = - new LinkedHashMap<String, GraphHistory>(); - for (int i = 0; i < this.graphIntervals.length; i++) { - String graphName = this.graphNames[i]; - long graphInterval = this.graphIntervals[i]; - long dataPointInterval = this.dataPointIntervals[i]; - List<Long> dataPoints = new ArrayList<Long>(); - long intervalStartMillis = ((this.now - graphInterval) - / dataPointInterval) * dataPointInterval; - long totalMillis = 0L, totalBandwidth = 0L; - for (long[] v : history.values()) { - long startMillis = v[0], endMillis = v[1], bandwidth = v[2]; - if (endMillis < intervalStartMillis) { - continue; - } - while ((intervalStartMillis / dataPointInterval) != - (endMillis / dataPointInterval)) { - dataPoints.add(totalMillis * 5L < dataPointInterval - ? -1L : (totalBandwidth * DateTimeHelper.ONE_SECOND) - / totalMillis); - totalBandwidth = 0L; - totalMillis = 0L; - intervalStartMillis += dataPointInterval; - } - totalBandwidth += bandwidth; - totalMillis += (endMillis - startMillis); - } - dataPoints.add(totalMillis * 5L < dataPointInterval - ? -1L : (totalBandwidth * DateTimeHelper.ONE_SECOND) - / totalMillis); - long maxValue = 1L; - int firstNonNullIndex = -1, lastNonNullIndex = -1; - for (int j = 0; j < dataPoints.size(); j++) { - long dataPoint = dataPoints.get(j); - if (dataPoint >= 0L) { - if (firstNonNullIndex < 0) { - firstNonNullIndex = j; - } - lastNonNullIndex = j; - if (dataPoint > maxValue) { - maxValue = dataPoint; - } - } - } - if (firstNonNullIndex < 0) { - continue; - } - long firstDataPointMillis = (((this.now - graphInterval) - / dataPointInterval) + firstNonNullIndex) * dataPointInterval - + dataPointInterval / 2L; - if (i > 0 && - firstDataPointMillis >= this.now - graphIntervals[i - 1]) { - /* Skip bandwidth history object, because it doesn't contain - * anything new that wasn't already contained in the last - * bandwidth history object(s). */ - continue; - } - long lastDataPointMillis = firstDataPointMillis - + (lastNonNullIndex - firstNonNullIndex) * dataPointInterval; - double factor = ((double) maxValue) / 999.0; - int count = lastNonNullIndex - firstNonNullIndex + 1; - GraphHistory graphHistory = new GraphHistory(); - graphHistory.setFirst(DateTimeHelper.format(firstDataPointMillis)); - graphHistory.setLast(DateTimeHelper.format(lastDataPointMillis)); - graphHistory.setInterval((int) (dataPointInterval - / DateTimeHelper.ONE_SECOND)); - graphHistory.setFactor(factor); - graphHistory.setCount(count); - int previousNonNullIndex = -2; - boolean foundTwoAdjacentDataPoints = false; - List<Integer> values = new ArrayList<Integer>(); - for (int j = firstNonNullIndex; j <= lastNonNullIndex; j++) { - long dataPoint = dataPoints.get(j); - if (dataPoint >= 0L) { - if (j - previousNonNullIndex == 1) { - foundTwoAdjacentDataPoints = true; - } - previousNonNullIndex = j; - } - values.add(dataPoint < 0L ? null : - (int) ((dataPoint * 999L) / maxValue)); - } - graphHistory.setValues(values); - if (foundTwoAdjacentDataPoints) { - graphs.put(graphName, graphHistory); - } - } - return graphs; - } - - public String getStatsString() { - /* TODO Add statistics string. */ - return null; - } -} diff --git a/src/org/torproject/onionoo/BandwidthStatus.java b/src/org/torproject/onionoo/BandwidthStatus.java deleted file mode 100644 index 3252d67..0000000 --- a/src/org/torproject/onionoo/BandwidthStatus.java +++ /dev/null @@ -1,78 +0,0 @@ -/* Copyright 2013--2014 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.util.Scanner; -import java.util.SortedMap; -import java.util.TreeMap; - -public class BandwidthStatus extends Document { - - private SortedMap<Long, long[]> writeHistory = - new TreeMap<Long, long[]>(); - public void setWriteHistory(SortedMap<Long, long[]> writeHistory) { - this.writeHistory = writeHistory; - } - public SortedMap<Long, long[]> getWriteHistory() { - return this.writeHistory; - } - - private SortedMap<Long, long[]> readHistory = - new TreeMap<Long, long[]>(); - public void setReadHistory(SortedMap<Long, long[]> readHistory) { - this.readHistory = readHistory; - } - public SortedMap<Long, long[]> getReadHistory() { - return this.readHistory; - } - - public void fromDocumentString(String documentString) { - Scanner s = new Scanner(documentString); - while (s.hasNextLine()) { - String line = s.nextLine(); - String[] parts = line.split(" "); - if (parts.length != 6) { - System.err.println("Illegal line '" + line + "' in bandwidth " - + "history. Skipping this line."); - continue; - } - SortedMap<Long, long[]> history = parts[0].equals("r") - ? readHistory : writeHistory; - long startMillis = DateTimeHelper.parse(parts[1] + " " + parts[2]); - long endMillis = DateTimeHelper.parse(parts[3] + " " + parts[4]); - if (startMillis < 0L || endMillis < 0L) { - System.err.println("Could not parse timestamp while reading " - + "bandwidth history. Skipping."); - break; - } - long bandwidth = Long.parseLong(parts[5]); - long previousEndMillis = history.headMap(startMillis).isEmpty() - ? startMillis - : history.get(history.headMap(startMillis).lastKey())[1]; - long nextStartMillis = history.tailMap(startMillis).isEmpty() - ? endMillis : history.tailMap(startMillis).firstKey(); - if (previousEndMillis <= startMillis && - nextStartMillis >= endMillis) { - history.put(startMillis, new long[] { startMillis, endMillis, - bandwidth }); - } - } - s.close(); - } - - public String toDocumentString() { - StringBuilder sb = new StringBuilder(); - for (long[] v : writeHistory.values()) { - sb.append("w " + DateTimeHelper.format(v[0]) + " " - + DateTimeHelper.format(v[1]) + " " + String.valueOf(v[2]) - + "\n"); - } - for (long[] v : readHistory.values()) { - sb.append("r " + DateTimeHelper.format(v[0]) + " " - + DateTimeHelper.format(v[1]) + " " + String.valueOf(v[2]) - + "\n"); - } - return sb.toString(); - } -} - diff --git a/src/org/torproject/onionoo/BandwidthStatusUpdater.java b/src/org/torproject/onionoo/BandwidthStatusUpdater.java deleted file mode 100644 index 6320f6e..0000000 --- a/src/org/torproject/onionoo/BandwidthStatusUpdater.java +++ /dev/null @@ -1,145 +0,0 @@ -/* Copyright 2011--2014 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.util.SortedMap; -import java.util.TreeMap; - -import org.torproject.descriptor.Descriptor; -import org.torproject.descriptor.ExtraInfoDescriptor; - -public class BandwidthStatusUpdater implements DescriptorListener, - StatusUpdater { - - private DescriptorSource descriptorSource; - - private DocumentStore documentStore; - - private long now; - - public BandwidthStatusUpdater() { - this.descriptorSource = ApplicationFactory.getDescriptorSource(); - this.documentStore = ApplicationFactory.getDocumentStore(); - this.now = ApplicationFactory.getTime().currentTimeMillis(); - this.registerDescriptorListeners(); - } - - private void registerDescriptorListeners() { - this.descriptorSource.registerDescriptorListener(this, - DescriptorType.RELAY_EXTRA_INFOS); - this.descriptorSource.registerDescriptorListener(this, - DescriptorType.BRIDGE_EXTRA_INFOS); - } - - public void processDescriptor(Descriptor descriptor, boolean relay) { - if (descriptor instanceof ExtraInfoDescriptor) { - this.parseDescriptor((ExtraInfoDescriptor) descriptor); - } - } - - public void updateStatuses() { - /* Status files are already updated while processing descriptors. */ - } - - private void parseDescriptor(ExtraInfoDescriptor descriptor) { - String fingerprint = descriptor.getFingerprint(); - BandwidthStatus bandwidthStatus = this.documentStore.retrieve( - BandwidthStatus.class, true, fingerprint); - if (bandwidthStatus == null) { - bandwidthStatus = new BandwidthStatus(); - } - if (descriptor.getWriteHistory() != null) { - parseHistoryLine(descriptor.getWriteHistory().getLine(), - bandwidthStatus.getWriteHistory()); - } - if (descriptor.getReadHistory() != null) { - parseHistoryLine(descriptor.getReadHistory().getLine(), - bandwidthStatus.getReadHistory()); - } - this.compressHistory(bandwidthStatus.getWriteHistory()); - this.compressHistory(bandwidthStatus.getReadHistory()); - this.documentStore.store(bandwidthStatus, fingerprint); - } - - private void parseHistoryLine(String line, - SortedMap<Long, long[]> history) { - String[] parts = line.split(" "); - if (parts.length < 6) { - return; - } - long endMillis = DateTimeHelper.parse(parts[1] + " " + parts[2]); - if (endMillis < 0L) { - System.err.println("Could not parse timestamp in line '" + line - + "'. Skipping."); - return; - } - long intervalMillis = Long.parseLong(parts[3].substring(1)) - * DateTimeHelper.ONE_SECOND; - String[] values = parts[5].split(","); - for (int i = values.length - 1; i >= 0; i--) { - long bandwidthValue = Long.parseLong(values[i]); - long startMillis = endMillis - intervalMillis; - /* TODO Should we first check whether an interval is already - * contained in history? */ - history.put(startMillis, new long[] { startMillis, endMillis, - bandwidthValue }); - endMillis -= intervalMillis; - } - } - - private void compressHistory(SortedMap<Long, long[]> history) { - SortedMap<Long, long[]> uncompressedHistory = - new TreeMap<Long, long[]>(history); - history.clear(); - long lastStartMillis = 0L, lastEndMillis = 0L, lastBandwidth = 0L; - String lastMonthString = "1970-01"; - for (long[] v : uncompressedHistory.values()) { - long startMillis = v[0], endMillis = v[1], bandwidth = v[2]; - long intervalLengthMillis; - if (this.now - endMillis <= DateTimeHelper.THREE_DAYS) { - intervalLengthMillis = DateTimeHelper.FIFTEEN_MINUTES; - } else if (this.now - endMillis <= DateTimeHelper.ONE_WEEK) { - intervalLengthMillis = DateTimeHelper.ONE_HOUR; - } else if (this.now - endMillis <= - DateTimeHelper.ROUGHLY_ONE_MONTH) { - intervalLengthMillis = DateTimeHelper.FOUR_HOURS; - } else if (this.now - endMillis <= - DateTimeHelper.ROUGHLY_THREE_MONTHS) { - intervalLengthMillis = DateTimeHelper.TWELVE_HOURS; - } else if (this.now - endMillis <= - DateTimeHelper.ROUGHLY_ONE_YEAR) { - intervalLengthMillis = DateTimeHelper.TWO_DAYS; - } else { - intervalLengthMillis = DateTimeHelper.TEN_DAYS; - } - String monthString = DateTimeHelper.format(startMillis, - DateTimeHelper.ISO_YEARMONTH_FORMAT); - if (lastEndMillis == startMillis && - ((lastEndMillis - 1L) / intervalLengthMillis) == - ((endMillis - 1L) / intervalLengthMillis) && - lastMonthString.equals(monthString)) { - lastEndMillis = endMillis; - lastBandwidth += bandwidth; - } else { - if (lastStartMillis > 0L) { - history.put(lastStartMillis, new long[] { lastStartMillis, - lastEndMillis, lastBandwidth }); - } - lastStartMillis = startMillis; - lastEndMillis = endMillis; - lastBandwidth = bandwidth; - } - lastMonthString = monthString; - } - if (lastStartMillis > 0L) { - history.put(lastStartMillis, new long[] { lastStartMillis, - lastEndMillis, lastBandwidth }); - } - } - - public String getStatsString() { - /* TODO Add statistics string. */ - return null; - } -} - diff --git a/src/org/torproject/onionoo/ClientsDocument.java b/src/org/torproject/onionoo/ClientsDocument.java deleted file mode 100644 index e8e40ee..0000000 --- a/src/org/torproject/onionoo/ClientsDocument.java +++ /dev/null @@ -1,22 +0,0 @@ -/* Copyright 2014 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.util.Map; - -public class ClientsDocument extends Document { - - @SuppressWarnings("unused") - private String fingerprint; - public void setFingerprint(String fingerprint) { - this.fingerprint = fingerprint; - } - - @SuppressWarnings("unused") - private Map<String, ClientsGraphHistory> average_clients; - public void setAverageClients( - Map<String, ClientsGraphHistory> averageClients) { - this.average_clients = averageClients; - } -} - diff --git a/src/org/torproject/onionoo/ClientsDocumentWriter.java b/src/org/torproject/onionoo/ClientsDocumentWriter.java deleted file mode 100644 index 1fced6d..0000000 --- a/src/org/torproject/onionoo/ClientsDocumentWriter.java +++ /dev/null @@ -1,284 +0,0 @@ -/* Copyright 2014 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.SortedMap; -import java.util.SortedSet; -import java.util.TreeMap; -import java.util.TreeSet; - -/* - * Clients status file produced as intermediate output: - * - * 2014-02-15 16:42:11 2014-02-16 00:00:00 - * 259.042 in=86.347,se=86.347 v4=259.042 - * 2014-02-16 00:00:00 2014-02-16 16:42:11 - * 592.958 in=197.653,se=197.653 v4=592.958 - * - * Clients document file produced as output: - * - * "1_month":{ - * "first":"2014-02-03 12:00:00", - * "last":"2014-02-28 12:00:00", - * "interval":86400, - * "factor":0.139049349, - * "count":26, - * "values":[371,354,349,374,432,null,485,458,493,536,null,null,524,576, - * 607,622,null,635,null,566,774,999,945,690,656,681], - * "countries":{"cn":0.0192,"in":0.1768,"ir":0.2487,"ru":0.0104, - * "se":0.1698,"sy":0.0325,"us":0.0406}, - * "transports":{"obfs2":0.4581}, - * "versions":{"v4":1.0000}} - */ -public class ClientsDocumentWriter implements FingerprintListener, - DocumentWriter { - - private DescriptorSource descriptorSource; - - private DocumentStore documentStore; - - private long now; - - public ClientsDocumentWriter() { - this.descriptorSource = ApplicationFactory.getDescriptorSource(); - this.documentStore = ApplicationFactory.getDocumentStore(); - this.now = ApplicationFactory.getTime().currentTimeMillis(); - this.registerFingerprintListeners(); - } - - private void registerFingerprintListeners() { - this.descriptorSource.registerFingerprintListener(this, - DescriptorType.BRIDGE_EXTRA_INFOS); - } - - private SortedSet<String> updateDocuments = new TreeSet<String>(); - - public void processFingerprints(SortedSet<String> fingerprints, - boolean relay) { - if (!relay) { - this.updateDocuments.addAll(fingerprints); - } - } - - private int writtenDocuments = 0; - - public void writeDocuments() { - for (String hashedFingerprint : this.updateDocuments) { - ClientsStatus clientsStatus = this.documentStore.retrieve( - ClientsStatus.class, true, hashedFingerprint); - if (clientsStatus == null) { - continue; - } - SortedSet<ClientsHistory> history = clientsStatus.getHistory(); - ClientsDocument clientsDocument = this.compileClientsDocument( - hashedFingerprint, history); - this.documentStore.store(clientsDocument, hashedFingerprint); - this.writtenDocuments++; - } - Logger.printStatusTime("Wrote clients document files"); - } - - private String[] graphNames = new String[] { - "1_week", - "1_month", - "3_months", - "1_year", - "5_years" }; - - private long[] graphIntervals = new long[] { - DateTimeHelper.ONE_WEEK, - DateTimeHelper.ROUGHLY_ONE_MONTH, - DateTimeHelper.ROUGHLY_THREE_MONTHS, - DateTimeHelper.ROUGHLY_ONE_YEAR, - DateTimeHelper.ROUGHLY_FIVE_YEARS }; - - private long[] dataPointIntervals = new long[] { - DateTimeHelper.ONE_DAY, - DateTimeHelper.ONE_DAY, - DateTimeHelper.ONE_DAY, - DateTimeHelper.TWO_DAYS, - DateTimeHelper.TEN_DAYS }; - - private ClientsDocument compileClientsDocument(String hashedFingerprint, - SortedSet<ClientsHistory> history) { - ClientsDocument clientsDocument = new ClientsDocument(); - clientsDocument.setFingerprint(hashedFingerprint); - Map<String, ClientsGraphHistory> averageClients = - new LinkedHashMap<String, ClientsGraphHistory>(); - for (int graphIntervalIndex = 0; graphIntervalIndex < - this.graphIntervals.length; graphIntervalIndex++) { - String graphName = this.graphNames[graphIntervalIndex]; - ClientsGraphHistory graphHistory = this.compileClientsHistory( - graphIntervalIndex, history); - if (graphHistory != null) { - averageClients.put(graphName, graphHistory); - } - } - clientsDocument.setAverageClients(averageClients); - return clientsDocument; - } - - private ClientsGraphHistory compileClientsHistory( - int graphIntervalIndex, SortedSet<ClientsHistory> history) { - long graphInterval = this.graphIntervals[graphIntervalIndex]; - long dataPointInterval = - this.dataPointIntervals[graphIntervalIndex]; - List<Double> dataPoints = new ArrayList<Double>(); - long intervalStartMillis = ((this.now - graphInterval) - / dataPointInterval) * dataPointInterval; - long millis = 0L; - double responses = 0.0, totalResponses = 0.0; - SortedMap<String, Double> - totalResponsesByCountry = new TreeMap<String, Double>(), - totalResponsesByTransport = new TreeMap<String, Double>(), - totalResponsesByVersion = new TreeMap<String, Double>(); - for (ClientsHistory hist : history) { - if (hist.getEndMillis() < intervalStartMillis) { - continue; - } - while ((intervalStartMillis / dataPointInterval) != - (hist.getEndMillis() / dataPointInterval)) { - dataPoints.add(millis * 2L < dataPointInterval - ? -1.0 : responses * ((double) DateTimeHelper.ONE_DAY) - / (((double) millis) * 10.0)); - responses = 0.0; - millis = 0L; - intervalStartMillis += dataPointInterval; - } - responses += hist.getTotalResponses(); - totalResponses += hist.getTotalResponses(); - for (Map.Entry<String, Double> e : - hist.getResponsesByCountry().entrySet()) { - if (!totalResponsesByCountry.containsKey(e.getKey())) { - totalResponsesByCountry.put(e.getKey(), 0.0); - } - totalResponsesByCountry.put(e.getKey(), e.getValue() - + totalResponsesByCountry.get(e.getKey())); - } - for (Map.Entry<String, Double> e : - hist.getResponsesByTransport().entrySet()) { - if (!totalResponsesByTransport.containsKey(e.getKey())) { - totalResponsesByTransport.put(e.getKey(), 0.0); - } - totalResponsesByTransport.put(e.getKey(), e.getValue() - + totalResponsesByTransport.get(e.getKey())); - } - for (Map.Entry<String, Double> e : - hist.getResponsesByVersion().entrySet()) { - if (!totalResponsesByVersion.containsKey(e.getKey())) { - totalResponsesByVersion.put(e.getKey(), 0.0); - } - totalResponsesByVersion.put(e.getKey(), e.getValue() - + totalResponsesByVersion.get(e.getKey())); - } - millis += (hist.getEndMillis() - hist.getStartMillis()); - } - dataPoints.add(millis * 2L < dataPointInterval - ? -1.0 : responses * ((double) DateTimeHelper.ONE_DAY) - / (((double) millis) * 10.0)); - double maxValue = 0.0; - int firstNonNullIndex = -1, lastNonNullIndex = -1; - for (int dataPointIndex = 0; dataPointIndex < dataPoints.size(); - dataPointIndex++) { - double dataPoint = dataPoints.get(dataPointIndex); - if (dataPoint >= 0.0) { - if (firstNonNullIndex < 0) { - firstNonNullIndex = dataPointIndex; - } - lastNonNullIndex = dataPointIndex; - if (dataPoint > maxValue) { - maxValue = dataPoint; - } - } - } - if (firstNonNullIndex < 0) { - return null; - } - long firstDataPointMillis = (((this.now - graphInterval) - / dataPointInterval) + firstNonNullIndex) * dataPointInterval - + dataPointInterval / 2L; - if (graphIntervalIndex > 0 && firstDataPointMillis >= - this.now - graphIntervals[graphIntervalIndex - 1]) { - /* Skip clients history object, because it doesn't contain - * anything new that wasn't already contained in the last - * clients history object(s). */ - return null; - } - long lastDataPointMillis = firstDataPointMillis - + (lastNonNullIndex - firstNonNullIndex) * dataPointInterval; - double factor = ((double) maxValue) / 999.0; - int count = lastNonNullIndex - firstNonNullIndex + 1; - ClientsGraphHistory graphHistory = new ClientsGraphHistory(); - graphHistory.setFirst(DateTimeHelper.format(firstDataPointMillis)); - graphHistory.setLast(DateTimeHelper.format(lastDataPointMillis)); - graphHistory.setInterval((int) (dataPointInterval - / DateTimeHelper.ONE_SECOND)); - graphHistory.setFactor(factor); - graphHistory.setCount(count); - int previousNonNullIndex = -2; - boolean foundTwoAdjacentDataPoints = false; - List<Integer> values = new ArrayList<Integer>(); - for (int dataPointIndex = firstNonNullIndex; dataPointIndex <= - lastNonNullIndex; dataPointIndex++) { - double dataPoint = dataPoints.get(dataPointIndex); - if (dataPoint >= 0.0) { - if (dataPointIndex - previousNonNullIndex == 1) { - foundTwoAdjacentDataPoints = true; - } - previousNonNullIndex = dataPointIndex; - } - values.add(dataPoint < 0.0 ? null : - (int) ((dataPoint * 999.0) / maxValue)); - } - graphHistory.setValues(values); - if (!totalResponsesByCountry.isEmpty()) { - SortedMap<String, Float> countries = new TreeMap<String, Float>(); - for (Map.Entry<String, Double> e : - totalResponsesByCountry.entrySet()) { - if (e.getValue() > totalResponses / 100.0) { - countries.put(e.getKey(), - (float) (e.getValue() / totalResponses)); - } - } - graphHistory.setCountries(countries); - } - if (!totalResponsesByTransport.isEmpty()) { - SortedMap<String, Float> transports = new TreeMap<String, Float>(); - for (Map.Entry<String, Double> e : - totalResponsesByTransport.entrySet()) { - if (e.getValue() > totalResponses / 100.0) { - transports.put(e.getKey(), - (float) (e.getValue() / totalResponses)); - } - } - graphHistory.setTransports(transports); - } - if (!totalResponsesByVersion.isEmpty()) { - SortedMap<String, Float> versions = new TreeMap<String, Float>(); - for (Map.Entry<String, Double> e : - totalResponsesByVersion.entrySet()) { - if (e.getValue() > totalResponses / 100.0) { - versions.put(e.getKey(), - (float) (e.getValue() / totalResponses)); - } - } - graphHistory.setVersions(versions); - } - if (foundTwoAdjacentDataPoints) { - return graphHistory; - } else { - return null; - } - } - - public String getStatsString() { - StringBuilder sb = new StringBuilder(); - sb.append(" " + Logger.formatDecimalNumber(this.writtenDocuments) - + " clients document files updated\n"); - return sb.toString(); - } -} diff --git a/src/org/torproject/onionoo/ClientsGraphHistory.java b/src/org/torproject/onionoo/ClientsGraphHistory.java deleted file mode 100644 index b7b312b..0000000 --- a/src/org/torproject/onionoo/ClientsGraphHistory.java +++ /dev/null @@ -1,83 +0,0 @@ -/* Copyright 2014 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.util.ArrayList; -import java.util.List; -import java.util.SortedMap; - -public class ClientsGraphHistory { - - private String first; - public void setFirst(String first) { - this.first = first; - } - public String getFirst() { - return this.first; - } - - private String last; - public void setLast(String last) { - this.last = last; - } - public String getLast() { - return this.last; - } - - private Integer interval; - public void setInterval(Integer interval) { - this.interval = interval; - } - public Integer getInterval() { - return this.interval; - } - - private Double factor; - public void setFactor(Double factor) { - this.factor = factor; - } - public Double getFactor() { - return this.factor; - } - - private Integer count; - public void setCount(Integer count) { - this.count = count; - } - public Integer getCount() { - return this.count; - } - - private List<Integer> values = new ArrayList<Integer>(); - public void setValues(List<Integer> values) { - this.values = values; - } - public List<Integer> getValues() { - return this.values; - } - - private SortedMap<String, Float> countries; - public void setCountries(SortedMap<String, Float> countries) { - this.countries = countries; - } - public SortedMap<String, Float> getCountries() { - return this.countries; - } - - private SortedMap<String, Float> transports; - public void setTransports(SortedMap<String, Float> transports) { - this.transports = transports; - } - public SortedMap<String, Float> getTransports() { - return this.transports; - } - - private SortedMap<String, Float> versions; - public void setVersions(SortedMap<String, Float> versions) { - this.versions = versions; - } - public SortedMap<String, Float> getVersions() { - return this.versions; - } -} - diff --git a/src/org/torproject/onionoo/ClientsStatus.java b/src/org/torproject/onionoo/ClientsStatus.java deleted file mode 100644 index 1ea16a7..0000000 --- a/src/org/torproject/onionoo/ClientsStatus.java +++ /dev/null @@ -1,211 +0,0 @@ -/* Copyright 2014 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.util.Map; -import java.util.Scanner; -import java.util.SortedMap; -import java.util.SortedSet; -import java.util.TreeMap; -import java.util.TreeSet; - -class ClientsHistory implements Comparable<ClientsHistory> { - - private long startMillis; - public long getStartMillis() { - return this.startMillis; - } - - private long endMillis; - public long getEndMillis() { - return this.endMillis; - } - - private double totalResponses; - public double getTotalResponses() { - return this.totalResponses; - } - - private SortedMap<String, Double> responsesByCountry; - public SortedMap<String, Double> getResponsesByCountry() { - return this.responsesByCountry; - } - - private SortedMap<String, Double> responsesByTransport; - public SortedMap<String, Double> getResponsesByTransport() { - return this.responsesByTransport; - } - - private SortedMap<String, Double> responsesByVersion; - public SortedMap<String, Double> getResponsesByVersion() { - return this.responsesByVersion; - } - - ClientsHistory(long startMillis, long endMillis, - double totalResponses, - SortedMap<String, Double> responsesByCountry, - SortedMap<String, Double> responsesByTransport, - SortedMap<String, Double> responsesByVersion) { - this.startMillis = startMillis; - this.endMillis = endMillis; - this.totalResponses = totalResponses; - this.responsesByCountry = responsesByCountry; - this.responsesByTransport = responsesByTransport; - this.responsesByVersion = responsesByVersion; - } - - public static ClientsHistory fromString( - String responseHistoryString) { - String[] parts = responseHistoryString.split(" ", 8); - if (parts.length != 8) { - return null; - } - long startMillis = DateTimeHelper.parse(parts[0] + " " + parts[1]); - long endMillis = DateTimeHelper.parse(parts[2] + " " + parts[3]); - if (startMillis < 0L || endMillis < 0L) { - return null; - } - if (startMillis >= endMillis) { - return null; - } - double totalResponses = 0.0; - try { - totalResponses = Double.parseDouble(parts[4]); - } catch (NumberFormatException e) { - return null; - } - SortedMap<String, Double> responsesByCountry = - parseResponses(parts[5]); - SortedMap<String, Double> responsesByTransport = - parseResponses(parts[6]); - SortedMap<String, Double> responsesByVersion = - parseResponses(parts[7]); - if (responsesByCountry == null || responsesByTransport == null || - responsesByVersion == null) { - return null; - } - return new ClientsHistory(startMillis, endMillis, totalResponses, - responsesByCountry, responsesByTransport, responsesByVersion); - } - - private static SortedMap<String, Double> parseResponses( - String responsesString) { - SortedMap<String, Double> responses = new TreeMap<String, Double>(); - if (responsesString.length() > 0) { - for (String pair : responsesString.split(",")) { - String[] keyValue = pair.split("="); - if (keyValue.length != 2) { - return null; - } - double value = 0.0; - try { - value = Double.parseDouble(keyValue[1]); - } catch (NumberFormatException e) { - return null; - } - responses.put(keyValue[0], value); - } - } - return responses; - } - - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(DateTimeHelper.format(startMillis)); - sb.append(" " + DateTimeHelper.format(endMillis)); - sb.append(" " + String.format("%.3f", this.totalResponses)); - this.appendResponses(sb, this.responsesByCountry); - this.appendResponses(sb, this.responsesByTransport); - this.appendResponses(sb, this.responsesByVersion); - return sb.toString(); - } - - private void appendResponses(StringBuilder sb, - SortedMap<String, Double> responses) { - sb.append(" "); - int written = 0; - for (Map.Entry<String, Double> e : responses.entrySet()) { - sb.append((written++ > 0 ? "," : "") + e.getKey() + "=" - + String.format("%.3f", e.getValue())); - } - } - - public void addResponses(ClientsHistory other) { - this.totalResponses += other.totalResponses; - this.addResponsesByCategory(this.responsesByCountry, - other.responsesByCountry); - this.addResponsesByCategory(this.responsesByTransport, - other.responsesByTransport); - this.addResponsesByCategory(this.responsesByVersion, - other.responsesByVersion); - if (this.startMillis > other.startMillis) { - this.startMillis = other.startMillis; - } - if (this.endMillis < other.endMillis) { - this.endMillis = other.endMillis; - } - } - - private void addResponsesByCategory( - SortedMap<String, Double> thisResponses, - SortedMap<String, Double> otherResponses) { - for (Map.Entry<String, Double> e : otherResponses.entrySet()) { - if (thisResponses.containsKey(e.getKey())) { - thisResponses.put(e.getKey(), thisResponses.get(e.getKey()) - + e.getValue()); - } else { - thisResponses.put(e.getKey(), e.getValue()); - } - } - } - - public int compareTo(ClientsHistory other) { - return this.startMillis < other.startMillis ? -1 : - this.startMillis > other.startMillis ? 1 : 0; - } - - public boolean equals(Object other) { - return other instanceof ClientsHistory && - this.startMillis == ((ClientsHistory) other).startMillis; - } - - public int hashCode() { - return (int) this.startMillis; - } -} - -public class ClientsStatus extends Document { - - private SortedSet<ClientsHistory> history = - new TreeSet<ClientsHistory>(); - public void setHistory(SortedSet<ClientsHistory> history) { - this.history = history; - } - public SortedSet<ClientsHistory> getHistory() { - return this.history; - } - - public void fromDocumentString(String documentString) { - Scanner s = new Scanner(documentString); - while (s.hasNextLine()) { - String line = s.nextLine(); - ClientsHistory parsedLine = ClientsHistory.fromString(line); - if (parsedLine != null) { - this.history.add(parsedLine); - } else { - System.err.println("Could not parse clients history line '" - + line + "'. Skipping."); - } - } - s.close(); - } - - public String toDocumentString() { - StringBuilder sb = new StringBuilder(); - for (ClientsHistory interval : this.history) { - sb.append(interval.toString() + "\n"); - } - return sb.toString(); - } -} - diff --git a/src/org/torproject/onionoo/ClientsStatusUpdater.java b/src/org/torproject/onionoo/ClientsStatusUpdater.java deleted file mode 100644 index 4fe2b2d..0000000 --- a/src/org/torproject/onionoo/ClientsStatusUpdater.java +++ /dev/null @@ -1,224 +0,0 @@ -/* Copyright 2014 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.util.Map; -import java.util.SortedMap; -import java.util.SortedSet; -import java.util.TreeMap; -import java.util.TreeSet; - -import org.torproject.descriptor.Descriptor; -import org.torproject.descriptor.ExtraInfoDescriptor; - -/* - * Example extra-info descriptor used as input: - * - * extra-info ndnop2 DE6397A047ABE5F78B4C87AF725047831B221AAB - * dirreq-stats-end 2014-02-16 16:42:11 (86400 s) - * dirreq-v3-resp ok=856,not-enough-sigs=0,unavailable=0,not-found=0, - * not-modified=40,busy=0 - * bridge-stats-end 2014-02-16 16:42:17 (86400 s) - * bridge-ips ??=8,in=8,se=8 - * bridge-ip-versions v4=8,v6=0 - * - * Clients status file produced as intermediate output: - * - * 2014-02-15 16:42:11 2014-02-16 00:00:00 - * 259.042 in=86.347,se=86.347 v4=259.042 - * 2014-02-16 00:00:00 2014-02-16 16:42:11 - * 592.958 in=197.653,se=197.653 v4=592.958 - */ -public class ClientsStatusUpdater implements DescriptorListener, - StatusUpdater { - - private DescriptorSource descriptorSource; - - private DocumentStore documentStore; - - private long now; - - public ClientsStatusUpdater() { - this.descriptorSource = ApplicationFactory.getDescriptorSource(); - this.documentStore = ApplicationFactory.getDocumentStore(); - this.now = ApplicationFactory.getTime().currentTimeMillis(); - this.registerDescriptorListeners(); - } - - private void registerDescriptorListeners() { - this.descriptorSource.registerDescriptorListener(this, - DescriptorType.BRIDGE_EXTRA_INFOS); - } - - public void processDescriptor(Descriptor descriptor, boolean relay) { - if (descriptor instanceof ExtraInfoDescriptor && !relay) { - this.processBridgeExtraInfoDescriptor( - (ExtraInfoDescriptor) descriptor); - } - } - - private SortedMap<String, SortedSet<ClientsHistory>> newResponses = - new TreeMap<String, SortedSet<ClientsHistory>>(); - - private void processBridgeExtraInfoDescriptor( - ExtraInfoDescriptor descriptor) { - long dirreqStatsEndMillis = descriptor.getDirreqStatsEndMillis(); - long dirreqStatsIntervalLengthMillis = - descriptor.getDirreqStatsIntervalLength() - * DateTimeHelper.ONE_SECOND; - SortedMap<String, Integer> responses = descriptor.getDirreqV3Resp(); - if (dirreqStatsEndMillis < 0L || - dirreqStatsIntervalLengthMillis != DateTimeHelper.ONE_DAY || - responses == null || !responses.containsKey("ok")) { - return; - } - double okResponses = (double) (responses.get("ok") - 4); - if (okResponses < 0.0) { - return; - } - String hashedFingerprint = descriptor.getFingerprint().toUpperCase(); - long dirreqStatsStartMillis = dirreqStatsEndMillis - - dirreqStatsIntervalLengthMillis; - long utcBreakMillis = (dirreqStatsEndMillis / DateTimeHelper.ONE_DAY) - * DateTimeHelper.ONE_DAY; - for (int i = 0; i < 2; i++) { - long startMillis = i == 0 ? dirreqStatsStartMillis : utcBreakMillis; - long endMillis = i == 0 ? utcBreakMillis : dirreqStatsEndMillis; - if (startMillis >= endMillis) { - continue; - } - double totalResponses = okResponses - * ((double) (endMillis - startMillis)) - / ((double) DateTimeHelper.ONE_DAY); - SortedMap<String, Double> responsesByCountry = - this.weightResponsesWithUniqueIps(totalResponses, - descriptor.getBridgeIps(), "??"); - SortedMap<String, Double> responsesByTransport = - this.weightResponsesWithUniqueIps(totalResponses, - descriptor.getBridgeIpTransports(), "<??>"); - SortedMap<String, Double> responsesByVersion = - this.weightResponsesWithUniqueIps(totalResponses, - descriptor.getBridgeIpVersions(), ""); - ClientsHistory newResponseHistory = new ClientsHistory( - startMillis, endMillis, totalResponses, responsesByCountry, - responsesByTransport, responsesByVersion); - if (!this.newResponses.containsKey(hashedFingerprint)) { - this.newResponses.put(hashedFingerprint, - new TreeSet<ClientsHistory>()); - } - this.newResponses.get(hashedFingerprint).add( - newResponseHistory); - } - } - - private SortedMap<String, Double> weightResponsesWithUniqueIps( - double totalResponses, SortedMap<String, Integer> uniqueIps, - String omitString) { - SortedMap<String, Double> weightedResponses = - new TreeMap<String, Double>(); - int totalUniqueIps = 0; - if (uniqueIps != null) { - for (Map.Entry<String, Integer> e : uniqueIps.entrySet()) { - if (e.getValue() > 4) { - totalUniqueIps += e.getValue() - 4; - } - } - } - if (totalUniqueIps > 0) { - for (Map.Entry<String, Integer> e : uniqueIps.entrySet()) { - if (!e.getKey().equals(omitString) && e.getValue() > 4) { - weightedResponses.put(e.getKey(), - (((double) (e.getValue() - 4)) * totalResponses) - / ((double) totalUniqueIps)); - } - } - } - return weightedResponses; - } - - public void updateStatuses() { - for (Map.Entry<String, SortedSet<ClientsHistory>> e : - this.newResponses.entrySet()) { - String hashedFingerprint = e.getKey(); - ClientsStatus clientsStatus = this.documentStore.retrieve( - ClientsStatus.class, true, hashedFingerprint); - if (clientsStatus == null) { - clientsStatus = new ClientsStatus(); - } - this.addToHistory(clientsStatus, e.getValue()); - this.compressHistory(clientsStatus); - this.documentStore.store(clientsStatus, hashedFingerprint); - } - } - - private void addToHistory(ClientsStatus clientsStatus, - SortedSet<ClientsHistory> newIntervals) { - SortedSet<ClientsHistory> history = clientsStatus.getHistory(); - for (ClientsHistory interval : newIntervals) { - if ((history.headSet(interval).isEmpty() || - history.headSet(interval).last().getEndMillis() <= - interval.getStartMillis()) && - (history.tailSet(interval).isEmpty() || - history.tailSet(interval).first().getStartMillis() >= - interval.getEndMillis())) { - history.add(interval); - } - } - } - - private void compressHistory(ClientsStatus clientsStatus) { - SortedSet<ClientsHistory> history = clientsStatus.getHistory(); - SortedSet<ClientsHistory> compressedHistory = - new TreeSet<ClientsHistory>(); - ClientsHistory lastResponses = null; - String lastMonthString = "1970-01"; - for (ClientsHistory responses : history) { - long intervalLengthMillis; - if (this.now - responses.getEndMillis() <= - DateTimeHelper.ROUGHLY_THREE_MONTHS) { - intervalLengthMillis = DateTimeHelper.ONE_DAY; - } else if (this.now - responses.getEndMillis() <= - DateTimeHelper.ROUGHLY_ONE_YEAR) { - intervalLengthMillis = DateTimeHelper.TWO_DAYS; - } else { - intervalLengthMillis = DateTimeHelper.TEN_DAYS; - } - String monthString = DateTimeHelper.format( - responses.getStartMillis(), - DateTimeHelper.ISO_YEARMONTH_FORMAT); - if (lastResponses != null && - lastResponses.getEndMillis() == responses.getStartMillis() && - ((lastResponses.getEndMillis() - 1L) / intervalLengthMillis) == - ((responses.getEndMillis() - 1L) / intervalLengthMillis) && - lastMonthString.equals(monthString)) { - lastResponses.addResponses(responses); - } else { - if (lastResponses != null) { - compressedHistory.add(lastResponses); - } - lastResponses = responses; - } - lastMonthString = monthString; - } - if (lastResponses != null) { - compressedHistory.add(lastResponses); - } - clientsStatus.setHistory(compressedHistory); - } - - public String getStatsString() { - int newIntervals = 0; - for (SortedSet<ClientsHistory> hist : this.newResponses.values()) { - newIntervals += hist.size(); - } - StringBuilder sb = new StringBuilder(); - sb.append(" " - + Logger.formatDecimalNumber(newIntervals / 2) - + " client statistics processed from extra-info descriptors\n"); - sb.append(" " - + Logger.formatDecimalNumber(this.newResponses.size()) - + " client status files updated\n"); - return sb.toString(); - } -} - diff --git a/src/org/torproject/onionoo/DateTimeHelper.java b/src/org/torproject/onionoo/DateTimeHelper.java deleted file mode 100644 index 41ac70b..0000000 --- a/src/org/torproject/onionoo/DateTimeHelper.java +++ /dev/null @@ -1,92 +0,0 @@ -/* Copyright 2014 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.HashMap; -import java.util.Map; -import java.util.TimeZone; - -public class DateTimeHelper { - - private DateTimeHelper() { - } - - public static final long ONE_SECOND = 1000L, - TEN_SECONDS = 10L * ONE_SECOND, - ONE_MINUTE = 60L * ONE_SECOND, - FIVE_MINUTES = 5L * ONE_MINUTE, - FIFTEEN_MINUTES = 15L * ONE_MINUTE, - ONE_HOUR = 60L * ONE_MINUTE, - FOUR_HOURS = 4L * ONE_HOUR, - SIX_HOURS = 6L * ONE_HOUR, - TWELVE_HOURS = 12L * ONE_HOUR, - ONE_DAY = 24L * ONE_HOUR, - TWO_DAYS = 2L * ONE_DAY, - THREE_DAYS = 3L * ONE_DAY, - ONE_WEEK = 7L * ONE_DAY, - TEN_DAYS = 10L * ONE_DAY, - ROUGHLY_ONE_MONTH = 31L * ONE_DAY, - ROUGHLY_THREE_MONTHS = 92L * ONE_DAY, - ROUGHLY_ONE_YEAR = 366L * ONE_DAY, - ROUGHLY_FIVE_YEARS = 5L * ROUGHLY_ONE_YEAR; - - public static final String ISO_DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; - - public static final String ISO_DATETIME_TAB_FORMAT = - "yyyy-MM-dd\tHH:mm:ss"; - - public static final String ISO_YEARMONTH_FORMAT = "yyyy-MM"; - - public static final String DATEHOUR_NOSPACE_FORMAT = "yyyy-MM-dd-HH"; - - private static ThreadLocal<Map<String, DateFormat>> dateFormats = - new ThreadLocal<Map<String, DateFormat>> () { - public Map<String, DateFormat> get() { - return super.get(); - } - protected Map<String, DateFormat> initialValue() { - return new HashMap<String, DateFormat>(); - } - public void remove() { - super.remove(); - } - public void set(Map<String, DateFormat> value) { - super.set(value); - } - }; - - private static DateFormat getDateFormat(String format) { - Map<String, DateFormat> threadDateFormats = dateFormats.get(); - if (!threadDateFormats.containsKey(format)) { - DateFormat dateFormat = new SimpleDateFormat(format); - dateFormat.setLenient(false); - dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - threadDateFormats.put(format, dateFormat); - } - return threadDateFormats.get(format); - } - - public static String format(long millis, String format) { - return getDateFormat(format).format(millis); - } - - public static String format(long millis) { - return format(millis, ISO_DATETIME_FORMAT); - } - - public static long parse(String string, String format) { - try { - return getDateFormat(format).parse(string).getTime(); - } catch (ParseException e) { - return -1L; - } - } - - public static long parse(String string) { - return parse(string, ISO_DATETIME_FORMAT); - } -} - diff --git a/src/org/torproject/onionoo/DescriptorSource.java b/src/org/torproject/onionoo/DescriptorSource.java deleted file mode 100644 index 8246bba..0000000 --- a/src/org/torproject/onionoo/DescriptorSource.java +++ /dev/null @@ -1,665 +0,0 @@ -/* Copyright 2013, 2014 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileOutputStream; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.SortedMap; -import java.util.SortedSet; -import java.util.TreeMap; -import java.util.TreeSet; -import java.util.zip.GZIPInputStream; - -import org.torproject.descriptor.BridgeNetworkStatus; -import org.torproject.descriptor.BridgePoolAssignment; -import org.torproject.descriptor.Descriptor; -import org.torproject.descriptor.DescriptorFile; -import org.torproject.descriptor.DescriptorReader; -import org.torproject.descriptor.DescriptorSourceFactory; -import org.torproject.descriptor.ExitList; -import org.torproject.descriptor.ExitListEntry; -import org.torproject.descriptor.ExtraInfoDescriptor; -import org.torproject.descriptor.RelayNetworkStatusConsensus; -import org.torproject.descriptor.ServerDescriptor; - -enum DescriptorType { - RELAY_CONSENSUSES, - RELAY_SERVER_DESCRIPTORS, - RELAY_EXTRA_INFOS, - EXIT_LISTS, - BRIDGE_STATUSES, - BRIDGE_SERVER_DESCRIPTORS, - BRIDGE_EXTRA_INFOS, - BRIDGE_POOL_ASSIGNMENTS, -} - -interface DescriptorListener { - abstract void processDescriptor(Descriptor descriptor, boolean relay); -} - -interface FingerprintListener { - abstract void processFingerprints(SortedSet<String> fingerprints, - boolean relay); -} - -enum DescriptorHistory { - RELAY_CONSENSUS_HISTORY, - RELAY_SERVER_HISTORY, - RELAY_EXTRAINFO_HISTORY, - EXIT_LIST_HISTORY, - BRIDGE_STATUS_HISTORY, - BRIDGE_SERVER_HISTORY, - BRIDGE_EXTRAINFO_HISTORY, - BRIDGE_POOLASSIGN_HISTORY, -} - -class DescriptorDownloader { - - private final String protocolHostNameResourcePrefix = - "https://collector.torproject.org/recent/"; - - private String directory; - - private final File inDir = new File("in/recent"); - - public DescriptorDownloader(DescriptorType descriptorType) { - switch (descriptorType) { - case RELAY_CONSENSUSES: - this.directory = "relay-descriptors/consensuses/"; - break; - case RELAY_SERVER_DESCRIPTORS: - this.directory = "relay-descriptors/server-descriptors/"; - break; - case RELAY_EXTRA_INFOS: - this.directory = "relay-descriptors/extra-infos/"; - break; - case EXIT_LISTS: - this.directory = "exit-lists/"; - break; - case BRIDGE_STATUSES: - this.directory = "bridge-descriptors/statuses/"; - break; - case BRIDGE_SERVER_DESCRIPTORS: - this.directory = "bridge-descriptors/server-descriptors/"; - break; - case BRIDGE_EXTRA_INFOS: - this.directory = "bridge-descriptors/extra-infos/"; - break; - case BRIDGE_POOL_ASSIGNMENTS: - this.directory = "bridge-pool-assignments/"; - break; - default: - System.err.println("Unknown descriptor type."); - return; - } - } - - private SortedSet<String> localFiles = new TreeSet<String>(); - - public int statLocalFiles() { - File localDirectory = new File(this.inDir, this.directory); - if (localDirectory.exists()) { - for (File file : localDirectory.listFiles()) { - this.localFiles.add(file.getName()); - } - } - return this.localFiles.size(); - } - - private SortedSet<String> remoteFiles = new TreeSet<String>(); - - public int fetchRemoteDirectory() { - String directoryUrl = this.protocolHostNameResourcePrefix - + this.directory; - try { - URL u = new URL(directoryUrl); - HttpURLConnection huc = (HttpURLConnection) u.openConnection(); - huc.setRequestMethod("GET"); - huc.connect(); - if (huc.getResponseCode() != 200) { - System.err.println("Could not fetch " + directoryUrl - + ": " + huc.getResponseCode() + " " - + huc.getResponseMessage() + ". Skipping."); - return 0; - } - BufferedReader br = new BufferedReader(new InputStreamReader( - huc.getInputStream())); - String line; - while ((line = br.readLine()) != null) { - if (!line.trim().startsWith("<tr>") || - !line.contains("<a href=\"")) { - continue; - } - String linePart = line.substring( - line.indexOf("<a href=\"") + "<a href=\"".length()); - if (!linePart.contains("\"")) { - continue; - } - linePart = linePart.substring(0, linePart.indexOf("\"")); - if (linePart.endsWith("/")) { - continue; - } - this.remoteFiles.add(linePart); - } - br.close(); - } catch (IOException e) { - System.err.println("Could not fetch or parse " + directoryUrl - + ". Skipping."); - } - return this.remoteFiles.size(); - } - - public int fetchRemoteFiles() { - int fetchedFiles = 0; - for (String remoteFile : this.remoteFiles) { - if (this.localFiles.contains(remoteFile)) { - continue; - } - String fileUrl = this.protocolHostNameResourcePrefix - + this.directory + remoteFile; - File localTempFile = new File(this.inDir, this.directory - + remoteFile + ".tmp"); - File localFile = new File(this.inDir, this.directory + remoteFile); - try { - localFile.getParentFile().mkdirs(); - URL u = new URL(fileUrl); - HttpURLConnection huc = (HttpURLConnection) u.openConnection(); - huc.setRequestMethod("GET"); - huc.addRequestProperty("Accept-Encoding", "gzip"); - huc.connect(); - if (huc.getResponseCode() != 200) { - System.err.println("Could not fetch " + fileUrl - + ": " + huc.getResponseCode() + " " - + huc.getResponseMessage() + ". Skipping."); - continue; - } - long lastModified = huc.getHeaderFieldDate("Last-Modified", -1L); - InputStream is; - if (huc.getContentEncoding() != null && - huc.getContentEncoding().equalsIgnoreCase("gzip")) { - is = new GZIPInputStream(huc.getInputStream()); - } else { - is = huc.getInputStream(); - } - BufferedInputStream bis = new BufferedInputStream(is); - BufferedOutputStream bos = new BufferedOutputStream( - new FileOutputStream(localTempFile)); - int len; - byte[] data = new byte[1024]; - while ((len = bis.read(data, 0, 1024)) >= 0) { - bos.write(data, 0, len); - } - bis.close(); - bos.close(); - localTempFile.renameTo(localFile); - if (lastModified >= 0) { - localFile.setLastModified(lastModified); - } - fetchedFiles++; - } catch (IOException e) { - System.err.println("Could not fetch or store " + fileUrl - + ". Skipping."); - } - } - return fetchedFiles; - } - - public int deleteOldLocalFiles() { - int deletedFiles = 0; - for (String localFile : this.localFiles) { - if (!this.remoteFiles.contains(localFile)) { - new File(this.inDir, this.directory + localFile).delete(); - deletedFiles++; - } - } - return deletedFiles; - } -} - -class DescriptorQueue { - - private File inDir; - - private File statusDir; - - private DescriptorReader descriptorReader; - - private File historyFile; - - private Iterator<DescriptorFile> descriptorFiles; - - private List<Descriptor> descriptors; - - private int historySizeBefore; - public int getHistorySizeBefore() { - return this.historySizeBefore; - } - - private int historySizeAfter; - public int getHistorySizeAfter() { - return this.historySizeAfter; - } - - private long returnedDescriptors = 0L; - public long getReturnedDescriptors() { - return this.returnedDescriptors; - } - - private long returnedBytes = 0L; - public long getReturnedBytes() { - return this.returnedBytes; - } - - public DescriptorQueue(File inDir, File statusDir) { - this.inDir = inDir; - this.statusDir = statusDir; - this.descriptorReader = - DescriptorSourceFactory.createDescriptorReader(); - } - - public void addDirectory(DescriptorType descriptorType) { - String directoryName = null; - switch (descriptorType) { - case RELAY_CONSENSUSES: - directoryName = "relay-descriptors/consensuses"; - break; - case RELAY_SERVER_DESCRIPTORS: - directoryName = "relay-descriptors/server-descriptors"; - break; - case RELAY_EXTRA_INFOS: - directoryName = "relay-descriptors/extra-infos"; - break; - case BRIDGE_STATUSES: - directoryName = "bridge-descriptors/statuses"; - break; - case BRIDGE_SERVER_DESCRIPTORS: - directoryName = "bridge-descriptors/server-descriptors"; - break; - case BRIDGE_EXTRA_INFOS: - directoryName = "bridge-descriptors/extra-infos"; - break; - case BRIDGE_POOL_ASSIGNMENTS: - directoryName = "bridge-pool-assignments"; - break; - case EXIT_LISTS: - directoryName = "exit-lists"; - break; - default: - System.err.println("Unknown descriptor type. Not adding directory " - + "to descriptor reader."); - return; - } - File directory = new File(this.inDir, directoryName); - if (directory.exists() && directory.isDirectory()) { - this.descriptorReader.addDirectory(directory); - this.descriptorReader.setMaxDescriptorFilesInQueue(1); - } else { - System.err.println("Directory " + directory.getAbsolutePath() - + " either does not exist or is not a directory. Not adding " - + "to descriptor reader."); - } - } - - public void readHistoryFile(DescriptorHistory descriptorHistory) { - String historyFileName = null; - switch (descriptorHistory) { - case RELAY_EXTRAINFO_HISTORY: - historyFileName = "relay-extrainfo-history"; - break; - case BRIDGE_EXTRAINFO_HISTORY: - historyFileName = "bridge-extrainfo-history"; - break; - case EXIT_LIST_HISTORY: - historyFileName = "exit-list-history"; - break; - case BRIDGE_POOLASSIGN_HISTORY: - historyFileName = "bridge-poolassign-history"; - break; - case RELAY_CONSENSUS_HISTORY: - historyFileName = "relay-consensus-history"; - break; - case BRIDGE_STATUS_HISTORY: - historyFileName = "bridge-status-history"; - break; - case RELAY_SERVER_HISTORY: - historyFileName = "relay-server-history"; - break; - case BRIDGE_SERVER_HISTORY: - historyFileName = "bridge-server-history"; - break; - default: - System.err.println("Unknown descriptor history. Not excluding " - + "files."); - return; - } - this.historyFile = new File(this.statusDir, historyFileName); - if (this.historyFile.exists() && this.historyFile.isFile()) { - SortedMap<String, Long> excludedFiles = new TreeMap<String, Long>(); - try { - BufferedReader br = new BufferedReader(new FileReader( - this.historyFile)); - String line; - while ((line = br.readLine()) != null) { - try { - String[] parts = line.split(" ", 2); - excludedFiles.put(parts[1], Long.parseLong(parts[0])); - } catch (NumberFormatException e) { - System.err.println("Illegal line '" + line + "' in parse " - + "history. Skipping line."); - } - } - br.close(); - } catch (IOException e) { - System.err.println("Could not read history file '" - + this.historyFile.getAbsolutePath() + "'. Not excluding " - + "descriptors in this execution."); - e.printStackTrace(); - return; - } - this.historySizeBefore = excludedFiles.size(); - this.descriptorReader.setExcludedFiles(excludedFiles); - } - } - - public void writeHistoryFile() { - if (this.historyFile == null) { - return; - } - SortedMap<String, Long> excludedAndParsedFiles = - new TreeMap<String, Long>(); - excludedAndParsedFiles.putAll( - this.descriptorReader.getExcludedFiles()); - excludedAndParsedFiles.putAll(this.descriptorReader.getParsedFiles()); - this.historySizeAfter = excludedAndParsedFiles.size(); - try { - this.historyFile.getParentFile().mkdirs(); - BufferedWriter bw = new BufferedWriter(new FileWriter( - this.historyFile)); - for (Map.Entry<String, Long> e : excludedAndParsedFiles.entrySet()) { - String absolutePath = e.getKey(); - long lastModifiedMillis = e.getValue(); - bw.write(String.valueOf(lastModifiedMillis) + " " + absolutePath - + "\n"); - } - bw.close(); - } catch (IOException e) { - System.err.println("Could not write history file '" - + this.historyFile.getAbsolutePath() + "'. Not excluding " - + "descriptors in next execution."); - return; - } - } - - public Descriptor nextDescriptor() { - Descriptor nextDescriptor = null; - if (this.descriptorFiles == null) { - this.descriptorFiles = this.descriptorReader.readDescriptors(); - } - while (this.descriptors == null && this.descriptorFiles.hasNext()) { - DescriptorFile descriptorFile = this.descriptorFiles.next(); - if (descriptorFile.getException() != null) { - System.err.println("Could not parse " - + descriptorFile.getFileName()); - descriptorFile.getException().printStackTrace(); - } - if (descriptorFile.getDescriptors() != null && - !descriptorFile.getDescriptors().isEmpty()) { - this.descriptors = descriptorFile.getDescriptors(); - } - } - if (this.descriptors != null) { - nextDescriptor = this.descriptors.remove(0); - this.returnedDescriptors++; - this.returnedBytes += nextDescriptor.getRawDescriptorBytes().length; - if (this.descriptors.isEmpty()) { - this.descriptors = null; - } - } - return nextDescriptor; - } -} - -public class DescriptorSource { - - private final File inDir = new File("in/recent"); - - private final File statusDir = new File("status"); - - private List<DescriptorQueue> descriptorQueues; - - public DescriptorSource() { - this.descriptorQueues = new ArrayList<DescriptorQueue>(); - this.descriptorListeners = - new HashMap<DescriptorType, Set<DescriptorListener>>(); - this.fingerprintListeners = - new HashMap<DescriptorType, Set<FingerprintListener>>(); - } - - private DescriptorQueue getDescriptorQueue( - DescriptorType descriptorType, - DescriptorHistory descriptorHistory) { - DescriptorQueue descriptorQueue = new DescriptorQueue(this.inDir, - this.statusDir); - descriptorQueue.addDirectory(descriptorType); - if (descriptorHistory != null) { - descriptorQueue.readHistoryFile(descriptorHistory); - } - this.descriptorQueues.add(descriptorQueue); - return descriptorQueue; - } - - private Map<DescriptorType, Set<DescriptorListener>> - descriptorListeners; - - private Map<DescriptorType, Set<FingerprintListener>> - fingerprintListeners; - - public void registerDescriptorListener(DescriptorListener listener, - DescriptorType descriptorType) { - if (!this.descriptorListeners.containsKey(descriptorType)) { - this.descriptorListeners.put(descriptorType, - new HashSet<DescriptorListener>()); - } - this.descriptorListeners.get(descriptorType).add(listener); - } - - public void registerFingerprintListener(FingerprintListener listener, - DescriptorType descriptorType) { - if (!this.fingerprintListeners.containsKey(descriptorType)) { - this.fingerprintListeners.put(descriptorType, - new HashSet<FingerprintListener>()); - } - this.fingerprintListeners.get(descriptorType).add(listener); - } - - public void downloadDescriptors() { - for (DescriptorType descriptorType : DescriptorType.values()) { - this.downloadDescriptors(descriptorType); - } - } - - private int localFilesBefore = 0, foundRemoteFiles = 0, - downloadedFiles = 0, deletedLocalFiles = 0; - - private void downloadDescriptors(DescriptorType descriptorType) { - if (!this.descriptorListeners.containsKey(descriptorType) && - !this.fingerprintListeners.containsKey(descriptorType)) { - return; - } - DescriptorDownloader descriptorDownloader = - new DescriptorDownloader(descriptorType); - this.localFilesBefore += descriptorDownloader.statLocalFiles(); - this.foundRemoteFiles += - descriptorDownloader.fetchRemoteDirectory(); - this.downloadedFiles += descriptorDownloader.fetchRemoteFiles(); - this.deletedLocalFiles += descriptorDownloader.deleteOldLocalFiles(); - } - - public void readDescriptors() { - /* Careful when changing the order of parsing descriptor types! The - * various status updaters may base assumptions on this order. */ - this.readDescriptors(DescriptorType.RELAY_SERVER_DESCRIPTORS, - DescriptorHistory.RELAY_SERVER_HISTORY, true); - this.readDescriptors(DescriptorType.RELAY_EXTRA_INFOS, - DescriptorHistory.RELAY_EXTRAINFO_HISTORY, true); - this.readDescriptors(DescriptorType.EXIT_LISTS, - DescriptorHistory.EXIT_LIST_HISTORY, true); - this.readDescriptors(DescriptorType.RELAY_CONSENSUSES, - DescriptorHistory.RELAY_CONSENSUS_HISTORY, true); - this.readDescriptors(DescriptorType.BRIDGE_SERVER_DESCRIPTORS, - DescriptorHistory.BRIDGE_SERVER_HISTORY, false); - this.readDescriptors(DescriptorType.BRIDGE_EXTRA_INFOS, - DescriptorHistory.BRIDGE_EXTRAINFO_HISTORY, false); - this.readDescriptors(DescriptorType.BRIDGE_POOL_ASSIGNMENTS, - DescriptorHistory.BRIDGE_POOLASSIGN_HISTORY, false); - this.readDescriptors(DescriptorType.BRIDGE_STATUSES, - DescriptorHistory.BRIDGE_STATUS_HISTORY, false); - } - - private void readDescriptors(DescriptorType descriptorType, - DescriptorHistory descriptorHistory, boolean relay) { - if (!this.descriptorListeners.containsKey(descriptorType) && - !this.fingerprintListeners.containsKey(descriptorType)) { - return; - } - Set<DescriptorListener> descriptorListeners = - this.descriptorListeners.get(descriptorType); - Set<FingerprintListener> fingerprintListeners = - this.fingerprintListeners.get(descriptorType); - DescriptorQueue descriptorQueue = this.getDescriptorQueue( - descriptorType, descriptorHistory); - Descriptor descriptor; - while ((descriptor = descriptorQueue.nextDescriptor()) != null) { - for (DescriptorListener descriptorListener : descriptorListeners) { - descriptorListener.processDescriptor(descriptor, relay); - } - if (fingerprintListeners == null) { - continue; - } - SortedSet<String> fingerprints = new TreeSet<String>(); - if (descriptorType == DescriptorType.RELAY_CONSENSUSES && - descriptor instanceof RelayNetworkStatusConsensus) { - fingerprints.addAll(((RelayNetworkStatusConsensus) descriptor). - getStatusEntries().keySet()); - } else if (descriptorType - == DescriptorType.RELAY_SERVER_DESCRIPTORS && - descriptor instanceof ServerDescriptor) { - fingerprints.add(((ServerDescriptor) descriptor). - getFingerprint()); - } else if (descriptorType == DescriptorType.RELAY_EXTRA_INFOS && - descriptor instanceof ExtraInfoDescriptor) { - fingerprints.add(((ExtraInfoDescriptor) descriptor). - getFingerprint()); - } else if (descriptorType == DescriptorType.EXIT_LISTS && - descriptor instanceof ExitList) { - for (ExitListEntry entry : - ((ExitList) descriptor).getExitListEntries()) { - fingerprints.add(entry.getFingerprint()); - } - } else if (descriptorType == DescriptorType.BRIDGE_STATUSES && - descriptor instanceof BridgeNetworkStatus) { - fingerprints.addAll(((BridgeNetworkStatus) descriptor). - getStatusEntries().keySet()); - } else if (descriptorType == - DescriptorType.BRIDGE_SERVER_DESCRIPTORS && - descriptor instanceof ServerDescriptor) { - fingerprints.add(((ServerDescriptor) descriptor). - getFingerprint()); - } else if (descriptorType == DescriptorType.BRIDGE_EXTRA_INFOS && - descriptor instanceof ExtraInfoDescriptor) { - fingerprints.add(((ExtraInfoDescriptor) descriptor). - getFingerprint()); - } else if (descriptorType == - DescriptorType.BRIDGE_POOL_ASSIGNMENTS && - descriptor instanceof BridgePoolAssignment) { - fingerprints.addAll(((BridgePoolAssignment) descriptor). - getEntries().keySet()); - } - for (FingerprintListener fingerprintListener : - fingerprintListeners) { - fingerprintListener.processFingerprints(fingerprints, relay); - } - } - switch (descriptorType) { - case RELAY_CONSENSUSES: - Logger.printStatusTime("Read relay network consensuses"); - break; - case RELAY_SERVER_DESCRIPTORS: - Logger.printStatusTime("Read relay server descriptors"); - break; - case RELAY_EXTRA_INFOS: - Logger.printStatusTime("Read relay extra-info descriptors"); - break; - case EXIT_LISTS: - Logger.printStatusTime("Read exit lists"); - break; - case BRIDGE_STATUSES: - Logger.printStatusTime("Read bridge network statuses"); - break; - case BRIDGE_SERVER_DESCRIPTORS: - Logger.printStatusTime("Read bridge server descriptors"); - break; - case BRIDGE_EXTRA_INFOS: - Logger.printStatusTime("Read bridge extra-info descriptors"); - break; - case BRIDGE_POOL_ASSIGNMENTS: - Logger.printStatusTime("Read bridge-pool assignments"); - break; - } - } - - public void writeHistoryFiles() { - for (DescriptorQueue descriptorQueue : this.descriptorQueues) { - descriptorQueue.writeHistoryFile(); - } - } - - public String getStatsString() { - StringBuilder sb = new StringBuilder(); - sb.append(" " + this.localFilesBefore + " descriptor files found " - + "locally\n"); - sb.append(" " + this.foundRemoteFiles + " descriptor files found " - + "remotely\n"); - sb.append(" " + this.downloadedFiles + " descriptor files " - + "downloaded from remote\n"); - sb.append(" " + this.deletedLocalFiles + " descriptor files " - + "deleted locally\n"); - sb.append(" " + this.descriptorQueues.size() + " descriptor " - + "queues created\n"); - int historySizeBefore = 0, historySizeAfter = 0; - long descriptors = 0L, bytes = 0L; - for (DescriptorQueue descriptorQueue : descriptorQueues) { - historySizeBefore += descriptorQueue.getHistorySizeBefore(); - historySizeAfter += descriptorQueue.getHistorySizeAfter(); - descriptors += descriptorQueue.getReturnedDescriptors(); - bytes += descriptorQueue.getReturnedBytes(); - } - sb.append(" " + Logger.formatDecimalNumber(historySizeBefore) - + " descriptors excluded from this execution\n"); - sb.append(" " + Logger.formatDecimalNumber(descriptors) - + " descriptors provided\n"); - sb.append(" " + Logger.formatBytes(bytes) + " provided\n"); - sb.append(" " + Logger.formatDecimalNumber(historySizeAfter) - + " descriptors excluded from next execution\n"); - return sb.toString(); - } -} - diff --git a/src/org/torproject/onionoo/DetailsDocument.java b/src/org/torproject/onionoo/DetailsDocument.java deleted file mode 100644 index 2df6e78..0000000 --- a/src/org/torproject/onionoo/DetailsDocument.java +++ /dev/null @@ -1,365 +0,0 @@ -/* Copyright 2013--2014 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.util.List; -import java.util.Map; - -import org.apache.commons.lang.StringEscapeUtils; - -public class DetailsDocument extends Document { - - /* We must ensure that details files only contain ASCII characters - * and no UTF-8 characters. While UTF-8 characters are perfectly - * valid in JSON, this would break compatibility with existing files - * pretty badly. We do this by escaping non-ASCII characters, e.g., - * \u00F2. Gson won't treat this as UTF-8, but will think that we want - * to write six characters '\', 'u', '0', '0', 'F', '2'. The only thing - * we'll have to do is to change back the '\\' that Gson writes for the - * '\'. */ - private static String escapeJSON(String s) { - return s == null ? null : - StringEscapeUtils.escapeJavaScript(s).replaceAll("\\\\'", "'"); - } - private static String unescapeJSON(String s) { - return s == null ? null : - StringEscapeUtils.unescapeJavaScript(s.replaceAll("'", "\\'")); - } - - private String nickname; - public void setNickname(String nickname) { - this.nickname = nickname; - } - public String getNickname() { - return this.nickname; - } - - private String fingerprint; - public void setFingerprint(String fingerprint) { - this.fingerprint = fingerprint; - } - public String getFingerprint() { - return this.fingerprint; - } - - private String hashed_fingerprint; - public void setHashedFingerprint(String hashedFingerprint) { - this.hashed_fingerprint = hashedFingerprint; - } - public String getHashedFingerprint() { - return this.hashed_fingerprint; - } - - private List<String> or_addresses; - public void setOrAddresses(List<String> orAddresses) { - this.or_addresses = orAddresses; - } - public List<String> getOrAddresses() { - return this.or_addresses; - } - - private List<String> exit_addresses; - public void setExitAddresses(List<String> exitAddresses) { - this.exit_addresses = exitAddresses; - } - public List<String> getExitAddresses() { - return this.exit_addresses; - } - - private String dir_address; - public void setDirAddress(String dirAddress) { - this.dir_address = dirAddress; - } - public String getDirAddress() { - return this.dir_address; - } - - private String last_seen; - public void setLastSeen(String lastSeen) { - this.last_seen = lastSeen; - } - public String getLastSeen() { - return this.last_seen; - } - - private String last_changed_address_or_port; - public void setLastChangedAddressOrPort( - String lastChangedAddressOrPort) { - this.last_changed_address_or_port = lastChangedAddressOrPort; - } - public String getLastChangedAddressOrPort() { - return this.last_changed_address_or_port; - } - - private String first_seen; - public void setFirstSeen(String firstSeen) { - this.first_seen = firstSeen; - } - public String getFirstSeen() { - return this.first_seen; - } - - private Boolean running; - public void setRunning(Boolean running) { - this.running = running; - } - public Boolean getRunning() { - return this.running; - } - - private List<String> flags; - public void setFlags(List<String> flags) { - this.flags = flags; - } - public List<String> getFlags() { - return this.flags; - } - - private String country; - public void setCountry(String country) { - this.country = country; - } - public String getCountry() { - return this.country; - } - - private String country_name; - public void setCountryName(String countryName) { - this.country_name = escapeJSON(countryName); - } - public String getCountryName() { - return unescapeJSON(this.country_name); - } - - private String region_name; - public void setRegionName(String regionName) { - this.region_name = escapeJSON(regionName); - } - public String getRegionName() { - return unescapeJSON(this.region_name); - } - - private String city_name; - public void setCityName(String cityName) { - this.city_name = escapeJSON(cityName); - } - public String getCityName() { - return unescapeJSON(this.city_name); - } - - private Float latitude; - public void setLatitude(Float latitude) { - this.latitude = latitude; - } - public Float getLatitude() { - return this.latitude; - } - - private Float longitude; - public void setLongitude(Float longitude) { - this.longitude = longitude; - } - public Float getLongitude() { - return this.longitude; - } - - private String as_number; - public void setAsNumber(String asNumber) { - this.as_number = escapeJSON(asNumber); - } - public String getAsNumber() { - return unescapeJSON(this.as_number); - } - - private String as_name; - public void setAsName(String asName) { - this.as_name = escapeJSON(asName); - } - public String getAsName() { - return unescapeJSON(this.as_name); - } - - private Long consensus_weight; - public void setConsensusWeight(Long consensusWeight) { - this.consensus_weight = consensusWeight; - } - public Long getConsensusWeight() { - return this.consensus_weight; - } - - private String host_name; - public void setHostName(String hostName) { - this.host_name = escapeJSON(hostName); - } - public String getHostName() { - return unescapeJSON(this.host_name); - } - - private String last_restarted; - public void setLastRestarted(String lastRestarted) { - this.last_restarted = lastRestarted; - } - public String getLastRestarted() { - return this.last_restarted; - } - - private Integer bandwidth_rate; - public void setBandwidthRate(Integer bandwidthRate) { - this.bandwidth_rate = bandwidthRate; - } - public Integer getBandwidthRate() { - return this.bandwidth_rate; - } - - private Integer bandwidth_burst; - public void setBandwidthBurst(Integer bandwidthBurst) { - this.bandwidth_burst = bandwidthBurst; - } - public Integer getBandwidthBurst() { - return this.bandwidth_burst; - } - - private Integer observed_bandwidth; - public void setObservedBandwidth(Integer observedBandwidth) { - this.observed_bandwidth = observedBandwidth; - } - public Integer getObservedBandwidth() { - return this.observed_bandwidth; - } - - private Integer advertised_bandwidth; - public void setAdvertisedBandwidth(Integer advertisedBandwidth) { - this.advertised_bandwidth = advertisedBandwidth; - } - public Integer getAdvertisedBandwidth() { - return this.advertised_bandwidth; - } - - private List<String> exit_policy; - public void setExitPolicy(List<String> exitPolicy) { - this.exit_policy = exitPolicy; - } - public List<String> getExitPolicy() { - return this.exit_policy; - } - - private Map<String, List<String>> exit_policy_summary; - public void setExitPolicySummary( - Map<String, List<String>> exitPolicySummary) { - this.exit_policy_summary = exitPolicySummary; - } - public Map<String, List<String>> getExitPolicySummary() { - return this.exit_policy_summary; - } - - private Map<String, List<String>> exit_policy_v6_summary; - public void setExitPolicyV6Summary( - Map<String, List<String>> exitPolicyV6Summary) { - this.exit_policy_v6_summary = exitPolicyV6Summary; - } - public Map<String, List<String>> getExitPolicyV6Summary() { - return this.exit_policy_v6_summary; - } - - private String contact; - public void setContact(String contact) { - this.contact = escapeJSON(contact); - } - public String getContact() { - return unescapeJSON(this.contact); - } - - private String platform; - public void setPlatform(String platform) { - this.platform = escapeJSON(platform); - } - public String getPlatform() { - return unescapeJSON(this.platform); - } - - private List<String> family; - public void setFamily(List<String> family) { - this.family = family; - } - public List<String> getFamily() { - return this.family; - } - - private Float advertised_bandwidth_fraction; - public void setAdvertisedBandwidthFraction( - Float advertisedBandwidthFraction) { - if (advertisedBandwidthFraction == null || - advertisedBandwidthFraction >= 0.0) { - this.advertised_bandwidth_fraction = advertisedBandwidthFraction; - } - } - public Float getAdvertisedBandwidthFraction() { - return this.advertised_bandwidth_fraction; - } - - private Float consensus_weight_fraction; - public void setConsensusWeightFraction(Float consensusWeightFraction) { - if (consensusWeightFraction == null || - consensusWeightFraction >= 0.0) { - this.consensus_weight_fraction = consensusWeightFraction; - } - } - public Float getConsensusWeightFraction() { - return this.consensus_weight_fraction; - } - - private Float guard_probability; - public void setGuardProbability(Float guardProbability) { - if (guardProbability == null || guardProbability >= 0.0) { - this.guard_probability = guardProbability; - } - } - public Float getGuardProbability() { - return this.guard_probability; - } - - private Float middle_probability; - public void setMiddleProbability(Float middleProbability) { - if (middleProbability == null || middleProbability >= 0.0) { - this.middle_probability = middleProbability; - } - } - public Float getMiddleProbability() { - return this.middle_probability; - } - - private Float exit_probability; - public void setExitProbability(Float exitProbability) { - if (exitProbability == null || exitProbability >= 0.0) { - this.exit_probability = exitProbability; - } - } - public Float getExitProbability() { - return this.exit_probability; - } - - private Boolean recommended_version; - public void setRecommendedVersion(Boolean recommendedVersion) { - this.recommended_version = recommendedVersion; - } - public Boolean getRecommendedVersion() { - return this.recommended_version; - } - - private Boolean hibernating; - public void setHibernating(Boolean hibernating) { - this.hibernating = hibernating; - } - public Boolean getHibernating() { - return this.hibernating; - } - - private String pool_assignment; - public void setPoolAssignment(String poolAssignment) { - this.pool_assignment = poolAssignment; - } - public String getPoolAssignment() { - return this.pool_assignment; - } -} - diff --git a/src/org/torproject/onionoo/DetailsDocumentWriter.java b/src/org/torproject/onionoo/DetailsDocumentWriter.java deleted file mode 100644 index a70efd0..0000000 --- a/src/org/torproject/onionoo/DetailsDocumentWriter.java +++ /dev/null @@ -1,222 +0,0 @@ -package org.torproject.onionoo; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.SortedSet; -import java.util.TreeSet; - -public class DetailsDocumentWriter implements FingerprintListener, - DocumentWriter { - - private DescriptorSource descriptorSource; - - private DocumentStore documentStore; - - private long now; - - public DetailsDocumentWriter() { - this.descriptorSource = ApplicationFactory.getDescriptorSource(); - this.documentStore = ApplicationFactory.getDocumentStore(); - this.now = ApplicationFactory.getTime().currentTimeMillis(); - this.registerFingerprintListeners(); - } - - private void registerFingerprintListeners() { - this.descriptorSource.registerFingerprintListener(this, - DescriptorType.RELAY_CONSENSUSES); - this.descriptorSource.registerFingerprintListener(this, - DescriptorType.RELAY_SERVER_DESCRIPTORS); - this.descriptorSource.registerFingerprintListener(this, - DescriptorType.BRIDGE_STATUSES); - this.descriptorSource.registerFingerprintListener(this, - DescriptorType.BRIDGE_SERVER_DESCRIPTORS); - this.descriptorSource.registerFingerprintListener(this, - DescriptorType.BRIDGE_POOL_ASSIGNMENTS); - this.descriptorSource.registerFingerprintListener(this, - DescriptorType.EXIT_LISTS); - } - - private SortedSet<String> newRelays = new TreeSet<String>(), - newBridges = new TreeSet<String>(); - - public void processFingerprints(SortedSet<String> fingerprints, - boolean relay) { - if (relay) { - this.newRelays.addAll(fingerprints); - } else { - this.newBridges.addAll(fingerprints); - } - } - - public void writeDocuments() { - this.updateRelayDetailsFiles(); - this.updateBridgeDetailsFiles(); - Logger.printStatusTime("Wrote details document files"); - } - - private void updateRelayDetailsFiles() { - for (String fingerprint : this.newRelays) { - - /* Generate network-status-specific part. */ - NodeStatus entry = this.documentStore.retrieve(NodeStatus.class, - true, fingerprint); - if (entry == null) { - continue; - } - DetailsDocument detailsDocument = new DetailsDocument(); - detailsDocument.setNickname(entry.getNickname()); - detailsDocument.setFingerprint(fingerprint); - List<String> orAddresses = new ArrayList<String>(); - orAddresses.add(entry.getAddress() + ":" + entry.getOrPort()); - for (String orAddress : entry.getOrAddressesAndPorts()) { - orAddresses.add(orAddress.toLowerCase()); - } - detailsDocument.setOrAddresses(orAddresses); - if (entry.getDirPort() != 0) { - detailsDocument.setDirAddress(entry.getAddress() + ":" - + entry.getDirPort()); - } - detailsDocument.setLastSeen(DateTimeHelper.format( - entry.getLastSeenMillis())); - detailsDocument.setFirstSeen(DateTimeHelper.format( - entry.getFirstSeenMillis())); - detailsDocument.setLastChangedAddressOrPort( - DateTimeHelper.format(entry.getLastChangedOrAddress())); - detailsDocument.setRunning(entry.getRunning()); - if (!entry.getRelayFlags().isEmpty()) { - detailsDocument.setFlags(new ArrayList<String>( - entry.getRelayFlags())); - } - detailsDocument.setCountry(entry.getCountryCode()); - detailsDocument.setLatitude(entry.getLatitude()); - detailsDocument.setLongitude(entry.getLongitude()); - detailsDocument.setCountryName(entry.getCountryName()); - detailsDocument.setRegionName(entry.getRegionName()); - detailsDocument.setCityName(entry.getCityName()); - detailsDocument.setAsNumber(entry.getASNumber()); - detailsDocument.setAsName(entry.getASName()); - detailsDocument.setConsensusWeight(entry.getConsensusWeight()); - detailsDocument.setHostName(entry.getHostName()); - detailsDocument.setAdvertisedBandwidthFraction( - (float) entry.getAdvertisedBandwidthFraction()); - detailsDocument.setConsensusWeightFraction( - (float) entry.getConsensusWeightFraction()); - detailsDocument.setGuardProbability( - (float) entry.getGuardProbability()); - detailsDocument.setMiddleProbability( - (float) entry.getMiddleProbability()); - detailsDocument.setExitProbability( - (float) entry.getExitProbability()); - String defaultPolicy = entry.getDefaultPolicy(); - String portList = entry.getPortList(); - if (defaultPolicy != null && (defaultPolicy.equals("accept") || - defaultPolicy.equals("reject")) && portList != null) { - Map<String, List<String>> exitPolicySummary = - new HashMap<String, List<String>>(); - List<String> portsOrPortRanges = Arrays.asList( - portList.split(",")); - exitPolicySummary.put(defaultPolicy, portsOrPortRanges); - detailsDocument.setExitPolicySummary(exitPolicySummary); - } - detailsDocument.setRecommendedVersion( - entry.getRecommendedVersion()); - - /* Append descriptor-specific part and exit addresses from details - * status file. */ - DetailsStatus detailsStatus = this.documentStore.retrieve( - DetailsStatus.class, true, fingerprint); - if (detailsStatus != null) { - detailsDocument.setLastRestarted( - detailsStatus.getLastRestarted()); - detailsDocument.setBandwidthRate( - detailsStatus.getBandwidthRate()); - detailsDocument.setBandwidthBurst( - detailsStatus.getBandwidthBurst()); - detailsDocument.setObservedBandwidth( - detailsStatus.getObservedBandwidth()); - detailsDocument.setAdvertisedBandwidth( - detailsStatus.getAdvertisedBandwidth()); - detailsDocument.setExitPolicy(detailsStatus.getExitPolicy()); - detailsDocument.setContact(detailsStatus.getContact()); - detailsDocument.setPlatform(detailsStatus.getPlatform()); - detailsDocument.setFamily(detailsStatus.getFamily()); - detailsDocument.setExitPolicyV6Summary( - detailsStatus.getExitPolicyV6Summary()); - detailsDocument.setHibernating(detailsStatus.getHibernating()); - if (detailsStatus.getExitAddresses() != null) { - SortedSet<String> exitAddresses = new TreeSet<String>(); - for (Map.Entry<String, Long> e : - detailsStatus.getExitAddresses().entrySet()) { - String exitAddress = e.getKey().toLowerCase(); - long scanMillis = e.getValue(); - if (!entry.getAddress().equals(exitAddress) && - !entry.getOrAddresses().contains(exitAddress) && - scanMillis >= this.now - DateTimeHelper.ONE_DAY) { - exitAddresses.add(exitAddress); - } - } - if (!exitAddresses.isEmpty()) { - detailsDocument.setExitAddresses(new ArrayList<String>( - exitAddresses)); - } - } - } - - /* Write details file to disk. */ - this.documentStore.store(detailsDocument, fingerprint); - } - } - - private void updateBridgeDetailsFiles() { - for (String fingerprint : this.newBridges) { - - /* Generate network-status-specific part. */ - NodeStatus entry = this.documentStore.retrieve(NodeStatus.class, - true, fingerprint); - if (entry == null) { - continue; - } - DetailsDocument detailsDocument = new DetailsDocument(); - detailsDocument.setNickname(entry.getNickname()); - detailsDocument.setHashedFingerprint(fingerprint); - String address = entry.getAddress(); - List<String> orAddresses = new ArrayList<String>(); - orAddresses.add(address + ":" + entry.getOrPort()); - for (String orAddress : entry.getOrAddressesAndPorts()) { - orAddresses.add(orAddress.toLowerCase()); - } - detailsDocument.setOrAddresses(orAddresses); - detailsDocument.setLastSeen(DateTimeHelper.format( - entry.getLastSeenMillis())); - detailsDocument.setFirstSeen(DateTimeHelper.format( - entry.getFirstSeenMillis())); - detailsDocument.setRunning(entry.getRunning()); - detailsDocument.setFlags(new ArrayList<String>( - entry.getRelayFlags())); - - /* Append descriptor-specific part from details status file. */ - DetailsStatus detailsStatus = this.documentStore.retrieve( - DetailsStatus.class, true, fingerprint); - if (detailsStatus != null) { - detailsDocument.setLastRestarted( - detailsStatus.getLastRestarted()); - detailsDocument.setAdvertisedBandwidth( - detailsStatus.getAdvertisedBandwidth()); - detailsDocument.setPlatform(detailsStatus.getPlatform()); - detailsDocument.setPoolAssignment( - detailsStatus.getPoolAssignment()); - } - - /* Write details file to disk. */ - this.documentStore.store(detailsDocument, fingerprint); - } - } - - public String getStatsString() { - /* TODO Add statistics string. */ - return null; - } -} diff --git a/src/org/torproject/onionoo/DetailsStatus.java b/src/org/torproject/onionoo/DetailsStatus.java deleted file mode 100644 index 1951c89..0000000 --- a/src/org/torproject/onionoo/DetailsStatus.java +++ /dev/null @@ -1,141 +0,0 @@ -/* Copyright 2013--2014 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.util.List; -import java.util.Map; - -import org.apache.commons.lang.StringEscapeUtils; - -public class DetailsStatus extends Document { - - /* We must ensure that details files only contain ASCII characters - * and no UTF-8 characters. While UTF-8 characters are perfectly - * valid in JSON, this would break compatibility with existing files - * pretty badly. We do this by escaping non-ASCII characters, e.g., - * \u00F2. Gson won't treat this as UTF-8, but will think that we want - * to write six characters '\', 'u', '0', '0', 'F', '2'. The only thing - * we'll have to do is to change back the '\\' that Gson writes for the - * '\'. */ - private static String escapeJSON(String s) { - return s == null ? null : - StringEscapeUtils.escapeJavaScript(s).replaceAll("\\\\'", "'"); - } - private static String unescapeJSON(String s) { - return s == null ? null : - StringEscapeUtils.unescapeJavaScript(s.replaceAll("'", "\\'")); - } - - private String desc_published; - public void setDescPublished(String descPublished) { - this.desc_published = descPublished; - } - public String getDescPublished() { - return this.desc_published; - } - - private String last_restarted; - public void setLastRestarted(String lastRestarted) { - this.last_restarted = lastRestarted; - } - public String getLastRestarted() { - return this.last_restarted; - } - - private Integer bandwidth_rate; - public void setBandwidthRate(Integer bandwidthRate) { - this.bandwidth_rate = bandwidthRate; - } - public Integer getBandwidthRate() { - return this.bandwidth_rate; - } - - private Integer bandwidth_burst; - public void setBandwidthBurst(Integer bandwidthBurst) { - this.bandwidth_burst = bandwidthBurst; - } - public Integer getBandwidthBurst() { - return this.bandwidth_burst; - } - - private Integer observed_bandwidth; - public void setObservedBandwidth(Integer observedBandwidth) { - this.observed_bandwidth = observedBandwidth; - } - public Integer getObservedBandwidth() { - return this.observed_bandwidth; - } - - private Integer advertised_bandwidth; - public void setAdvertisedBandwidth(Integer advertisedBandwidth) { - this.advertised_bandwidth = advertisedBandwidth; - } - public Integer getAdvertisedBandwidth() { - return this.advertised_bandwidth; - } - - private List<String> exit_policy; - public void setExitPolicy(List<String> exitPolicy) { - this.exit_policy = exitPolicy; - } - public List<String> getExitPolicy() { - return this.exit_policy; - } - - private String contact; - public void setContact(String contact) { - this.contact = escapeJSON(contact); - } - public String getContact() { - return unescapeJSON(this.contact); - } - - private String platform; - public void setPlatform(String platform) { - this.platform = escapeJSON(platform); - } - public String getPlatform() { - return unescapeJSON(this.platform); - } - - private List<String> family; - public void setFamily(List<String> family) { - this.family = family; - } - public List<String> getFamily() { - return this.family; - } - - private Map<String, List<String>> exit_policy_v6_summary; - public void setExitPolicyV6Summary( - Map<String, List<String>> exitPolicyV6Summary) { - this.exit_policy_v6_summary = exitPolicyV6Summary; - } - public Map<String, List<String>> getExitPolicyV6Summary() { - return this.exit_policy_v6_summary; - } - - private Boolean hibernating; - public void setHibernating(Boolean hibernating) { - this.hibernating = hibernating; - } - public Boolean getHibernating() { - return this.hibernating; - } - - private String pool_assignment; - public void setPoolAssignment(String poolAssignment) { - this.pool_assignment = poolAssignment; - } - public String getPoolAssignment() { - return this.pool_assignment; - } - - private Map<String, Long> exit_addresses; - public void setExitAddresses(Map<String, Long> exitAddresses) { - this.exit_addresses = exitAddresses; - } - public Map<String, Long> getExitAddresses() { - return this.exit_addresses; - } -} diff --git a/src/org/torproject/onionoo/Document.java b/src/org/torproject/onionoo/Document.java deleted file mode 100644 index f49574c..0000000 --- a/src/org/torproject/onionoo/Document.java +++ /dev/null @@ -1,24 +0,0 @@ -/* Copyright 2013 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -public abstract class Document { - - private transient String documentString; - public void setDocumentString(String documentString) { - this.documentString = documentString; - } - public String getDocumentString() { - return this.documentString; - } - - public void fromDocumentString(String documentString) { - /* Subclasses may override this method to parse documentString. */ - } - - public String toDocumentString() { - /* Subclasses may override this method to write documentString. */ - return null; - } -} - diff --git a/src/org/torproject/onionoo/DocumentStore.java b/src/org/torproject/onionoo/DocumentStore.java deleted file mode 100644 index 434ecb6..0000000 --- a/src/org/torproject/onionoo/DocumentStore.java +++ /dev/null @@ -1,744 +0,0 @@ -/* Copyright 2013 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.io.BufferedInputStream; -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.SortedMap; -import java.util.SortedSet; -import java.util.Stack; -import java.util.TreeMap; -import java.util.TreeSet; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonParseException; - -// TODO For later migration from disk to database, do the following: -// - read from database and then from disk if not found -// - write only to database, delete from disk once in database -// - move entirely to database once disk is "empty" -// TODO Also look into simple key-value stores instead of real databases. -public class DocumentStore { - - private final File statusDir = new File("status"); - - private File outDir = new File("out"); - public void setOutDir(File outDir) { - this.outDir = outDir; - } - - private Time time; - - public DocumentStore() { - this.time = ApplicationFactory.getTime(); - } - - private long listOperations = 0L, listedFiles = 0L, storedFiles = 0L, - storedBytes = 0L, retrievedFiles = 0L, retrievedBytes = 0L, - removedFiles = 0L; - - /* Node statuses and summary documents are cached in memory, as opposed - * to all other document types. These caches are initialized when first - * accessing or modifying a NodeStatus or SummaryDocument document, - * respectively. */ - private SortedMap<String, NodeStatus> cachedNodeStatuses; - private SortedMap<String, SummaryDocument> cachedSummaryDocuments; - - public <T extends Document> SortedSet<String> list( - Class<T> documentType) { - if (documentType.equals(NodeStatus.class)) { - return this.listNodeStatuses(); - } else if (documentType.equals(SummaryDocument.class)) { - return this.listSummaryDocuments(); - } else { - return this.listDocumentFiles(documentType); - } - } - - private SortedSet<String> listNodeStatuses() { - if (this.cachedNodeStatuses == null) { - this.cacheNodeStatuses(); - } - return new TreeSet<String>(this.cachedNodeStatuses.keySet()); - } - - private void cacheNodeStatuses() { - SortedMap<String, NodeStatus> parsedNodeStatuses = - new TreeMap<String, NodeStatus>(); - File directory = this.statusDir; - if (directory != null) { - File summaryFile = new File(directory, "summary"); - if (summaryFile.exists()) { - try { - BufferedReader br = new BufferedReader(new FileReader( - summaryFile)); - String line; - while ((line = br.readLine()) != null) { - if (line.length() == 0) { - continue; - } - NodeStatus node = NodeStatus.fromString(line); - if (node != null) { - parsedNodeStatuses.put(node.getFingerprint(), node); - } - } - br.close(); - this.listedFiles += parsedNodeStatuses.size(); - this.listOperations++; - } catch (IOException e) { - System.err.println("Could not read file '" - + summaryFile.getAbsolutePath() + "'."); - e.printStackTrace(); - } - } - } - this.cachedNodeStatuses = parsedNodeStatuses; - } - - private SortedSet<String> listSummaryDocuments() { - if (this.cachedSummaryDocuments == null) { - this.cacheSummaryDocuments(); - } - return new TreeSet<String>(this.cachedSummaryDocuments.keySet()); - } - - private void cacheSummaryDocuments() { - SortedMap<String, SummaryDocument> parsedSummaryDocuments = - new TreeMap<String, SummaryDocument>(); - File directory = this.outDir; - if (directory != null) { - File summaryFile = new File(directory, "summary"); - if (summaryFile.exists()) { - String line = null; - try { - Gson gson = new Gson(); - BufferedReader br = new BufferedReader(new FileReader( - summaryFile)); - while ((line = br.readLine()) != null) { - if (line.length() == 0) { - continue; - } - SummaryDocument summaryDocument = gson.fromJson(line, - SummaryDocument.class); - if (summaryDocument != null) { - parsedSummaryDocuments.put(summaryDocument.getFingerprint(), - summaryDocument); - } - } - br.close(); - this.listedFiles += parsedSummaryDocuments.size(); - this.listOperations++; - } catch (IOException e) { - System.err.println("Could not read file '" - + summaryFile.getAbsolutePath() + "'."); - e.printStackTrace(); - } catch (JsonParseException e) { - System.err.println("Could not parse summary document '" + line - + "' in file '" + summaryFile.getAbsolutePath() + "'."); - e.printStackTrace(); - } - } - } - this.cachedSummaryDocuments = parsedSummaryDocuments; - } - - private <T extends Document> SortedSet<String> listDocumentFiles( - Class<T> documentType) { - SortedSet<String> fingerprints = new TreeSet<String>(); - File directory = null; - String subdirectory = null; - if (documentType.equals(DetailsStatus.class)) { - directory = this.statusDir; - subdirectory = "details"; - } else if (documentType.equals(BandwidthStatus.class)) { - directory = this.statusDir; - subdirectory = "bandwidth"; - } else if (documentType.equals(WeightsStatus.class)) { - directory = this.statusDir; - subdirectory = "weights"; - } else if (documentType.equals(ClientsStatus.class)) { - directory = this.statusDir; - subdirectory = "clients"; - } else if (documentType.equals(UptimeStatus.class)) { - directory = this.statusDir; - subdirectory = "uptimes"; - } else if (documentType.equals(DetailsDocument.class)) { - directory = this.outDir; - subdirectory = "details"; - } else if (documentType.equals(BandwidthDocument.class)) { - directory = this.outDir; - subdirectory = "bandwidth"; - } else if (documentType.equals(WeightsDocument.class)) { - directory = this.outDir; - subdirectory = "weights"; - } else if (documentType.equals(ClientsDocument.class)) { - directory = this.outDir; - subdirectory = "clients"; - } else if (documentType.equals(UptimeDocument.class)) { - directory = this.outDir; - subdirectory = "uptimes"; - } - if (directory != null && subdirectory != null) { - Stack<File> files = new Stack<File>(); - files.add(new File(directory, subdirectory)); - while (!files.isEmpty()) { - File file = files.pop(); - if (file.isDirectory()) { - files.addAll(Arrays.asList(file.listFiles())); - } else if (file.getName().length() == 40) { - fingerprints.add(file.getName()); - } - } - } - this.listOperations++; - this.listedFiles += fingerprints.size(); - return fingerprints; - } - - public <T extends Document> boolean store(T document) { - return this.store(document, null); - } - - public <T extends Document> boolean store(T document, - String fingerprint) { - if (document instanceof NodeStatus) { - return this.storeNodeStatus((NodeStatus) document, fingerprint); - } else if (document instanceof SummaryDocument) { - return this.storeSummaryDocument((SummaryDocument) document, - fingerprint); - } else { - return this.storeDocumentFile(document, fingerprint); - } - } - - private <T extends Document> boolean storeNodeStatus( - NodeStatus nodeStatus, String fingerprint) { - if (this.cachedNodeStatuses == null) { - this.cacheNodeStatuses(); - } - this.cachedNodeStatuses.put(fingerprint, nodeStatus); - return true; - } - - private <T extends Document> boolean storeSummaryDocument( - SummaryDocument summaryDocument, String fingerprint) { - if (this.cachedSummaryDocuments == null) { - this.cacheSummaryDocuments(); - } - this.cachedSummaryDocuments.put(fingerprint, summaryDocument); - return true; - } - - private <T extends Document> boolean storeDocumentFile(T document, - String fingerprint) { - File documentFile = this.getDocumentFile(document.getClass(), - fingerprint); - if (documentFile == null) { - return false; - } - String documentString; - if (document.getDocumentString() != null) { - documentString = document.getDocumentString(); - } else if (document instanceof BandwidthDocument || - document instanceof WeightsDocument || - document instanceof ClientsDocument || - document instanceof UptimeDocument) { - Gson gson = new Gson(); - documentString = gson.toJson(document); - } else if (document instanceof DetailsStatus || - document instanceof DetailsDocument) { - /* Don't escape HTML characters, like < and >, contained in - * strings. */ - Gson gson = new GsonBuilder().disableHtmlEscaping().create(); - /* We must ensure that details files only contain ASCII characters - * and no UTF-8 characters. While UTF-8 characters are perfectly - * valid in JSON, this would break compatibility with existing files - * pretty badly. We already make sure that all strings in details - * objects are escaped JSON, e.g., \u00F2. When Gson serlializes - * this string, it escapes the \ to \\, hence writes \\u00F2. We - * need to undo this and change \\u00F2 back to \u00F2. */ - documentString = gson.toJson(document).replaceAll("\\\\\\\\u", - "\\\\u"); - /* Existing details statuses don't contain opening and closing curly - * brackets, so we should remove them from new details statuses, - * too. */ - if (document instanceof DetailsStatus) { - documentString = documentString.substring( - documentString.indexOf("{") + 1, - documentString.lastIndexOf("}")); - } - } else if (document instanceof BandwidthStatus || - document instanceof WeightsStatus || - document instanceof ClientsStatus || - document instanceof UptimeStatus) { - documentString = document.toDocumentString(); - } else { - System.err.println("Serializing is not supported for type " - + document.getClass().getName() + "."); - return false; - } - try { - documentFile.getParentFile().mkdirs(); - File documentTempFile = new File( - documentFile.getAbsolutePath() + ".tmp"); - BufferedWriter bw = new BufferedWriter(new FileWriter( - documentTempFile)); - bw.write(documentString); - bw.close(); - documentFile.delete(); - documentTempFile.renameTo(documentFile); - this.storedFiles++; - this.storedBytes += documentString.length(); - } catch (IOException e) { - System.err.println("Could not write file '" - + documentFile.getAbsolutePath() + "'."); - e.printStackTrace(); - return false; - } - return true; - } - - public <T extends Document> T retrieve(Class<T> documentType, - boolean parse) { - return this.retrieve(documentType, parse, null); - } - - public <T extends Document> T retrieve(Class<T> documentType, - boolean parse, String fingerprint) { - if (documentType.equals(NodeStatus.class)) { - return documentType.cast(this.retrieveNodeStatus(fingerprint)); - } else if (documentType.equals(SummaryDocument.class)) { - return documentType.cast(this.retrieveSummaryDocument(fingerprint)); - } else { - return this.retrieveDocumentFile(documentType, parse, fingerprint); - } - } - - private NodeStatus retrieveNodeStatus(String fingerprint) { - if (this.cachedNodeStatuses == null) { - this.cacheNodeStatuses(); - } - return this.cachedNodeStatuses.get(fingerprint); - } - - private SummaryDocument retrieveSummaryDocument(String fingerprint) { - if (this.cachedSummaryDocuments == null) { - this.cacheSummaryDocuments(); - } - if (this.cachedSummaryDocuments.containsKey(fingerprint)) { - return this.cachedSummaryDocuments.get(fingerprint); - } - /* TODO This is an evil hack to support looking up relays or bridges - * that haven't been running for a week without having to load - * 500,000 NodeStatus instances into memory. Maybe there's a better - * way? Or do we need to switch to a real database for this? */ - DetailsDocument detailsDocument = this.retrieveDocumentFile( - DetailsDocument.class, true, fingerprint); - if (detailsDocument == null) { - return null; - } - boolean isRelay = detailsDocument.getHashedFingerprint() == null; - boolean running = false; - String nickname = detailsDocument.getNickname(); - List<String> addresses = new ArrayList<String>(); - String countryCode = null, aSNumber = null, contact = null; - for (String orAddressAndPort : detailsDocument.getOrAddresses()) { - if (!orAddressAndPort.contains(":")) { - return null; - } - String orAddress = orAddressAndPort.substring(0, - orAddressAndPort.lastIndexOf(":")); - if (!addresses.contains(orAddress)) { - addresses.add(orAddress); - } - } - if (detailsDocument.getExitAddresses() != null) { - for (String exitAddress : detailsDocument.getExitAddresses()) { - if (!addresses.contains(exitAddress)) { - addresses.add(exitAddress); - } - } - } - SortedSet<String> relayFlags = new TreeSet<String>(), family = null; - long lastSeenMillis = -1L, consensusWeight = -1L, - firstSeenMillis = -1L; - SummaryDocument summaryDocument = new SummaryDocument(isRelay, - nickname, fingerprint, addresses, lastSeenMillis, running, - relayFlags, consensusWeight, countryCode, firstSeenMillis, - aSNumber, contact, family); - return summaryDocument; - } - - private <T extends Document> T retrieveDocumentFile( - Class<T> documentType, boolean parse, String fingerprint) { - File documentFile = this.getDocumentFile(documentType, fingerprint); - if (documentFile == null || !documentFile.exists()) { - return null; - } else if (documentFile.isDirectory()) { - System.err.println("Could not read file '" - + documentFile.getAbsolutePath() + "', because it is a " - + "directory."); - return null; - } - String documentString = null; - try { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - BufferedInputStream bis = new BufferedInputStream( - new FileInputStream(documentFile)); - int len; - byte[] data = new byte[1024]; - while ((len = bis.read(data, 0, 1024)) >= 0) { - baos.write(data, 0, len); - } - bis.close(); - byte[] allData = baos.toByteArray(); - if (allData.length == 0) { - return null; - } - documentString = new String(allData, "US-ASCII"); - this.retrievedFiles++; - this.retrievedBytes += documentString.length(); - } catch (IOException e) { - System.err.println("Could not read file '" - + documentFile.getAbsolutePath() + "'."); - e.printStackTrace(); - return null; - } - T result = null; - if (!parse) { - return this.retrieveUnparsedDocumentFile(documentType, - documentString); - } else if (documentType.equals(DetailsDocument.class) || - documentType.equals(BandwidthDocument.class) || - documentType.equals(WeightsDocument.class) || - documentType.equals(ClientsDocument.class) || - documentType.equals(UptimeDocument.class)) { - return this.retrieveParsedDocumentFile(documentType, - documentString); - } else if (documentType.equals(BandwidthStatus.class) || - documentType.equals(WeightsStatus.class) || - documentType.equals(ClientsStatus.class) || - documentType.equals(UptimeStatus.class)) { - return this.retrieveParsedStatusFile(documentType, documentString); - } else if (documentType.equals(DetailsStatus.class)) { - return this.retrieveParsedDocumentFile(documentType, "{" - + documentString + "}"); - } else { - System.err.println("Parsing is not supported for type " - + documentType.getName() + "."); - } - return result; - } - - private <T extends Document> T retrieveParsedStatusFile( - Class<T> documentType, String documentString) { - T result = null; - try { - result = documentType.newInstance(); - result.fromDocumentString(documentString); - } catch (InstantiationException e) { - /* Handle below. */ - e.printStackTrace(); - } catch (IllegalAccessException e) { - /* Handle below. */ - e.printStackTrace(); - } - if (result == null) { - System.err.println("Could not initialize parsed status file of " - + "type " + documentType.getName() + "."); - } - return result; - } - - private <T extends Document> T retrieveParsedDocumentFile( - Class<T> documentType, String documentString) { - T result = null; - Gson gson = new Gson(); - try { - result = gson.fromJson(documentString, documentType); - } catch (JsonParseException e) { - /* Handle below. */ - e.printStackTrace(); - } - if (result == null) { - System.err.println("Could not initialize parsed document of type " - + documentType.getName() + "."); - } - return result; - } - - private <T extends Document> T retrieveUnparsedDocumentFile( - Class<T> documentType, String documentString) { - T result = null; - try { - result = documentType.newInstance(); - result.setDocumentString(documentString); - } catch (InstantiationException e) { - /* Handle below. */ - e.printStackTrace(); - } catch (IllegalAccessException e) { - /* Handle below. */ - e.printStackTrace(); - } - if (result == null) { - System.err.println("Could not initialize unparsed document of type " - + documentType.getName() + "."); - } - return result; - } - - public <T extends Document> boolean remove(Class<T> documentType) { - return this.remove(documentType, null); - } - - public <T extends Document> boolean remove(Class<T> documentType, - String fingerprint) { - if (documentType.equals(NodeStatus.class)) { - return this.removeNodeStatus(fingerprint); - } else if (documentType.equals(SummaryDocument.class)) { - return this.removeSummaryDocument(fingerprint); - } else { - return this.removeDocumentFile(documentType, fingerprint); - } - } - - private boolean removeNodeStatus(String fingerprint) { - if (this.cachedNodeStatuses == null) { - this.cacheNodeStatuses(); - } - return this.cachedNodeStatuses.remove(fingerprint) != null; - } - - private boolean removeSummaryDocument(String fingerprint) { - if (this.cachedSummaryDocuments == null) { - this.cacheSummaryDocuments(); - } - return this.cachedSummaryDocuments.remove(fingerprint) != null; - } - - private <T extends Document> boolean removeDocumentFile( - Class<T> documentType, String fingerprint) { - File documentFile = this.getDocumentFile(documentType, fingerprint); - if (documentFile == null || !documentFile.delete()) { - System.err.println("Could not delete file '" - + documentFile.getAbsolutePath() + "'."); - return false; - } - this.removedFiles++; - return true; - } - - private <T extends Document> File getDocumentFile(Class<T> documentType, - String fingerprint) { - File documentFile = null; - if (fingerprint == null && !documentType.equals(UpdateStatus.class) && - !documentType.equals(UptimeStatus.class)) { - // TODO Instead of using the update file workaround, add new method - // lastModified(Class<T> documentType) that serves a similar - // purpose. - return null; - } - File directory = null; - String fileName = null; - if (documentType.equals(DetailsStatus.class)) { - directory = this.statusDir; - fileName = String.format("details/%s/%s/%s", - fingerprint.substring(0, 1), fingerprint.substring(1, 2), - fingerprint); - } else if (documentType.equals(BandwidthStatus.class)) { - directory = this.statusDir; - fileName = String.format("bandwidth/%s/%s/%s", - fingerprint.substring(0, 1), fingerprint.substring(1, 2), - fingerprint); - } else if (documentType.equals(WeightsStatus.class)) { - directory = this.statusDir; - fileName = String.format("weights/%s/%s/%s", - fingerprint.substring(0, 1), fingerprint.substring(1, 2), - fingerprint); - } else if (documentType.equals(ClientsStatus.class)) { - directory = this.statusDir; - fileName = String.format("clients/%s/%s/%s", - fingerprint.substring(0, 1), fingerprint.substring(1, 2), - fingerprint); - } else if (documentType.equals(UptimeStatus.class)) { - directory = this.statusDir; - if (fingerprint == null) { - fileName = "uptime"; - } else { - fileName = String.format("uptimes/%s/%s/%s", - fingerprint.substring(0, 1), fingerprint.substring(1, 2), - fingerprint); - } - } else if (documentType.equals(UpdateStatus.class)) { - directory = this.outDir; - fileName = "update"; - } else if (documentType.equals(DetailsDocument.class)) { - directory = this.outDir; - fileName = String.format("details/%s", fingerprint); - } else if (documentType.equals(BandwidthDocument.class)) { - directory = this.outDir; - fileName = String.format("bandwidth/%s", fingerprint); - } else if (documentType.equals(WeightsDocument.class)) { - directory = this.outDir; - fileName = String.format("weights/%s", fingerprint); - } else if (documentType.equals(ClientsDocument.class)) { - directory = this.outDir; - fileName = String.format("clients/%s", fingerprint); - } else if (documentType.equals(UptimeDocument.class)) { - directory = this.outDir; - fileName = String.format("uptimes/%s", fingerprint); - } - if (directory != null && fileName != null) { - documentFile = new File(directory, fileName); - } - return documentFile; - } - - public void flushDocumentCache() { - /* Write cached node statuses to disk, and write update file - * containing current time. It's important to write the update file - * now, not earlier, because the front-end should not read new node - * statuses until all details, bandwidths, and weights are ready. */ - if (this.cachedNodeStatuses != null || - this.cachedSummaryDocuments != null) { - if (this.cachedNodeStatuses != null) { - this.writeNodeStatuses(); - } - if (this.cachedSummaryDocuments != null) { - this.writeSummaryDocuments(); - } - this.writeUpdateStatus(); - } - } - - private void writeNodeStatuses() { - File directory = this.statusDir; - if (directory == null) { - return; - } - File summaryFile = new File(directory, "summary"); - SortedMap<String, NodeStatus> - cachedRelays = new TreeMap<String, NodeStatus>(), - cachedBridges = new TreeMap<String, NodeStatus>(); - for (Map.Entry<String, NodeStatus> e : - this.cachedNodeStatuses.entrySet()) { - if (e.getValue().isRelay()) { - cachedRelays.put(e.getKey(), e.getValue()); - } else { - cachedBridges.put(e.getKey(), e.getValue()); - } - } - StringBuilder sb = new StringBuilder(); - for (NodeStatus relay : cachedRelays.values()) { - String line = relay.toString(); - if (line != null) { - sb.append(line + "\n"); - } else { - System.err.println("Could not serialize relay node status '" - + relay.getFingerprint() + "'"); - } - } - for (NodeStatus bridge : cachedBridges.values()) { - String line = bridge.toString(); - if (line != null) { - sb.append(line + "\n"); - } else { - System.err.println("Could not serialize bridge node status '" - + bridge.getFingerprint() + "'"); - } - } - String documentString = sb.toString(); - try { - summaryFile.getParentFile().mkdirs(); - BufferedWriter bw = new BufferedWriter(new FileWriter(summaryFile)); - bw.write(documentString); - bw.close(); - this.storedFiles++; - this.storedBytes += documentString.length(); - } catch (IOException e) { - System.err.println("Could not write file '" - + summaryFile.getAbsolutePath() + "'."); - e.printStackTrace(); - } - } - - private void writeSummaryDocuments() { - StringBuilder sb = new StringBuilder(); - Gson gson = new Gson(); - for (SummaryDocument summaryDocument : - this.cachedSummaryDocuments.values()) { - String line = gson.toJson(summaryDocument); - if (line != null) { - sb.append(line + "\n"); - } else { - System.err.println("Could not serialize relay summary document '" - + summaryDocument.getFingerprint() + "'"); - } - } - String documentString = sb.toString(); - File summaryFile = new File(this.outDir, "summary"); - try { - summaryFile.getParentFile().mkdirs(); - BufferedWriter bw = new BufferedWriter(new FileWriter(summaryFile)); - bw.write(documentString); - bw.close(); - this.storedFiles++; - this.storedBytes += documentString.length(); - } catch (IOException e) { - System.err.println("Could not write file '" - + summaryFile.getAbsolutePath() + "'."); - e.printStackTrace(); - } - } - - private void writeUpdateStatus() { - if (this.outDir == null) { - return; - } - File updateFile = new File(this.outDir, "update"); - String documentString = String.valueOf(this.time.currentTimeMillis()); - try { - updateFile.getParentFile().mkdirs(); - BufferedWriter bw = new BufferedWriter(new FileWriter(updateFile)); - bw.write(documentString); - bw.close(); - this.storedFiles++; - this.storedBytes += documentString.length(); - } catch (IOException e) { - System.err.println("Could not write file '" - + updateFile.getAbsolutePath() + "'."); - e.printStackTrace(); - } - } - - public String getStatsString() { - StringBuilder sb = new StringBuilder(); - sb.append(" " + Logger.formatDecimalNumber(listOperations) - + " list operations performed\n"); - sb.append(" " + Logger.formatDecimalNumber(listedFiles) - + " files listed\n"); - sb.append(" " + Logger.formatDecimalNumber(storedFiles) - + " files stored\n"); - sb.append(" " + Logger.formatBytes(storedBytes) + " stored\n"); - sb.append(" " + Logger.formatDecimalNumber(retrievedFiles) - + " files retrieved\n"); - sb.append(" " + Logger.formatBytes(retrievedBytes) - + " retrieved\n"); - sb.append(" " + Logger.formatDecimalNumber(removedFiles) - + " files removed\n"); - return sb.toString(); - } -} - diff --git a/src/org/torproject/onionoo/DocumentWriter.java b/src/org/torproject/onionoo/DocumentWriter.java deleted file mode 100644 index 4cdeef9..0000000 --- a/src/org/torproject/onionoo/DocumentWriter.java +++ /dev/null @@ -1,11 +0,0 @@ -/* Copyright 2014 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -public interface DocumentWriter { - - public abstract void writeDocuments(); - - public abstract String getStatsString(); -} - diff --git a/src/org/torproject/onionoo/GraphHistory.java b/src/org/torproject/onionoo/GraphHistory.java deleted file mode 100644 index f03be58..0000000 --- a/src/org/torproject/onionoo/GraphHistory.java +++ /dev/null @@ -1,56 +0,0 @@ -/* Copyright 2014 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.util.List; - -public class GraphHistory { - - private String first; - public void setFirst(String first) { - this.first = first; - } - public String getFirst() { - return this.first; - } - - private String last; - public void setLast(String last) { - this.last = last; - } - public String getLast() { - return this.last; - } - - private Integer interval; - public void setInterval(Integer interval) { - this.interval = interval; - } - public Integer getInterval() { - return this.interval; - } - - private Double factor; - public void setFactor(Double factor) { - this.factor = factor; - } - public Double getFactor() { - return this.factor; - } - - private Integer count; - public void setCount(Integer count) { - this.count = count; - } - public Integer getCount() { - return this.count; - } - - private List<Integer> values; - public void setValues(List<Integer> values) { - this.values = values; - } - public List<Integer> getValues() { - return this.values; - } -} diff --git a/src/org/torproject/onionoo/LockFile.java b/src/org/torproject/onionoo/LockFile.java deleted file mode 100644 index 768db53..0000000 --- a/src/org/torproject/onionoo/LockFile.java +++ /dev/null @@ -1,43 +0,0 @@ -/* Copyright 2013 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; - -public class LockFile { - - private final File lockFile = new File("lock"); - - public boolean acquireLock() { - Time time = ApplicationFactory.getTime(); - try { - if (this.lockFile.exists()) { - return false; - } - if (this.lockFile.getParentFile() != null) { - this.lockFile.getParentFile().mkdirs(); - } - BufferedWriter bw = new BufferedWriter(new FileWriter( - this.lockFile)); - bw.append("" + time.currentTimeMillis() + "\n"); - bw.close(); - return true; - } catch (IOException e) { - System.err.println("Caught exception while trying to acquire " - + "lock!"); - e.printStackTrace(); - return false; - } - } - - public boolean releaseLock() { - if (this.lockFile.exists()) { - this.lockFile.delete(); - } - return !this.lockFile.exists(); - } -} - diff --git a/src/org/torproject/onionoo/Logger.java b/src/org/torproject/onionoo/Logger.java deleted file mode 100644 index 6906a6a..0000000 --- a/src/org/torproject/onionoo/Logger.java +++ /dev/null @@ -1,81 +0,0 @@ -/* Copyright 2013 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.util.Date; - -public class Logger { - - private Logger() { - } - - private static Time time; - - public static void setTime() { - time = ApplicationFactory.getTime(); - } - - private static long currentTimeMillis() { - if (time == null) { - return System.currentTimeMillis(); - } else { - return time.currentTimeMillis(); - } - } - - public static String formatDecimalNumber(long decimalNumber) { - return String.format("%,d", decimalNumber); - } - - public static String formatMillis(long millis) { - return String.format("%02d:%02d.%03d minutes", - millis / DateTimeHelper.ONE_MINUTE, - (millis % DateTimeHelper.ONE_MINUTE) / DateTimeHelper.ONE_SECOND, - millis % DateTimeHelper.ONE_SECOND); - } - - public static String formatBytes(long bytes) { - if (bytes < 1024) { - return bytes + " B"; - } else { - int exp = (int) (Math.log(bytes) / Math.log(1024)); - return String.format("%.1f %siB", bytes / Math.pow(1024, exp), - "KMGTPE".charAt(exp-1)); - } - } - - private static long printedLastStatusMessage = -1L; - - public static void printStatus(String message) { - System.out.println(new Date() + ": " + message); - printedLastStatusMessage = currentTimeMillis(); - } - - public static void printStatistics(String component, String message) { - System.out.print(" " + component + " statistics:\n" + message); - } - - public static void printStatusTime(String message) { - printStatusOrErrorTime(message, false); - } - - public static void printErrorTime(String message) { - printStatusOrErrorTime(message, true); - } - - private static void printStatusOrErrorTime(String message, - boolean printToSystemErr) { - long now = currentTimeMillis(); - long millis = printedLastStatusMessage < 0 ? 0 : - now - printedLastStatusMessage; - String line = " " + message + " (" + Logger.formatMillis(millis) - + ")."; - if (printToSystemErr) { - System.err.println(line); - } else { - System.out.println(line); - } - printedLastStatusMessage = now; - } -} - diff --git a/src/org/torproject/onionoo/LookupService.java b/src/org/torproject/onionoo/LookupService.java deleted file mode 100644 index a968bc0..0000000 --- a/src/org/torproject/onionoo/LookupService.java +++ /dev/null @@ -1,408 +0,0 @@ -/* Copyright 2013 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.SortedMap; -import java.util.SortedSet; -import java.util.TreeMap; -import java.util.TreeSet; -import java.util.regex.Pattern; - -class LookupResult { - - private String countryCode; - public void setCountryCode(String countryCode) { - this.countryCode = countryCode; - } - public String getCountryCode() { - return this.countryCode; - } - - private String countryName; - public void setCountryName(String countryName) { - this.countryName = countryName; - } - public String getCountryName() { - return this.countryName; - } - - private String regionName; - public void setRegionName(String regionName) { - this.regionName = regionName; - } - public String getRegionName() { - return this.regionName; - } - - private String cityName; - public void setCityName(String cityName) { - this.cityName = cityName; - } - public String getCityName() { - return this.cityName; - } - - private Float latitude; - public void setLatitude(Float latitude) { - this.latitude = latitude; - } - public Float getLatitude() { - return this.latitude; - } - - private Float longitude; - public void setLongitude(Float longitude) { - this.longitude = longitude; - } - public Float getLongitude() { - return this.longitude; - } - - private String asNumber; - public void setAsNumber(String asNumber) { - this.asNumber = asNumber; - } - public String getAsNumber() { - return this.asNumber; - } - - private String asName; - public void setAsName(String asName) { - this.asName = asName; - } - public String getAsName() { - return this.asName; - } -} - -public class LookupService { - - private File geoipDir; - private File geoLite2CityBlocksCsvFile; - private File geoLite2CityLocationsCsvFile; - private File geoIPASNum2CsvFile; - private boolean hasAllFiles = false; - public LookupService(File geoipDir) { - this.geoipDir = geoipDir; - this.findRequiredCsvFiles(); - } - - /* Make sure we have all required .csv files. */ - private void findRequiredCsvFiles() { - this.geoLite2CityBlocksCsvFile = new File(this.geoipDir, - "GeoLite2-City-Blocks.csv"); - if (!this.geoLite2CityBlocksCsvFile.exists()) { - System.err.println("No GeoLite2-City-Blocks.csv file in geoip/."); - return; - } - this.geoLite2CityLocationsCsvFile = new File(this.geoipDir, - "GeoLite2-City-Locations.csv"); - if (!this.geoLite2CityLocationsCsvFile.exists()) { - System.err.println("No GeoLite2-City-Locations.csv file in " - + "geoip/."); - return; - } - this.geoIPASNum2CsvFile = new File(this.geoipDir, "GeoIPASNum2.csv"); - if (!this.geoIPASNum2CsvFile.exists()) { - System.err.println("No GeoIPASNum2.csv file in geoip/."); - return; - } - this.hasAllFiles = true; - } - - private Pattern ipv4Pattern = Pattern.compile("^[0-9\\.]{7,15}$"); - private long parseAddressString(String addressString) { - long addressNumber = -1L; - if (ipv4Pattern.matcher(addressString).matches()) { - String[] parts = addressString.split("\\.", 4); - if (parts.length == 4) { - addressNumber = 0L; - for (int i = 0; i < 4; i++) { - addressNumber *= 256L; - int octetValue = -1; - try { - octetValue = Integer.parseInt(parts[i]); - } catch (NumberFormatException e) { - } - if (octetValue < 0 || octetValue > 255) { - addressNumber = -1L; - break; - } - addressNumber += octetValue; - } - } - } - return addressNumber; - } - - public SortedMap<String, LookupResult> lookup( - SortedSet<String> addressStrings) { - - SortedMap<String, LookupResult> lookupResults = - new TreeMap<String, LookupResult>(); - - if (!this.hasAllFiles) { - return lookupResults; - } - - /* Obtain a map from relay IP address strings to numbers. */ - Map<String, Long> addressStringNumbers = new HashMap<String, Long>(); - for (String addressString : addressStrings) { - long addressNumber = this.parseAddressString(addressString); - if (addressNumber >= 0L) { - addressStringNumbers.put(addressString, addressNumber); - } - } - if (addressStringNumbers.isEmpty()) { - return lookupResults; - } - - /* Obtain a map from IP address numbers to blocks and to latitudes and - longitudes. */ - Map<Long, Long> addressNumberBlocks = new HashMap<Long, Long>(); - Map<Long, Float[]> addressNumberLatLong = - new HashMap<Long, Float[]>(); - try { - SortedSet<Long> sortedAddressNumbers = new TreeSet<Long>( - addressStringNumbers.values()); - BufferedReader br = new BufferedReader(new InputStreamReader( - new FileInputStream(geoLite2CityBlocksCsvFile), "ISO-8859-1")); - String line = br.readLine(); - while ((line = br.readLine()) != null) { - if (!line.startsWith("::ffff:")) { - /* TODO Make this less hacky and IPv6-ready at some point. */ - continue; - } - String[] parts = line.replaceAll("\"", "").split(",", 10); - if (parts.length != 10) { - System.err.println("Illegal line '" + line + "' in " - + geoLite2CityBlocksCsvFile.getAbsolutePath() + "."); - br.close(); - return lookupResults; - } - try { - String startAddressString = parts[0].substring(7); /* ::ffff: */ - long startIpNum = this.parseAddressString(startAddressString); - if (startIpNum < 0L) { - System.err.println("Illegal IP address in '" + line - + "' in " + geoLite2CityBlocksCsvFile.getAbsolutePath() - + "."); - br.close(); - return lookupResults; - } - int networkMaskLength = Integer.parseInt(parts[1]); - if (networkMaskLength < 96 || networkMaskLength > 128) { - System.err.println("Illegal network mask in '" + line - + "' in " + geoLite2CityBlocksCsvFile.getAbsolutePath() - + "."); - br.close(); - return lookupResults; - } - if (parts[2].length() == 0 && parts[3].length() == 0) { - continue; - } - long endIpNum = startIpNum + (1 << (128 - networkMaskLength)) - - 1; - for (long addressNumber : sortedAddressNumbers. - tailSet(startIpNum).headSet(endIpNum + 1L)) { - String blockString = parts[2].length() > 0 ? parts[2] : - parts[3]; - long blockNumber = Long.parseLong(blockString); - addressNumberBlocks.put(addressNumber, blockNumber); - if (parts[6].length() > 0 && parts[7].length() > 0) { - addressNumberLatLong.put(addressNumber, - new Float[] { Float.parseFloat(parts[6]), - Float.parseFloat(parts[7]) }); - } - } - } catch (NumberFormatException e) { - System.err.println("Number format exception while parsing line " - + "'" + line + "' in " - + geoLite2CityBlocksCsvFile.getAbsolutePath() + "."); - br.close(); - return lookupResults; - } - } - br.close(); - } catch (IOException e) { - System.err.println("I/O exception while reading " - + geoLite2CityBlocksCsvFile.getAbsolutePath() + "."); - return lookupResults; - } - - /* Obtain a map from relevant blocks to location lines. */ - Map<Long, String> blockLocations = new HashMap<Long, String>(); - try { - Set<Long> blockNumbers = new HashSet<Long>( - addressNumberBlocks.values()); - BufferedReader br = new BufferedReader(new InputStreamReader( - new FileInputStream(geoLite2CityLocationsCsvFile), - "ISO-8859-1")); - String line = br.readLine(); - while ((line = br.readLine()) != null) { - String[] parts = line.replaceAll("\"", "").split(",", 10); - if (parts.length != 10) { - System.err.println("Illegal line '" + line + "' in " - + geoLite2CityLocationsCsvFile.getAbsolutePath() + "."); - br.close(); - return lookupResults; - } - try { - long locId = Long.parseLong(parts[0]); - if (blockNumbers.contains(locId)) { - blockLocations.put(locId, line); - } - } catch (NumberFormatException e) { - System.err.println("Number format exception while parsing line " - + "'" + line + "' in " - + geoLite2CityLocationsCsvFile.getAbsolutePath() + "."); - br.close(); - return lookupResults; - } - } - br.close(); - } catch (IOException e) { - System.err.println("I/O exception while reading " - + geoLite2CityLocationsCsvFile.getAbsolutePath() + "."); - return lookupResults; - } - - /* Obtain a map from IP address numbers to ASN. */ - Map<Long, String> addressNumberASN = new HashMap<Long, String>(); - try { - SortedSet<Long> sortedAddressNumbers = new TreeSet<Long>( - addressStringNumbers.values()); - long firstAddressNumber = sortedAddressNumbers.first(); - BufferedReader br = new BufferedReader(new InputStreamReader( - new FileInputStream(geoIPASNum2CsvFile), "ISO-8859-1")); - String line; - long previousStartIpNum = -1L; - while ((line = br.readLine()) != null) { - String[] parts = line.replaceAll("\"", "").split(",", 3); - if (parts.length != 3) { - System.err.println("Illegal line '" + line + "' in " - + geoIPASNum2CsvFile.getAbsolutePath() + "."); - br.close(); - return lookupResults; - } - try { - long startIpNum = Long.parseLong(parts[0]); - if (startIpNum <= previousStartIpNum) { - System.err.println("Line '" + line + "' not sorted in " - + geoIPASNum2CsvFile.getAbsolutePath() + "."); - br.close(); - return lookupResults; - } - previousStartIpNum = startIpNum; - while (firstAddressNumber < startIpNum && - firstAddressNumber != -1L) { - sortedAddressNumbers.remove(firstAddressNumber); - if (sortedAddressNumbers.isEmpty()) { - firstAddressNumber = -1L; - } else { - firstAddressNumber = sortedAddressNumbers.first(); - } - } - long endIpNum = Long.parseLong(parts[1]); - while (firstAddressNumber <= endIpNum && - firstAddressNumber != -1L) { - if (parts[2].startsWith("AS") && - parts[2].split(" ", 2).length == 2) { - addressNumberASN.put(firstAddressNumber, parts[2]); - } - sortedAddressNumbers.remove(firstAddressNumber); - if (sortedAddressNumbers.isEmpty()) { - firstAddressNumber = -1L; - } else { - firstAddressNumber = sortedAddressNumbers.first(); - } - } - if (firstAddressNumber == -1L) { - break; - } - } - catch (NumberFormatException e) { - System.err.println("Number format exception while parsing line " - + "'" + line + "' in " - + geoIPASNum2CsvFile.getAbsolutePath() + "."); - br.close(); - return lookupResults; - } - } - br.close(); - } catch (IOException e) { - System.err.println("I/O exception while reading " - + geoIPASNum2CsvFile.getAbsolutePath() + "."); - return lookupResults; - } - - /* Finally, put together lookup results. */ - for (String addressString : addressStrings) { - if (!addressStringNumbers.containsKey(addressString)) { - continue; - } - long addressNumber = addressStringNumbers.get(addressString); - if (!addressNumberBlocks.containsKey(addressNumber) && - !addressNumberLatLong.containsKey(addressNumber) && - !addressNumberASN.containsKey(addressNumber)) { - continue; - } - LookupResult lookupResult = new LookupResult(); - if (addressNumberBlocks.containsKey(addressNumber)) { - long blockNumber = addressNumberBlocks.get(addressNumber); - if (blockLocations.containsKey(blockNumber)) { - String[] parts = blockLocations.get(blockNumber). - replaceAll("\"", "").split(",", -1); - lookupResult.setCountryCode(parts[3].toLowerCase()); - if (parts[4].length() > 0) { - lookupResult.setCountryName(parts[4]); - } - if (parts[6].length() > 0) { - lookupResult.setRegionName(parts[6]); - } - if (parts[7].length() > 0) { - lookupResult.setCityName(parts[7]); - } - } - } - if (addressNumberLatLong.containsKey(addressNumber)) { - Float[] latLong = addressNumberLatLong.get(addressNumber); - lookupResult.setLatitude(latLong[0]); - lookupResult.setLongitude(latLong[1]); - } - if (addressNumberASN.containsKey(addressNumber)) { - String[] parts = addressNumberASN.get(addressNumber).split(" ", - 2); - lookupResult.setAsNumber(parts[0]); - lookupResult.setAsName(parts[1]); - } - lookupResults.put(addressString, lookupResult); - } - - /* Keep statistics. */ - this.addressesLookedUp += addressStrings.size(); - this.addressesResolved += lookupResults.size(); - - return lookupResults; - } - - private int addressesLookedUp = 0, addressesResolved = 0; - - public String getStatsString() { - StringBuilder sb = new StringBuilder(); - sb.append(" " + Logger.formatDecimalNumber(addressesLookedUp) - + " addresses looked up\n"); - sb.append(" " + Logger.formatDecimalNumber(addressesResolved) - + " addresses resolved\n"); - return sb.toString(); - } -} diff --git a/src/org/torproject/onionoo/Main.java b/src/org/torproject/onionoo/Main.java deleted file mode 100644 index e9afe2f..0000000 --- a/src/org/torproject/onionoo/Main.java +++ /dev/null @@ -1,119 +0,0 @@ -/* Copyright 2011, 2012 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.io.File; - -/* Update search data and status data files. */ -public class Main { - - private Main() { - } - - public static void main(String[] args) { - - LockFile lf = new LockFile(); - Logger.setTime(); - Logger.printStatus("Initializing."); - if (lf.acquireLock()) { - Logger.printStatusTime("Acquired lock"); - } else { - Logger.printErrorTime("Could not acquire lock. Is Onionoo " - + "already running? Terminating"); - return; - } - - DescriptorSource dso = ApplicationFactory.getDescriptorSource(); - Logger.printStatusTime("Initialized descriptor source"); - DocumentStore ds = ApplicationFactory.getDocumentStore(); - Logger.printStatusTime("Initialized document store"); - LookupService ls = new LookupService(new File("geoip")); - Logger.printStatusTime("Initialized Geoip lookup service"); - ReverseDomainNameResolver rdnr = new ReverseDomainNameResolver(); - Logger.printStatusTime("Initialized reverse domain name resolver"); - NodeDetailsStatusUpdater ndsu = new NodeDetailsStatusUpdater(rdnr, - ls); - Logger.printStatusTime("Initialized node data writer"); - BandwidthStatusUpdater bsu = new BandwidthStatusUpdater(); - Logger.printStatusTime("Initialized bandwidth status updater"); - WeightsStatusUpdater wsu = new WeightsStatusUpdater(); - Logger.printStatusTime("Initialized weights status updater"); - ClientsStatusUpdater csu = new ClientsStatusUpdater(); - Logger.printStatusTime("Initialized clients status updater"); - UptimeStatusUpdater usu = new UptimeStatusUpdater(); - Logger.printStatusTime("Initialized uptime status updater"); - StatusUpdater[] sus = new StatusUpdater[] { ndsu, bsu, wsu, csu, - usu }; - - SummaryDocumentWriter sdw = new SummaryDocumentWriter(); - Logger.printStatusTime("Initialized summary document writer"); - DetailsDocumentWriter ddw = new DetailsDocumentWriter(); - Logger.printStatusTime("Initialized details document writer"); - BandwidthDocumentWriter bdw = new BandwidthDocumentWriter(); - Logger.printStatusTime("Initialized bandwidth document writer"); - WeightsDocumentWriter wdw = new WeightsDocumentWriter(); - Logger.printStatusTime("Initialized weights document writer"); - ClientsDocumentWriter cdw = new ClientsDocumentWriter(); - Logger.printStatusTime("Initialized clients document writer"); - UptimeDocumentWriter udw = new UptimeDocumentWriter(); - Logger.printStatusTime("Initialized uptime document writer"); - DocumentWriter[] dws = new DocumentWriter[] { sdw, ddw, bdw, wdw, cdw, - udw }; - - Logger.printStatus("Downloading descriptors."); - dso.downloadDescriptors(); - - Logger.printStatus("Reading descriptors."); - dso.readDescriptors(); - - Logger.printStatus("Updating internal status files."); - for (StatusUpdater su : sus) { - su.updateStatuses(); - Logger.printStatusTime(su.getClass().getSimpleName() - + " updated status files"); - } - - Logger.printStatus("Updating document files."); - for (DocumentWriter dw : dws) { - dw.writeDocuments(); - } - - Logger.printStatus("Shutting down."); - dso.writeHistoryFiles(); - Logger.printStatusTime("Wrote parse histories"); - ds.flushDocumentCache(); - Logger.printStatusTime("Flushed document cache"); - - Logger.printStatus("Gathering statistics."); - for (StatusUpdater su : sus) { - String statsString = su.getStatsString(); - if (statsString != null) { - Logger.printStatistics(su.getClass().getSimpleName(), - statsString); - } - } - for (DocumentWriter dw : dws) { - String statsString = dw.getStatsString(); - if (statsString != null) { - Logger.printStatistics(dw.getClass().getSimpleName(), - statsString); - } - } - Logger.printStatistics("Descriptor source", dso.getStatsString()); - Logger.printStatistics("Document store", ds.getStatsString()); - Logger.printStatistics("GeoIP lookup service", ls.getStatsString()); - Logger.printStatistics("Reverse domain name resolver", - rdnr.getStatsString()); - - Logger.printStatus("Releasing lock."); - if (lf.releaseLock()) { - Logger.printStatusTime("Released lock"); - } else { - Logger.printErrorTime("Could not release lock. The next " - + "execution may not start as expected"); - } - - Logger.printStatus("Terminating."); - } -} - diff --git a/src/org/torproject/onionoo/NodeDetailsStatusUpdater.java b/src/org/torproject/onionoo/NodeDetailsStatusUpdater.java deleted file mode 100644 index a1149a5..0000000 --- a/src/org/torproject/onionoo/NodeDetailsStatusUpdater.java +++ /dev/null @@ -1,620 +0,0 @@ -/* Copyright 2011--2014 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.SortedMap; -import java.util.SortedSet; -import java.util.TreeMap; -import java.util.TreeSet; - -import org.torproject.descriptor.BridgeNetworkStatus; -import org.torproject.descriptor.BridgePoolAssignment; -import org.torproject.descriptor.Descriptor; -import org.torproject.descriptor.ExitList; -import org.torproject.descriptor.ExitListEntry; -import org.torproject.descriptor.NetworkStatusEntry; -import org.torproject.descriptor.RelayNetworkStatusConsensus; -import org.torproject.descriptor.ServerDescriptor; - -public class NodeDetailsStatusUpdater implements DescriptorListener, - StatusUpdater { - - private DescriptorSource descriptorSource; - - private ReverseDomainNameResolver reverseDomainNameResolver; - - private LookupService lookupService; - - private DocumentStore documentStore; - - private long now; - - private SortedMap<String, NodeStatus> knownNodes = - new TreeMap<String, NodeStatus>(); - - private SortedMap<String, NodeStatus> relays; - - private SortedMap<String, NodeStatus> bridges; - - private long relaysLastValidAfterMillis = -1L; - - private long bridgesLastPublishedMillis = -1L; - - private SortedMap<String, Integer> lastBandwidthWeights = null; - - private int relayConsensusesProcessed = 0, bridgeStatusesProcessed = 0; - - public NodeDetailsStatusUpdater( - ReverseDomainNameResolver reverseDomainNameResolver, - LookupService lookupService) { - this.descriptorSource = ApplicationFactory.getDescriptorSource(); - this.reverseDomainNameResolver = reverseDomainNameResolver; - this.lookupService = lookupService; - this.documentStore = ApplicationFactory.getDocumentStore(); - this.now = ApplicationFactory.getTime().currentTimeMillis(); - this.registerDescriptorListeners(); - } - - private void registerDescriptorListeners() { - this.descriptorSource.registerDescriptorListener(this, - DescriptorType.RELAY_CONSENSUSES); - this.descriptorSource.registerDescriptorListener(this, - DescriptorType.RELAY_SERVER_DESCRIPTORS); - this.descriptorSource.registerDescriptorListener(this, - DescriptorType.BRIDGE_STATUSES); - this.descriptorSource.registerDescriptorListener(this, - DescriptorType.BRIDGE_SERVER_DESCRIPTORS); - this.descriptorSource.registerDescriptorListener(this, - DescriptorType.BRIDGE_POOL_ASSIGNMENTS); - this.descriptorSource.registerDescriptorListener(this, - DescriptorType.EXIT_LISTS); - } - - public void processDescriptor(Descriptor descriptor, boolean relay) { - if (descriptor instanceof ServerDescriptor && relay) { - this.processRelayServerDescriptor((ServerDescriptor) descriptor); - } else if (descriptor instanceof ExitList) { - this.processExitList((ExitList) descriptor); - } else if (descriptor instanceof RelayNetworkStatusConsensus) { - this.processRelayNetworkStatusConsensus( - (RelayNetworkStatusConsensus) descriptor); - } else if (descriptor instanceof ServerDescriptor && !relay) { - this.processBridgeServerDescriptor((ServerDescriptor) descriptor); - } else if (descriptor instanceof BridgePoolAssignment) { - this.processBridgePoolAssignment((BridgePoolAssignment) descriptor); - } else if (descriptor instanceof BridgeNetworkStatus) { - this.processBridgeNetworkStatus((BridgeNetworkStatus) descriptor); - } - } - - private void processRelayServerDescriptor( - ServerDescriptor descriptor) { - String fingerprint = descriptor.getFingerprint(); - DetailsStatus detailsStatus = this.documentStore.retrieve( - DetailsStatus.class, true, fingerprint); - String publishedDateTime = - DateTimeHelper.format(descriptor.getPublishedMillis()); - if (detailsStatus == null) { - detailsStatus = new DetailsStatus(); - } else if (detailsStatus.getDescPublished() != null && - publishedDateTime.compareTo( - detailsStatus.getDescPublished()) < 0) { - return; - } - String lastRestartedString = DateTimeHelper.format( - descriptor.getPublishedMillis() - descriptor.getUptime() - * DateTimeHelper.ONE_SECOND); - int bandwidthRate = descriptor.getBandwidthRate(); - int bandwidthBurst = descriptor.getBandwidthBurst(); - int observedBandwidth = descriptor.getBandwidthObserved(); - int advertisedBandwidth = Math.min(bandwidthRate, - Math.min(bandwidthBurst, observedBandwidth)); - detailsStatus.setDescPublished(publishedDateTime); - detailsStatus.setLastRestarted(lastRestartedString); - detailsStatus.setBandwidthRate(bandwidthRate); - detailsStatus.setBandwidthBurst(bandwidthBurst); - detailsStatus.setObservedBandwidth(observedBandwidth); - detailsStatus.setAdvertisedBandwidth(advertisedBandwidth); - detailsStatus.setExitPolicy(descriptor.getExitPolicyLines()); - detailsStatus.setContact(descriptor.getContact()); - detailsStatus.setPlatform(descriptor.getPlatform()); - detailsStatus.setFamily(descriptor.getFamilyEntries()); - if (descriptor.getIpv6DefaultPolicy() != null && - (descriptor.getIpv6DefaultPolicy().equals("accept") || - descriptor.getIpv6DefaultPolicy().equals("reject")) && - descriptor.getIpv6PortList() != null) { - Map<String, List<String>> exitPolicyV6Summary = - new HashMap<String, List<String>>(); - List<String> portsOrPortRanges = Arrays.asList( - descriptor.getIpv6PortList().split(",")); - exitPolicyV6Summary.put(descriptor.getIpv6DefaultPolicy(), - portsOrPortRanges); - detailsStatus.setExitPolicyV6Summary(exitPolicyV6Summary); - } - if (descriptor.isHibernating()) { - detailsStatus.setHibernating(true); - } - this.documentStore.store(detailsStatus, fingerprint); - } - - private Map<String, Map<String, Long>> exitListEntries = - new HashMap<String, Map<String, Long>>(); - - private void processExitList(ExitList exitList) { - for (ExitListEntry exitListEntry : exitList.getExitListEntries()) { - String fingerprint = exitListEntry.getFingerprint(); - if (exitListEntry.getScanMillis() < - this.now - DateTimeHelper.ONE_DAY) { - continue; - } - if (!this.exitListEntries.containsKey(fingerprint)) { - this.exitListEntries.put(fingerprint, - new HashMap<String, Long>()); - } - String exitAddress = exitListEntry.getExitAddress(); - long scanMillis = exitListEntry.getScanMillis(); - if (!this.exitListEntries.get(fingerprint).containsKey(exitAddress) - || this.exitListEntries.get(fingerprint).get(exitAddress) - < scanMillis) { - this.exitListEntries.get(fingerprint).put(exitAddress, - scanMillis); - } - } - } - - private void processRelayNetworkStatusConsensus( - RelayNetworkStatusConsensus consensus) { - long validAfterMillis = consensus.getValidAfterMillis(); - if (validAfterMillis > this.relaysLastValidAfterMillis) { - this.relaysLastValidAfterMillis = validAfterMillis; - } - Set<String> recommendedVersions = null; - if (consensus.getRecommendedServerVersions() != null) { - recommendedVersions = new HashSet<String>(); - for (String recommendedVersion : - consensus.getRecommendedServerVersions()) { - recommendedVersions.add("Tor " + recommendedVersion); - } - } - for (NetworkStatusEntry entry : - consensus.getStatusEntries().values()) { - String nickname = entry.getNickname(); - String fingerprint = entry.getFingerprint(); - String address = entry.getAddress(); - SortedSet<String> orAddressesAndPorts = new TreeSet<String>( - entry.getOrAddresses()); - int orPort = entry.getOrPort(); - int dirPort = entry.getDirPort(); - SortedSet<String> relayFlags = entry.getFlags(); - long consensusWeight = entry.getBandwidth(); - String defaultPolicy = entry.getDefaultPolicy(); - String portList = entry.getPortList(); - Boolean recommendedVersion = (recommendedVersions == null || - entry.getVersion() == null) ? null : - recommendedVersions.contains(entry.getVersion()); - NodeStatus newNodeStatus = new NodeStatus(true, nickname, - fingerprint, address, orAddressesAndPorts, null, - validAfterMillis, orPort, dirPort, relayFlags, consensusWeight, - null, null, -1L, defaultPolicy, portList, validAfterMillis, - validAfterMillis, null, null, recommendedVersion, null); - if (this.knownNodes.containsKey(fingerprint)) { - this.knownNodes.get(fingerprint).update(newNodeStatus); - } else { - this.knownNodes.put(fingerprint, newNodeStatus); - } - } - this.relayConsensusesProcessed++; - if (this.relaysLastValidAfterMillis == validAfterMillis) { - this.lastBandwidthWeights = consensus.getBandwidthWeights(); - } - } - - private void processBridgeServerDescriptor( - ServerDescriptor descriptor) { - String fingerprint = descriptor.getFingerprint(); - DetailsStatus detailsStatus = this.documentStore.retrieve( - DetailsStatus.class, true, fingerprint); - String publishedDateTime = - DateTimeHelper.format(descriptor.getPublishedMillis()); - if (detailsStatus == null) { - detailsStatus = new DetailsStatus(); - } else if (detailsStatus.getDescPublished() != null && - publishedDateTime.compareTo( - detailsStatus.getDescPublished()) < 0) { - return; - } - String lastRestartedString = DateTimeHelper.format( - descriptor.getPublishedMillis() - descriptor.getUptime() - * DateTimeHelper.ONE_SECOND); - int advertisedBandwidth = Math.min(descriptor.getBandwidthRate(), - Math.min(descriptor.getBandwidthBurst(), - descriptor.getBandwidthObserved())); - detailsStatus.setDescPublished(publishedDateTime); - detailsStatus.setLastRestarted(lastRestartedString); - detailsStatus.setAdvertisedBandwidth(advertisedBandwidth); - detailsStatus.setPlatform(descriptor.getPlatform()); - this.documentStore.store(detailsStatus, fingerprint); - } - - private void processBridgePoolAssignment( - BridgePoolAssignment bridgePoolAssignment) { - for (Map.Entry<String, String> e : - bridgePoolAssignment.getEntries().entrySet()) { - String fingerprint = e.getKey(); - String details = e.getValue(); - DetailsStatus detailsStatus = this.documentStore.retrieve( - DetailsStatus.class, true, fingerprint); - if (detailsStatus == null) { - detailsStatus = new DetailsStatus(); - } else if (details.equals(detailsStatus.getPoolAssignment())) { - continue; - } - detailsStatus.setPoolAssignment(details); - this.documentStore.store(detailsStatus, fingerprint); - } - } - - private void processBridgeNetworkStatus(BridgeNetworkStatus status) { - long publishedMillis = status.getPublishedMillis(); - if (publishedMillis > this.bridgesLastPublishedMillis) { - this.bridgesLastPublishedMillis = publishedMillis; - } - for (NetworkStatusEntry entry : status.getStatusEntries().values()) { - String nickname = entry.getNickname(); - String fingerprint = entry.getFingerprint(); - String address = entry.getAddress(); - SortedSet<String> orAddressesAndPorts = new TreeSet<String>( - entry.getOrAddresses()); - int orPort = entry.getOrPort(); - int dirPort = entry.getDirPort(); - SortedSet<String> relayFlags = entry.getFlags(); - NodeStatus newNodeStatus = new NodeStatus(false, nickname, - fingerprint, address, orAddressesAndPorts, null, - publishedMillis, orPort, dirPort, relayFlags, -1L, "??", null, - -1L, null, null, publishedMillis, -1L, null, null, null, null); - if (this.knownNodes.containsKey(fingerprint)) { - this.knownNodes.get(fingerprint).update(newNodeStatus); - } else { - this.knownNodes.put(fingerprint, newNodeStatus); - } - } - this.bridgeStatusesProcessed++; - } - - public void updateStatuses() { - this.readStatusSummary(); - Logger.printStatusTime("Read status summary"); - this.setCurrentNodes(); - Logger.printStatusTime("Set current node fingerprints"); - this.startReverseDomainNameLookups(); - Logger.printStatusTime("Started reverse domain name lookups"); - this.lookUpCitiesAndASes(); - Logger.printStatusTime("Looked up cities and ASes"); - this.setDescriptorPartsOfNodeStatus(); - Logger.printStatusTime("Set descriptor parts of node statuses."); - this.calculatePathSelectionProbabilities(); - Logger.printStatusTime("Calculated path selection probabilities"); - this.finishReverseDomainNameLookups(); - Logger.printStatusTime("Finished reverse domain name lookups"); - this.writeStatusSummary(); - Logger.printStatusTime("Wrote status summary"); - this.updateDetailsStatuses(); - Logger.printStatusTime("Updated exit addresses in details statuses"); - } - - private void readStatusSummary() { - SortedSet<String> fingerprints = this.documentStore.list( - NodeStatus.class); - for (String fingerprint : fingerprints) { - NodeStatus node = this.documentStore.retrieve(NodeStatus.class, - true, fingerprint); - if (node.isRelay()) { - this.relaysLastValidAfterMillis = Math.max( - this.relaysLastValidAfterMillis, node.getLastSeenMillis()); - } else { - this.bridgesLastPublishedMillis = Math.max( - this.bridgesLastPublishedMillis, node.getLastSeenMillis()); - } - if (this.knownNodes.containsKey(fingerprint)) { - this.knownNodes.get(fingerprint).update(node); - } else { - this.knownNodes.put(fingerprint, node); - } - } - } - - private void setCurrentNodes() { - long cutoff = Math.max(this.relaysLastValidAfterMillis, - this.bridgesLastPublishedMillis) - 7L * 24L * 60L * 60L * 1000L; - SortedMap<String, NodeStatus> currentNodes = - new TreeMap<String, NodeStatus>(); - for (Map.Entry<String, NodeStatus> e : this.knownNodes.entrySet()) { - if (e.getValue().getLastSeenMillis() >= cutoff) { - currentNodes.put(e.getKey(), e.getValue()); - } - } - this.relays = new TreeMap<String, NodeStatus>(); - this.bridges = new TreeMap<String, NodeStatus>(); - for (Map.Entry<String, NodeStatus> e : currentNodes.entrySet()) { - if (e.getValue().isRelay()) { - this.relays.put(e.getKey(), e.getValue()); - } else { - this.bridges.put(e.getKey(), e.getValue()); - } - } - } - - private void startReverseDomainNameLookups() { - Map<String, Long> addressLastLookupTimes = - new HashMap<String, Long>(); - for (NodeStatus relay : relays.values()) { - addressLastLookupTimes.put(relay.getAddress(), - relay.getLastRdnsLookup()); - } - this.reverseDomainNameResolver.setAddresses(addressLastLookupTimes); - this.reverseDomainNameResolver.startReverseDomainNameLookups(); - } - - private void lookUpCitiesAndASes() { - SortedSet<String> addressStrings = new TreeSet<String>(); - for (NodeStatus node : this.knownNodes.values()) { - if (node.isRelay()) { - addressStrings.add(node.getAddress()); - } - } - if (addressStrings.isEmpty()) { - System.err.println("No relay IP addresses to resolve to cities or " - + "ASN."); - return; - } - SortedMap<String, LookupResult> lookupResults = - this.lookupService.lookup(addressStrings); - for (NodeStatus node : knownNodes.values()) { - if (!node.isRelay()) { - continue; - } - String addressString = node.getAddress(); - if (lookupResults.containsKey(addressString)) { - LookupResult lookupResult = lookupResults.get(addressString); - node.setCountryCode(lookupResult.getCountryCode()); - node.setCountryName(lookupResult.getCountryName()); - node.setRegionName(lookupResult.getRegionName()); - node.setCityName(lookupResult.getCityName()); - node.setLatitude(lookupResult.getLatitude()); - node.setLongitude(lookupResult.getLongitude()); - node.setASNumber(lookupResult.getAsNumber()); - node.setASName(lookupResult.getAsName()); - } - } - } - - private void setDescriptorPartsOfNodeStatus() { - for (Map.Entry<String, NodeStatus> e : this.knownNodes.entrySet()) { - String fingerprint = e.getKey(); - NodeStatus node = e.getValue(); - if (node.isRelay()) { - if (node.getRelayFlags().contains("Running") && - node.getLastSeenMillis() == this.relaysLastValidAfterMillis) { - node.setRunning(true); - } - DetailsStatus detailsStatus = this.documentStore.retrieve( - DetailsStatus.class, true, fingerprint); - if (detailsStatus != null) { - node.setContact(detailsStatus.getContact()); - if (detailsStatus.getExitAddresses() != null) { - for (Map.Entry<String, Long> ea : - detailsStatus.getExitAddresses().entrySet()) { - if (ea.getValue() >= this.now - DateTimeHelper.ONE_DAY) { - node.addExitAddress(ea.getKey()); - } - } - } - if (detailsStatus.getFamily() != null && - !detailsStatus.getFamily().isEmpty()) { - SortedSet<String> familyFingerprints = new TreeSet<String>(); - for (String familyMember : detailsStatus.getFamily()) { - if (familyMember.startsWith("$") && - familyMember.length() == 41) { - familyFingerprints.add(familyMember.substring(1)); - } - } - if (!familyFingerprints.isEmpty()) { - node.setFamilyFingerprints(familyFingerprints); - } - } - } - } - if (!node.isRelay() && node.getRelayFlags().contains("Running") && - node.getLastSeenMillis() == this.bridgesLastPublishedMillis) { - node.setRunning(true); - } - } - } - - private void calculatePathSelectionProbabilities() { - boolean consensusContainsBandwidthWeights = false; - double wgg = 0.0, wgd = 0.0, wmg = 0.0, wmm = 0.0, wme = 0.0, - wmd = 0.0, wee = 0.0, wed = 0.0; - if (this.lastBandwidthWeights != null) { - SortedSet<String> weightKeys = new TreeSet<String>(Arrays.asList( - "Wgg,Wgd,Wmg,Wmm,Wme,Wmd,Wee,Wed".split(","))); - weightKeys.removeAll(this.lastBandwidthWeights.keySet()); - if (weightKeys.isEmpty()) { - consensusContainsBandwidthWeights = true; - wgg = ((double) this.lastBandwidthWeights.get("Wgg")) / 10000.0; - wgd = ((double) this.lastBandwidthWeights.get("Wgd")) / 10000.0; - wmg = ((double) this.lastBandwidthWeights.get("Wmg")) / 10000.0; - wmm = ((double) this.lastBandwidthWeights.get("Wmm")) / 10000.0; - wme = ((double) this.lastBandwidthWeights.get("Wme")) / 10000.0; - wmd = ((double) this.lastBandwidthWeights.get("Wmd")) / 10000.0; - wee = ((double) this.lastBandwidthWeights.get("Wee")) / 10000.0; - wed = ((double) this.lastBandwidthWeights.get("Wed")) / 10000.0; - } - } else { - System.err.println("Could not determine most recent Wxx parameter " - + "values, probably because we didn't parse a consensus in " - + "this execution. All relays' guard/middle/exit weights are " - + "going to be 0.0."); - } - SortedMap<String, Double> - advertisedBandwidths = new TreeMap<String, Double>(), - consensusWeights = new TreeMap<String, Double>(), - guardWeights = new TreeMap<String, Double>(), - middleWeights = new TreeMap<String, Double>(), - exitWeights = new TreeMap<String, Double>(); - double totalAdvertisedBandwidth = 0.0; - double totalConsensusWeight = 0.0; - double totalGuardWeight = 0.0; - double totalMiddleWeight = 0.0; - double totalExitWeight = 0.0; - for (Map.Entry<String, NodeStatus> e : this.relays.entrySet()) { - String fingerprint = e.getKey(); - NodeStatus relay = e.getValue(); - if (!relay.getRunning()) { - continue; - } - boolean isExit = relay.getRelayFlags().contains("Exit") && - !relay.getRelayFlags().contains("BadExit"); - boolean isGuard = relay.getRelayFlags().contains("Guard"); - DetailsStatus detailsStatus = this.documentStore.retrieve( - DetailsStatus.class, true, fingerprint); - if (detailsStatus != null) { - double advertisedBandwidth = - detailsStatus.getAdvertisedBandwidth(); - if (advertisedBandwidth >= 0.0) { - advertisedBandwidths.put(fingerprint, advertisedBandwidth); - totalAdvertisedBandwidth += advertisedBandwidth; - } - } - double consensusWeight = (double) relay.getConsensusWeight(); - consensusWeights.put(fingerprint, consensusWeight); - totalConsensusWeight += consensusWeight; - if (consensusContainsBandwidthWeights) { - double guardWeight = consensusWeight, - middleWeight = consensusWeight, - exitWeight = consensusWeight; - if (isGuard && isExit) { - guardWeight *= wgd; - middleWeight *= wmd; - exitWeight *= wed; - } else if (isGuard) { - guardWeight *= wgg; - middleWeight *= wmg; - exitWeight = 0.0; - } else if (isExit) { - guardWeight = 0.0; - middleWeight *= wme; - exitWeight *= wee; - } else { - guardWeight = 0.0; - middleWeight *= wmm; - exitWeight = 0.0; - } - guardWeights.put(fingerprint, guardWeight); - middleWeights.put(fingerprint, middleWeight); - exitWeights.put(fingerprint, exitWeight); - totalGuardWeight += guardWeight; - totalMiddleWeight += middleWeight; - totalExitWeight += exitWeight; - } - } - for (Map.Entry<String, NodeStatus> e : this.relays.entrySet()) { - String fingerprint = e.getKey(); - NodeStatus relay = e.getValue(); - if (advertisedBandwidths.containsKey(fingerprint)) { - relay.setAdvertisedBandwidthFraction(advertisedBandwidths.get( - fingerprint) / totalAdvertisedBandwidth); - } - if (consensusWeights.containsKey(fingerprint)) { - relay.setConsensusWeightFraction(consensusWeights.get(fingerprint) - / totalConsensusWeight); - } - if (guardWeights.containsKey(fingerprint)) { - relay.setGuardProbability(guardWeights.get(fingerprint) - / totalGuardWeight); - } - if (middleWeights.containsKey(fingerprint)) { - relay.setMiddleProbability(middleWeights.get(fingerprint) - / totalMiddleWeight); - } - if (exitWeights.containsKey(fingerprint)) { - relay.setExitProbability(exitWeights.get(fingerprint) - / totalExitWeight); - } - } - } - - private void finishReverseDomainNameLookups() { - this.reverseDomainNameResolver.finishReverseDomainNameLookups(); - Map<String, String> lookupResults = - this.reverseDomainNameResolver.getLookupResults(); - long startedRdnsLookups = - this.reverseDomainNameResolver.getLookupStartMillis(); - for (NodeStatus relay : relays.values()) { - if (lookupResults.containsKey(relay.getAddress())) { - relay.setHostName(lookupResults.get(relay.getAddress())); - relay.setLastRdnsLookup(startedRdnsLookups); - } - } - } - - private void writeStatusSummary() { - for (Map.Entry<String, NodeStatus> e : this.knownNodes.entrySet()) { - this.documentStore.store(e.getValue(), e.getKey()); - } - } - - private void updateDetailsStatuses() { - SortedSet<String> fingerprints = new TreeSet<String>(); - fingerprints.addAll(this.exitListEntries.keySet()); - for (String fingerprint : fingerprints) { - DetailsStatus detailsStatus = this.documentStore.retrieve( - DetailsStatus.class, true, fingerprint); - if (detailsStatus == null) { - detailsStatus = new DetailsStatus(); - } - Map<String, Long> exitAddresses = new HashMap<String, Long>(); - if (detailsStatus.getExitAddresses() != null) { - for (Map.Entry<String, Long> e : - detailsStatus.getExitAddresses().entrySet()) { - if (e.getValue() >= this.now - DateTimeHelper.ONE_DAY) { - exitAddresses.put(e.getKey(), e.getValue()); - } - } - } - if (this.exitListEntries.containsKey(fingerprint)) { - for (Map.Entry<String, Long> e : - this.exitListEntries.get(fingerprint).entrySet()) { - if (!exitAddresses.containsKey(e.getKey()) || - exitAddresses.get(e.getKey()) < e.getValue()) { - exitAddresses.put(e.getKey(), e.getValue()); - } - } - } - if (this.knownNodes.containsKey(fingerprint)) { - for (String orAddress : - this.knownNodes.get(fingerprint).getOrAddresses()) { - this.exitListEntries.remove(orAddress); - } - } - detailsStatus.setExitAddresses(exitAddresses); - this.documentStore.store(detailsStatus, fingerprint); - } - } - - public String getStatsString() { - StringBuilder sb = new StringBuilder(); - sb.append(" " + Logger.formatDecimalNumber( - relayConsensusesProcessed) + " relay consensuses processed\n"); - sb.append(" " + Logger.formatDecimalNumber(bridgeStatusesProcessed) - + " bridge statuses processed\n"); - return sb.toString(); - } -} - diff --git a/src/org/torproject/onionoo/NodeIndexer.java b/src/org/torproject/onionoo/NodeIndexer.java deleted file mode 100644 index d24284e..0000000 --- a/src/org/torproject/onionoo/NodeIndexer.java +++ /dev/null @@ -1,425 +0,0 @@ -package org.torproject.onionoo; - -import java.io.File; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.SortedMap; -import java.util.SortedSet; -import java.util.TreeMap; - -import javax.servlet.ServletContext; -import javax.servlet.ServletContextEvent; -import javax.servlet.ServletContextListener; - -class NodeIndex { - - private String relaysPublishedString; - public void setRelaysPublishedString(String relaysPublishedString) { - this.relaysPublishedString = relaysPublishedString; - } - public String getRelaysPublishedString() { - return relaysPublishedString; - } - - private String bridgesPublishedString; - public void setBridgesPublishedString(String bridgesPublishedString) { - this.bridgesPublishedString = bridgesPublishedString; - } - public String getBridgesPublishedString() { - return bridgesPublishedString; - } - - private List<String> relaysByConsensusWeight; - public void setRelaysByConsensusWeight( - List<String> relaysByConsensusWeight) { - this.relaysByConsensusWeight = relaysByConsensusWeight; - } - public List<String> getRelaysByConsensusWeight() { - return relaysByConsensusWeight; - } - - - private Map<String, SummaryDocument> relayFingerprintSummaryLines; - public void setRelayFingerprintSummaryLines( - Map<String, SummaryDocument> relayFingerprintSummaryLines) { - this.relayFingerprintSummaryLines = relayFingerprintSummaryLines; - } - public Map<String, SummaryDocument> getRelayFingerprintSummaryLines() { - return this.relayFingerprintSummaryLines; - } - - private Map<String, SummaryDocument> bridgeFingerprintSummaryLines; - public void setBridgeFingerprintSummaryLines( - Map<String, SummaryDocument> bridgeFingerprintSummaryLines) { - this.bridgeFingerprintSummaryLines = bridgeFingerprintSummaryLines; - } - public Map<String, SummaryDocument> getBridgeFingerprintSummaryLines() { - return this.bridgeFingerprintSummaryLines; - } - - private Map<String, Set<String>> relaysByCountryCode = null; - public void setRelaysByCountryCode( - Map<String, Set<String>> relaysByCountryCode) { - this.relaysByCountryCode = relaysByCountryCode; - } - public Map<String, Set<String>> getRelaysByCountryCode() { - return relaysByCountryCode; - } - - private Map<String, Set<String>> relaysByASNumber = null; - public void setRelaysByASNumber( - Map<String, Set<String>> relaysByASNumber) { - this.relaysByASNumber = relaysByASNumber; - } - public Map<String, Set<String>> getRelaysByASNumber() { - return relaysByASNumber; - } - - private Map<String, Set<String>> relaysByFlag = null; - public void setRelaysByFlag(Map<String, Set<String>> relaysByFlag) { - this.relaysByFlag = relaysByFlag; - } - public Map<String, Set<String>> getRelaysByFlag() { - return relaysByFlag; - } - - private Map<String, Set<String>> bridgesByFlag = null; - public void setBridgesByFlag(Map<String, Set<String>> bridgesByFlag) { - this.bridgesByFlag = bridgesByFlag; - } - public Map<String, Set<String>> getBridgesByFlag() { - return bridgesByFlag; - } - - private Map<String, Set<String>> relaysByContact = null; - public void setRelaysByContact( - Map<String, Set<String>> relaysByContact) { - this.relaysByContact = relaysByContact; - } - public Map<String, Set<String>> getRelaysByContact() { - return relaysByContact; - } - - private Map<String, Set<String>> relaysByFamily = null; - public void setRelaysByFamily(Map<String, Set<String>> relaysByFamily) { - this.relaysByFamily = relaysByFamily; - } - public Map<String, Set<String>> getRelaysByFamily() { - return this.relaysByFamily; - } - - private SortedMap<Integer, Set<String>> relaysByFirstSeenDays; - public void setRelaysByFirstSeenDays( - SortedMap<Integer, Set<String>> relaysByFirstSeenDays) { - this.relaysByFirstSeenDays = relaysByFirstSeenDays; - } - public SortedMap<Integer, Set<String>> getRelaysByFirstSeenDays() { - return relaysByFirstSeenDays; - } - - private SortedMap<Integer, Set<String>> bridgesByFirstSeenDays; - public void setBridgesByFirstSeenDays( - SortedMap<Integer, Set<String>> bridgesByFirstSeenDays) { - this.bridgesByFirstSeenDays = bridgesByFirstSeenDays; - } - public SortedMap<Integer, Set<String>> getBridgesByFirstSeenDays() { - return bridgesByFirstSeenDays; - } - - private SortedMap<Integer, Set<String>> relaysByLastSeenDays; - public void setRelaysByLastSeenDays( - SortedMap<Integer, Set<String>> relaysByLastSeenDays) { - this.relaysByLastSeenDays = relaysByLastSeenDays; - } - public SortedMap<Integer, Set<String>> getRelaysByLastSeenDays() { - return relaysByLastSeenDays; - } - - private SortedMap<Integer, Set<String>> bridgesByLastSeenDays; - public void setBridgesByLastSeenDays( - SortedMap<Integer, Set<String>> bridgesByLastSeenDays) { - this.bridgesByLastSeenDays = bridgesByLastSeenDays; - } - public SortedMap<Integer, Set<String>> getBridgesByLastSeenDays() { - return bridgesByLastSeenDays; - } -} - -public class NodeIndexer implements ServletContextListener, Runnable { - - public void contextInitialized(ServletContextEvent contextEvent) { - ServletContext servletContext = contextEvent.getServletContext(); - File outDir = new File(servletContext.getInitParameter("outDir")); - DocumentStore documentStore = ApplicationFactory.getDocumentStore(); - documentStore.setOutDir(outDir); - /* The servlet container created us, and we need to avoid that - * ApplicationFactory creates another instance of us. */ - ApplicationFactory.setNodeIndexer(this); - this.startIndexing(); - } - - public void contextDestroyed(ServletContextEvent contextEvent) { - this.stopIndexing(); - } - - private long lastIndexed = -1L; - - private NodeIndex latestNodeIndex = null; - - private Thread nodeIndexerThread = null; - - public synchronized long getLastIndexed(long timeoutMillis) { - if (this.lastIndexed == -1L && this.nodeIndexerThread != null && - timeoutMillis > 0L) { - try { - this.wait(timeoutMillis); - } catch (InterruptedException e) { - } - } - return this.lastIndexed; - } - - public synchronized NodeIndex getLatestNodeIndex(long timeoutMillis) { - if (this.latestNodeIndex == null && this.nodeIndexerThread != null && - timeoutMillis > 0L) { - try { - this.wait(timeoutMillis); - } catch (InterruptedException e) { - } - } - return this.latestNodeIndex; - } - - public synchronized void startIndexing() { - if (this.nodeIndexerThread == null) { - this.nodeIndexerThread = new Thread(this); - this.nodeIndexerThread.setDaemon(true); - this.nodeIndexerThread.start(); - } - } - - public void run() { - while (this.nodeIndexerThread != null) { - this.indexNodeStatuses(); - try { - Thread.sleep(DateTimeHelper.ONE_MINUTE); - } catch (InterruptedException e) { - } - } - } - - public synchronized void stopIndexing() { - Thread indexerThread = this.nodeIndexerThread; - this.nodeIndexerThread = null; - indexerThread.interrupt(); - } - - private void indexNodeStatuses() { - long updateStatusMillis = -1L; - DocumentStore documentStore = ApplicationFactory.getDocumentStore(); - UpdateStatus updateStatus = documentStore.retrieve(UpdateStatus.class, - false); - if (updateStatus != null && - updateStatus.getDocumentString() != null) { - String updateString = updateStatus.getDocumentString(); - try { - updateStatusMillis = Long.parseLong(updateString.trim()); - } catch (NumberFormatException e) { - /* Handle below. */ - } - } - synchronized (this) { - if (updateStatusMillis <= this.lastIndexed) { - return; - } - } - List<String> newRelaysByConsensusWeight = new ArrayList<String>(); - Map<String, SummaryDocument> - newRelayFingerprintSummaryLines = - new HashMap<String, SummaryDocument>(), - newBridgeFingerprintSummaryLines = - new HashMap<String, SummaryDocument>(); - Map<String, Set<String>> - newRelaysByCountryCode = new HashMap<String, Set<String>>(), - newRelaysByASNumber = new HashMap<String, Set<String>>(), - newRelaysByFlag = new HashMap<String, Set<String>>(), - newBridgesByFlag = new HashMap<String, Set<String>>(), - newRelaysByContact = new HashMap<String, Set<String>>(), - newRelaysByFamily = new HashMap<String, Set<String>>(); - SortedMap<Integer, Set<String>> - newRelaysByFirstSeenDays = new TreeMap<Integer, Set<String>>(), - newBridgesByFirstSeenDays = new TreeMap<Integer, Set<String>>(), - newRelaysByLastSeenDays = new TreeMap<Integer, Set<String>>(), - newBridgesByLastSeenDays = new TreeMap<Integer, Set<String>>(); - Set<SummaryDocument> currentRelays = new HashSet<SummaryDocument>(), - currentBridges = new HashSet<SummaryDocument>(); - SortedSet<String> fingerprints = documentStore.list( - SummaryDocument.class); - long relaysLastValidAfterMillis = 0L, bridgesLastPublishedMillis = 0L; - for (String fingerprint : fingerprints) { - SummaryDocument node = documentStore.retrieve(SummaryDocument.class, - true, fingerprint); - if (node.isRelay()) { - relaysLastValidAfterMillis = Math.max( - relaysLastValidAfterMillis, node.getLastSeenMillis()); - currentRelays.add(node); - } else { - bridgesLastPublishedMillis = Math.max( - bridgesLastPublishedMillis, node.getLastSeenMillis()); - currentBridges.add(node); - } - } - Time time = ApplicationFactory.getTime(); - List<String> orderRelaysByConsensusWeight = new ArrayList<String>(); - for (SummaryDocument entry : currentRelays) { - String fingerprint = entry.getFingerprint().toUpperCase(); - String hashedFingerprint = entry.getHashedFingerprint(). - toUpperCase(); - newRelayFingerprintSummaryLines.put(fingerprint, entry); - newRelayFingerprintSummaryLines.put(hashedFingerprint, entry); - long consensusWeight = entry.getConsensusWeight(); - orderRelaysByConsensusWeight.add(String.format("%020d %s", - consensusWeight, fingerprint)); - orderRelaysByConsensusWeight.add(String.format("%020d %s", - consensusWeight, hashedFingerprint)); - if (entry.getCountryCode() != null) { - String countryCode = entry.getCountryCode(); - if (!newRelaysByCountryCode.containsKey(countryCode)) { - newRelaysByCountryCode.put(countryCode, - new HashSet<String>()); - } - newRelaysByCountryCode.get(countryCode).add(fingerprint); - newRelaysByCountryCode.get(countryCode).add(hashedFingerprint); - } - if (entry.getASNumber() != null) { - String aSNumber = entry.getASNumber(); - if (!newRelaysByASNumber.containsKey(aSNumber)) { - newRelaysByASNumber.put(aSNumber, new HashSet<String>()); - } - newRelaysByASNumber.get(aSNumber).add(fingerprint); - newRelaysByASNumber.get(aSNumber).add(hashedFingerprint); - } - for (String flag : entry.getRelayFlags()) { - String flagLowerCase = flag.toLowerCase(); - if (!newRelaysByFlag.containsKey(flagLowerCase)) { - newRelaysByFlag.put(flagLowerCase, new HashSet<String>()); - } - newRelaysByFlag.get(flagLowerCase).add(fingerprint); - newRelaysByFlag.get(flagLowerCase).add(hashedFingerprint); - } - if (entry.getFamilyFingerprints() != null) { - newRelaysByFamily.put(fingerprint, entry.getFamilyFingerprints()); - } - int daysSinceFirstSeen = (int) ((time.currentTimeMillis() - - entry.getFirstSeenMillis()) / DateTimeHelper.ONE_DAY); - if (!newRelaysByFirstSeenDays.containsKey(daysSinceFirstSeen)) { - newRelaysByFirstSeenDays.put(daysSinceFirstSeen, - new HashSet<String>()); - } - newRelaysByFirstSeenDays.get(daysSinceFirstSeen).add(fingerprint); - newRelaysByFirstSeenDays.get(daysSinceFirstSeen).add( - hashedFingerprint); - int daysSinceLastSeen = (int) ((time.currentTimeMillis() - - entry.getLastSeenMillis()) / DateTimeHelper.ONE_DAY); - if (!newRelaysByLastSeenDays.containsKey(daysSinceLastSeen)) { - newRelaysByLastSeenDays.put(daysSinceLastSeen, - new HashSet<String>()); - } - newRelaysByLastSeenDays.get(daysSinceLastSeen).add(fingerprint); - newRelaysByLastSeenDays.get(daysSinceLastSeen).add( - hashedFingerprint); - String contact = entry.getContact(); - if (!newRelaysByContact.containsKey(contact)) { - newRelaysByContact.put(contact, new HashSet<String>()); - } - newRelaysByContact.get(contact).add(fingerprint); - newRelaysByContact.get(contact).add(hashedFingerprint); - } - Collections.sort(orderRelaysByConsensusWeight); - newRelaysByConsensusWeight = new ArrayList<String>(); - for (String relay : orderRelaysByConsensusWeight) { - newRelaysByConsensusWeight.add(relay.split(" ")[1]); - } - for (Map.Entry<String, Set<String>> e : - newRelaysByFamily.entrySet()) { - String fingerprint = e.getKey(); - Set<String> inMutualFamilyRelation = new HashSet<String>(); - for (String otherFingerprint : e.getValue()) { - if (newRelaysByFamily.containsKey(otherFingerprint) && - newRelaysByFamily.get(otherFingerprint).contains( - fingerprint)) { - inMutualFamilyRelation.add(otherFingerprint); - } - } - e.getValue().retainAll(inMutualFamilyRelation); - } - for (SummaryDocument entry : currentBridges) { - String hashedFingerprint = entry.getFingerprint().toUpperCase(); - String hashedHashedFingerprint = entry.getHashedFingerprint(). - toUpperCase(); - newBridgeFingerprintSummaryLines.put(hashedFingerprint, entry); - newBridgeFingerprintSummaryLines.put(hashedHashedFingerprint, - entry); - for (String flag : entry.getRelayFlags()) { - String flagLowerCase = flag.toLowerCase(); - if (!newBridgesByFlag.containsKey(flagLowerCase)) { - newBridgesByFlag.put(flagLowerCase, new HashSet<String>()); - } - newBridgesByFlag.get(flagLowerCase).add(hashedFingerprint); - newBridgesByFlag.get(flagLowerCase).add( - hashedHashedFingerprint); - } - int daysSinceFirstSeen = (int) ((time.currentTimeMillis() - - entry.getFirstSeenMillis()) / DateTimeHelper.ONE_DAY); - if (!newBridgesByFirstSeenDays.containsKey(daysSinceFirstSeen)) { - newBridgesByFirstSeenDays.put(daysSinceFirstSeen, - new HashSet<String>()); - } - newBridgesByFirstSeenDays.get(daysSinceFirstSeen).add( - hashedFingerprint); - newBridgesByFirstSeenDays.get(daysSinceFirstSeen).add( - hashedHashedFingerprint); - int daysSinceLastSeen = (int) ((time.currentTimeMillis() - - entry.getLastSeenMillis()) / DateTimeHelper.ONE_DAY); - if (!newBridgesByLastSeenDays.containsKey(daysSinceLastSeen)) { - newBridgesByLastSeenDays.put(daysSinceLastSeen, - new HashSet<String>()); - } - newBridgesByLastSeenDays.get(daysSinceLastSeen).add( - hashedFingerprint); - newBridgesByLastSeenDays.get(daysSinceLastSeen).add( - hashedHashedFingerprint); - } - NodeIndex newNodeIndex = new NodeIndex(); - newNodeIndex.setRelaysByConsensusWeight(newRelaysByConsensusWeight); - newNodeIndex.setRelayFingerprintSummaryLines( - newRelayFingerprintSummaryLines); - newNodeIndex.setBridgeFingerprintSummaryLines( - newBridgeFingerprintSummaryLines); - newNodeIndex.setRelaysByCountryCode(newRelaysByCountryCode); - newNodeIndex.setRelaysByASNumber(newRelaysByASNumber); - newNodeIndex.setRelaysByFlag(newRelaysByFlag); - newNodeIndex.setBridgesByFlag(newBridgesByFlag); - newNodeIndex.setRelaysByContact(newRelaysByContact); - newNodeIndex.setRelaysByFamily(newRelaysByFamily); - newNodeIndex.setRelaysByFirstSeenDays(newRelaysByFirstSeenDays); - newNodeIndex.setRelaysByLastSeenDays(newRelaysByLastSeenDays); - newNodeIndex.setBridgesByFirstSeenDays(newBridgesByFirstSeenDays); - newNodeIndex.setBridgesByLastSeenDays(newBridgesByLastSeenDays); - newNodeIndex.setRelaysPublishedString(DateTimeHelper.format( - relaysLastValidAfterMillis)); - newNodeIndex.setBridgesPublishedString(DateTimeHelper.format( - bridgesLastPublishedMillis)); - synchronized (this) { - this.lastIndexed = updateStatusMillis; - this.latestNodeIndex = newNodeIndex; - this.notifyAll(); - } - } -} - diff --git a/src/org/torproject/onionoo/NodeStatus.java b/src/org/torproject/onionoo/NodeStatus.java deleted file mode 100644 index 1bc74f9..0000000 --- a/src/org/torproject/onionoo/NodeStatus.java +++ /dev/null @@ -1,581 +0,0 @@ -/* Copyright 2011, 2012 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.SortedMap; -import java.util.SortedSet; -import java.util.TreeMap; -import java.util.TreeSet; - -import org.apache.commons.codec.DecoderException; -import org.apache.commons.codec.binary.Hex; -import org.apache.commons.codec.digest.DigestUtils; - -/* Store search data of a single relay that was running in the past seven - * days. */ -public class NodeStatus extends Document { - - private boolean isRelay; - public boolean isRelay() { - return this.isRelay; - } - - private String fingerprint; - public String getFingerprint() { - return this.fingerprint; - } - - private String hashedFingerprint; - public String getHashedFingerprint() { - return this.hashedFingerprint; - } - - private String nickname; - public String getNickname() { - return this.nickname; - } - - private String address; - public String getAddress() { - return this.address; - } - - private SortedSet<String> orAddresses; - private SortedSet<String> orAddressesAndPorts; - public SortedSet<String> getOrAddresses() { - return new TreeSet<String>(this.orAddresses); - } - public void addOrAddressAndPort(String orAddressAndPort) { - if (orAddressAndPort.contains(":") && orAddressAndPort.length() > 0) { - String orAddress = orAddressAndPort.substring(0, - orAddressAndPort.lastIndexOf(':')); - if (this.exitAddresses.contains(orAddress)) { - this.exitAddresses.remove(orAddress); - } - this.orAddresses.add(orAddress); - this.orAddressesAndPorts.add(orAddressAndPort); - } - } - public SortedSet<String> getOrAddressesAndPorts() { - return new TreeSet<String>(this.orAddressesAndPorts); - } - - private SortedSet<String> exitAddresses; - public void addExitAddress(String exitAddress) { - if (exitAddress.length() > 0 && !this.address.equals(exitAddress) && - !this.orAddresses.contains(exitAddress)) { - this.exitAddresses.add(exitAddress); - } - } - public SortedSet<String> getExitAddresses() { - return new TreeSet<String>(this.exitAddresses); - } - - private Float latitude; - public void setLatitude(Float latitude) { - this.latitude = latitude; - } - public Float getLatitude() { - return this.latitude; - } - - private Float longitude; - public void setLongitude(Float longitude) { - this.longitude = longitude; - } - public Float getLongitude() { - return this.longitude; - } - - private String countryCode; - public void setCountryCode(String countryCode) { - this.countryCode = countryCode; - } - public String getCountryCode() { - return this.countryCode; - } - - private String countryName; - public void setCountryName(String countryName) { - this.countryName = countryName; - } - public String getCountryName() { - return this.countryName; - } - - private String regionName; - public void setRegionName(String regionName) { - this.regionName = regionName; - } - public String getRegionName() { - return this.regionName; - } - - private String cityName; - public void setCityName(String cityName) { - this.cityName = cityName; - } - public String getCityName() { - return this.cityName; - } - - private String aSName; - public void setASName(String aSName) { - this.aSName = aSName; - } - public String getASName() { - return this.aSName; - } - - private String aSNumber; - public void setASNumber(String aSNumber) { - this.aSNumber = aSNumber; - } - public String getASNumber() { - return this.aSNumber; - } - - private long firstSeenMillis; - public long getFirstSeenMillis() { - return this.firstSeenMillis; - } - - private long lastSeenMillis; - public long getLastSeenMillis() { - return this.lastSeenMillis; - } - - private int orPort; - public int getOrPort() { - return this.orPort; - } - - private int dirPort; - public int getDirPort() { - return this.dirPort; - } - - private SortedSet<String> relayFlags; - public SortedSet<String> getRelayFlags() { - return this.relayFlags; - } - - private long consensusWeight; - public long getConsensusWeight() { - return this.consensusWeight; - } - - private boolean running; - public void setRunning(boolean running) { - this.running = running; - } - public boolean getRunning() { - return this.running; - } - - private String hostName; - public void setHostName(String hostName) { - this.hostName = hostName; - } - public String getHostName() { - return this.hostName; - } - - private long lastRdnsLookup = -1L; - public void setLastRdnsLookup(long lastRdnsLookup) { - this.lastRdnsLookup = lastRdnsLookup; - } - public long getLastRdnsLookup() { - return this.lastRdnsLookup; - } - - private double advertisedBandwidthFraction = -1.0; - public void setAdvertisedBandwidthFraction( - double advertisedBandwidthFraction) { - this.advertisedBandwidthFraction = advertisedBandwidthFraction; - } - public double getAdvertisedBandwidthFraction() { - return this.advertisedBandwidthFraction; - } - - private double consensusWeightFraction = -1.0; - public void setConsensusWeightFraction(double consensusWeightFraction) { - this.consensusWeightFraction = consensusWeightFraction; - } - public double getConsensusWeightFraction() { - return this.consensusWeightFraction; - } - - private double guardProbability = -1.0; - public void setGuardProbability(double guardProbability) { - this.guardProbability = guardProbability; - } - public double getGuardProbability() { - return this.guardProbability; - } - - private double middleProbability = -1.0; - public void setMiddleProbability(double middleProbability) { - this.middleProbability = middleProbability; - } - public double getMiddleProbability() { - return this.middleProbability; - } - - private double exitProbability = -1.0; - public void setExitProbability(double exitProbability) { - this.exitProbability = exitProbability; - } - public double getExitProbability() { - return this.exitProbability; - } - - private String defaultPolicy; - public String getDefaultPolicy() { - return this.defaultPolicy; - } - - private String portList; - public String getPortList() { - return this.portList; - } - - private SortedMap<Long, Set<String>> lastAddresses; - public SortedMap<Long, Set<String>> getLastAddresses() { - return this.lastAddresses == null ? null : - new TreeMap<Long, Set<String>>(this.lastAddresses); - } - public long getLastChangedOrAddress() { - long lastChangedAddressesMillis = -1L; - if (this.lastAddresses != null) { - Set<String> lastAddresses = null; - for (Map.Entry<Long, Set<String>> e : this.lastAddresses.entrySet()) { - if (lastAddresses != null) { - for (String address : e.getValue()) { - if (!lastAddresses.contains(address)) { - return lastChangedAddressesMillis; - } - } - } - lastChangedAddressesMillis = e.getKey(); - lastAddresses = e.getValue(); - } - } - return lastChangedAddressesMillis; - } - - private String contact; - public void setContact(String contact) { - if (contact == null) { - this.contact = null; - } else { - StringBuilder sb = new StringBuilder(); - for (char c : contact.toLowerCase().toCharArray()) { - if (c >= 32 && c < 127) { - sb.append(c); - } else { - sb.append(" "); - } - } - this.contact = sb.toString(); - } - } - public String getContact() { - return this.contact; - } - - private Boolean recommendedVersion; - public Boolean getRecommendedVersion() { - return this.recommendedVersion; - } - - private SortedSet<String> familyFingerprints; - public void setFamilyFingerprints( - SortedSet<String> familyFingerprints) { - this.familyFingerprints = familyFingerprints; - } - public SortedSet<String> getFamilyFingerprints() { - return this.familyFingerprints; - } - - public NodeStatus(boolean isRelay, String nickname, String fingerprint, - String address, SortedSet<String> orAddressesAndPorts, - SortedSet<String> exitAddresses, long lastSeenMillis, int orPort, - int dirPort, SortedSet<String> relayFlags, long consensusWeight, - String countryCode, String hostName, long lastRdnsLookup, - String defaultPolicy, String portList, long firstSeenMillis, - long lastChangedAddresses, String aSNumber, String contact, - Boolean recommendedVersion, SortedSet<String> familyFingerprints) { - this.isRelay = isRelay; - this.nickname = nickname; - this.fingerprint = fingerprint; - try { - this.hashedFingerprint = DigestUtils.shaHex(Hex.decodeHex( - this.fingerprint.toCharArray())).toUpperCase(); - } catch (DecoderException e) { - throw new IllegalArgumentException("Fingerprint '" + fingerprint - + "' is not a valid fingerprint.", e); - } - this.address = address; - this.exitAddresses = new TreeSet<String>(); - if (exitAddresses != null) { - this.exitAddresses.addAll(exitAddresses); - } - this.exitAddresses.remove(this.address); - this.orAddresses = new TreeSet<String>(); - this.orAddressesAndPorts = new TreeSet<String>(); - if (orAddressesAndPorts != null) { - for (String orAddressAndPort : orAddressesAndPorts) { - this.addOrAddressAndPort(orAddressAndPort); - } - } - this.lastSeenMillis = lastSeenMillis; - this.orPort = orPort; - this.dirPort = dirPort; - this.relayFlags = relayFlags; - this.consensusWeight = consensusWeight; - this.countryCode = countryCode; - this.hostName = hostName; - this.lastRdnsLookup = lastRdnsLookup; - this.defaultPolicy = defaultPolicy; - this.portList = portList; - this.firstSeenMillis = firstSeenMillis; - this.lastAddresses = - new TreeMap<Long, Set<String>>(Collections.reverseOrder()); - Set<String> addresses = new HashSet<String>(); - addresses.add(address + ":" + orPort); - if (dirPort > 0) { - addresses.add(address + ":" + dirPort); - } - addresses.addAll(orAddressesAndPorts); - this.lastAddresses.put(lastChangedAddresses, addresses); - this.aSNumber = aSNumber; - this.contact = contact; - this.recommendedVersion = recommendedVersion; - this.familyFingerprints = familyFingerprints; - } - - public static NodeStatus fromString(String documentString) { - boolean isRelay = false; - String nickname = null, fingerprint = null, address = null, - countryCode = null, hostName = null, defaultPolicy = null, - portList = null, aSNumber = null, contact = null; - SortedSet<String> orAddressesAndPorts = null, exitAddresses = null, - relayFlags = null, familyFingerprints = null; - long lastSeenMillis = -1L, consensusWeight = -1L, - lastRdnsLookup = -1L, firstSeenMillis = -1L, - lastChangedAddresses = -1L; - int orPort = -1, dirPort = -1; - Boolean recommendedVersion = null; - try { - String separator = documentString.contains("\t") ? "\t" : " "; - String[] parts = documentString.trim().split(separator); - isRelay = parts[0].equals("r"); - if (parts.length < 9) { - System.err.println("Too few space-separated values in line '" - + documentString.trim() + "'. Skipping."); - return null; - } - nickname = parts[1]; - fingerprint = parts[2]; - orAddressesAndPorts = new TreeSet<String>(); - exitAddresses = new TreeSet<String>(); - String addresses = parts[3]; - if (addresses.contains(";")) { - String[] addressParts = addresses.split(";", -1); - if (addressParts.length != 3) { - System.err.println("Invalid addresses entry in line '" - + documentString.trim() + "'. Skipping."); - return null; - } - address = addressParts[0]; - if (addressParts[1].length() > 0) { - orAddressesAndPorts.addAll(Arrays.asList( - addressParts[1].split("\\+"))); - } - if (addressParts[2].length() > 0) { - exitAddresses.addAll(Arrays.asList( - addressParts[2].split("\\+"))); - } - } else { - address = addresses; - } - lastSeenMillis = DateTimeHelper.parse(parts[4] + " " + parts[5]); - if (lastSeenMillis < 0L) { - System.err.println("Parse exception while parsing node status " - + "line '" + documentString + "'. Skipping."); - return null; - } - orPort = Integer.parseInt(parts[6]); - dirPort = Integer.parseInt(parts[7]); - relayFlags = new TreeSet<String>(); - if (parts[8].length() > 0) { - relayFlags.addAll(Arrays.asList(parts[8].split(","))); - } - if (parts.length > 9) { - consensusWeight = Long.parseLong(parts[9]); - } - if (parts.length > 10) { - countryCode = parts[10]; - } - if (parts.length > 12) { - hostName = parts[11].equals("null") ? null : parts[11]; - lastRdnsLookup = Long.parseLong(parts[12]); - } - if (parts.length > 14) { - if (!parts[13].equals("null")) { - defaultPolicy = parts[13]; - } - if (!parts[14].equals("null")) { - portList = parts[14]; - } - } - firstSeenMillis = lastSeenMillis; - if (parts.length > 16) { - firstSeenMillis = DateTimeHelper.parse(parts[15] + " " - + parts[16]); - if (firstSeenMillis < 0L) { - System.err.println("Parse exception while parsing node status " - + "line '" + documentString + "'. Skipping."); - return null; - } - } - lastChangedAddresses = lastSeenMillis; - if (parts.length > 18 && !parts[17].equals("null")) { - lastChangedAddresses = DateTimeHelper.parse(parts[17] + " " - + parts[18]); - if (lastChangedAddresses < 0L) { - System.err.println("Parse exception while parsing node status " - + "line '" + documentString + "'. Skipping."); - return null; - } - } - if (parts.length > 19) { - aSNumber = parts[19]; - } - if (parts.length > 20) { - contact = parts[20]; - } - if (parts.length > 21) { - recommendedVersion = parts[21].equals("null") ? null : - parts[21].equals("true"); - } - if (parts.length > 22 && !parts[22].equals("null")) { - familyFingerprints = new TreeSet<String>(Arrays.asList( - parts[22].split(";"))); - } - } catch (NumberFormatException e) { - System.err.println("Number format exception while parsing node " - + "status line '" + documentString + "': " + e.getMessage() - + ". Skipping."); - return null; - } catch (Exception e) { - /* This catch block is only here to handle yet unknown errors. It - * should go away once we're sure what kind of errors can occur. */ - System.err.println("Unknown exception while parsing node status " - + "line '" + documentString + "': " + e.getMessage() + ". " - + "Skipping."); - return null; - } - NodeStatus newNodeStatus = new NodeStatus(isRelay, nickname, - fingerprint, address, orAddressesAndPorts, exitAddresses, - lastSeenMillis, orPort, dirPort, relayFlags, consensusWeight, - countryCode, hostName, lastRdnsLookup, defaultPolicy, portList, - firstSeenMillis, lastChangedAddresses, aSNumber, contact, - recommendedVersion, familyFingerprints); - return newNodeStatus; - } - - public void update(NodeStatus newNodeStatus) { - if (newNodeStatus.lastSeenMillis > this.lastSeenMillis) { - this.nickname = newNodeStatus.nickname; - this.address = newNodeStatus.address; - this.orAddressesAndPorts = newNodeStatus.orAddressesAndPorts; - this.lastSeenMillis = newNodeStatus.lastSeenMillis; - this.orPort = newNodeStatus.orPort; - this.dirPort = newNodeStatus.dirPort; - this.relayFlags = newNodeStatus.relayFlags; - this.consensusWeight = newNodeStatus.consensusWeight; - this.countryCode = newNodeStatus.countryCode; - this.defaultPolicy = newNodeStatus.defaultPolicy; - this.portList = newNodeStatus.portList; - this.aSNumber = newNodeStatus.aSNumber; - this.recommendedVersion = newNodeStatus.recommendedVersion; - } - if (this.isRelay && newNodeStatus.isRelay) { - this.lastAddresses.putAll(newNodeStatus.lastAddresses); - } - this.firstSeenMillis = Math.min(newNodeStatus.firstSeenMillis, - this.firstSeenMillis); - } - - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(this.isRelay ? "r" : "b"); - sb.append("\t" + this.nickname); - sb.append("\t" + this.fingerprint); - sb.append("\t" + this.address + ";"); - int written = 0; - for (String orAddressAndPort : this.orAddressesAndPorts) { - sb.append((written++ > 0 ? "+" : "") + orAddressAndPort); - } - sb.append(";"); - if (this.isRelay) { - written = 0; - for (String exitAddress : this.exitAddresses) { - sb.append((written++ > 0 ? "+" : "") - + exitAddress); - } - } - sb.append("\t" + DateTimeHelper.format(this.lastSeenMillis, - DateTimeHelper.ISO_DATETIME_TAB_FORMAT)); - sb.append("\t" + this.orPort); - sb.append("\t" + this.dirPort + "\t"); - written = 0; - for (String relayFlag : this.relayFlags) { - sb.append((written++ > 0 ? "," : "") + relayFlag); - } - if (this.isRelay) { - sb.append("\t" + String.valueOf(this.consensusWeight)); - sb.append("\t" - + (this.countryCode != null ? this.countryCode : "??")); - sb.append("\t" + (this.hostName != null ? this.hostName : "null")); - sb.append("\t" + String.valueOf(this.lastRdnsLookup)); - sb.append("\t" + (this.defaultPolicy != null ? this.defaultPolicy - : "null")); - sb.append("\t" + (this.portList != null ? this.portList : "null")); - } else { - sb.append("\t-1\t??\tnull\t-1\tnull\tnull"); - } - sb.append("\t" + DateTimeHelper.format(this.firstSeenMillis, - DateTimeHelper.ISO_DATETIME_TAB_FORMAT)); - if (this.isRelay) { - sb.append("\t" + DateTimeHelper.format( - this.getLastChangedOrAddress(), - DateTimeHelper.ISO_DATETIME_TAB_FORMAT)); - sb.append("\t" + (this.aSNumber != null ? this.aSNumber : "null")); - } else { - sb.append("\tnull\tnull\tnull"); - } - sb.append("\t" + (this.contact != null ? this.contact : "")); - sb.append("\t" + (this.recommendedVersion == null ? "null" : - this.recommendedVersion ? "true" : "false")); - if (this.familyFingerprints == null || - this.familyFingerprints.isEmpty()) { - sb.append("\tnull"); - } else { - sb.append("\t"); - written = 0; - for (String familyFingerprint : this.familyFingerprints) { - sb.append((written++ > 0 ? ";" : "") + familyFingerprint); - } - } - return sb.toString(); - } -} - diff --git a/src/org/torproject/onionoo/RequestHandler.java b/src/org/torproject/onionoo/RequestHandler.java deleted file mode 100644 index 58ec17a..0000000 --- a/src/org/torproject/onionoo/RequestHandler.java +++ /dev/null @@ -1,548 +0,0 @@ -/* Copyright 2011--2014 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.SortedMap; - -public class RequestHandler { - - private NodeIndex nodeIndex; - - private DocumentStore documentStore; - - public RequestHandler(NodeIndex nodeIndex) { - this.nodeIndex = nodeIndex; - this.documentStore = ApplicationFactory.getDocumentStore(); - } - - private String resourceType; - public void setResourceType(String resourceType) { - this.resourceType = resourceType; - } - - private String type; - public void setType(String type) { - this.type = type; - } - - private String running; - public void setRunning(String running) { - this.running = running; - } - - private String[] search; - public void setSearch(String[] search) { - this.search = new String[search.length]; - System.arraycopy(search, 0, this.search, 0, search.length); - } - - private String lookup; - public void setLookup(String lookup) { - this.lookup = lookup; - } - - private String fingerprint; - public void setFingerprint(String fingerprint) { - this.fingerprint = fingerprint; - } - - private String country; - public void setCountry(String country) { - this.country = country; - } - - private String as; - public void setAs(String as) { - this.as = as; - } - - private String flag; - public void setFlag(String flag) { - this.flag = flag; - } - - private String[] contact; - public void setContact(String[] contact) { - this.contact = new String[contact.length]; - System.arraycopy(contact, 0, this.contact, 0, contact.length); - } - - private String[] order; - public void setOrder(String[] order) { - this.order = new String[order.length]; - System.arraycopy(order, 0, this.order, 0, order.length); - } - - private String offset; - public void setOffset(String offset) { - this.offset = offset; - } - - private String limit; - public void setLimit(String limit) { - this.limit = limit; - } - - private int[] firstSeenDays; - public void setFirstSeenDays(int[] firstSeenDays) { - this.firstSeenDays = new int[firstSeenDays.length]; - System.arraycopy(firstSeenDays, 0, this.firstSeenDays, 0, - firstSeenDays.length); - } - - private int[] lastSeenDays; - public void setLastSeenDays(int[] lastSeenDays) { - this.lastSeenDays = new int[lastSeenDays.length]; - System.arraycopy(lastSeenDays, 0, this.lastSeenDays, 0, - lastSeenDays.length); - } - - private String family; - public void setFamily(String family) { - this.family = family; - } - - private Map<String, SummaryDocument> filteredRelays = - new HashMap<String, SummaryDocument>(); - - private Map<String, SummaryDocument> filteredBridges = - new HashMap<String, SummaryDocument>(); - - public void handleRequest() { - this.filteredRelays.putAll( - this.nodeIndex.getRelayFingerprintSummaryLines()); - this.filteredBridges.putAll( - this.nodeIndex.getBridgeFingerprintSummaryLines()); - this.filterByResourceType(); - this.filterByType(); - this.filterByRunning(); - this.filterBySearchTerms(); - this.filterByLookup(); - this.filterByFingerprint(); - this.filterByCountryCode(); - this.filterByASNumber(); - this.filterByFlag(); - this.filterNodesByFirstSeenDays(); - this.filterNodesByLastSeenDays(); - this.filterByContact(); - this.filterByFamily(); - this.order(); - this.offset(); - this.limit(); - } - - - private void filterByResourceType() { - if (this.resourceType.equals("clients")) { - this.filteredRelays.clear(); - } - if (this.resourceType.equals("weights")) { - this.filteredBridges.clear(); - } - } - - private void filterByType() { - if (this.type == null) { - return; - } else if (this.type.equals("relay")) { - this.filteredBridges.clear(); - } else { - this.filteredRelays.clear(); - } - } - - private void filterByRunning() { - if (this.running == null) { - return; - } - boolean runningRequested = this.running.equals("true"); - Set<String> removeRelays = new HashSet<String>(); - for (Map.Entry<String, SummaryDocument> e : - filteredRelays.entrySet()) { - if (e.getValue().isRunning() != runningRequested) { - removeRelays.add(e.getKey()); - } - } - for (String fingerprint : removeRelays) { - this.filteredRelays.remove(fingerprint); - } - Set<String> removeBridges = new HashSet<String>(); - for (Map.Entry<String, SummaryDocument> e : - filteredBridges.entrySet()) { - if (e.getValue().isRunning() != runningRequested) { - removeBridges.add(e.getKey()); - } - } - for (String fingerprint : removeBridges) { - this.filteredBridges.remove(fingerprint); - } - } - - private void filterBySearchTerms() { - if (this.search == null) { - return; - } - for (String searchTerm : this.search) { - filterBySearchTerm(searchTerm); - } - } - - private void filterBySearchTerm(String searchTerm) { - Set<String> removeRelays = new HashSet<String>(); - for (Map.Entry<String, SummaryDocument> e : - filteredRelays.entrySet()) { - String fingerprint = e.getKey(); - SummaryDocument entry = e.getValue(); - boolean lineMatches = false; - String nickname = entry.getNickname() != null ? - entry.getNickname().toLowerCase() : "unnamed"; - if (searchTerm.startsWith("$")) { - /* Search is for $-prefixed fingerprint. */ - if (fingerprint.startsWith( - searchTerm.substring(1).toUpperCase())) { - /* $-prefixed fingerprint matches. */ - lineMatches = true; - } - } else if (nickname.contains(searchTerm.toLowerCase())) { - /* Nickname matches. */ - lineMatches = true; - } else if (fingerprint.startsWith(searchTerm.toUpperCase())) { - /* Non-$-prefixed fingerprint matches. */ - lineMatches = true; - } else { - List<String> addresses = entry.getAddresses(); - for (String address : addresses) { - if (address.startsWith(searchTerm.toLowerCase())) { - /* Address matches. */ - lineMatches = true; - break; - } - } - } - if (!lineMatches) { - removeRelays.add(e.getKey()); - } - } - for (String fingerprint : removeRelays) { - this.filteredRelays.remove(fingerprint); - } - Set<String> removeBridges = new HashSet<String>(); - for (Map.Entry<String, SummaryDocument> e : - filteredBridges.entrySet()) { - String hashedFingerprint = e.getKey(); - SummaryDocument entry = e.getValue(); - boolean lineMatches = false; - String nickname = entry.getNickname() != null ? - entry.getNickname().toLowerCase() : "unnamed"; - if (searchTerm.startsWith("$")) { - /* Search is for $-prefixed hashed fingerprint. */ - if (hashedFingerprint.startsWith( - searchTerm.substring(1).toUpperCase())) { - /* $-prefixed hashed fingerprint matches. */ - lineMatches = true; - } - } else if (nickname.contains(searchTerm.toLowerCase())) { - /* Nickname matches. */ - lineMatches = true; - } else if (hashedFingerprint.startsWith(searchTerm.toUpperCase())) { - /* Non-$-prefixed hashed fingerprint matches. */ - lineMatches = true; - } - if (!lineMatches) { - removeBridges.add(e.getKey()); - } - } - for (String fingerprint : removeBridges) { - this.filteredBridges.remove(fingerprint); - } - } - - private void filterByLookup() { - if (this.lookup == null) { - return; - } - String fingerprint = this.lookup; - SummaryDocument relayLine = this.filteredRelays.get(fingerprint); - this.filteredRelays.clear(); - if (relayLine != null) { - this.filteredRelays.put(fingerprint, relayLine); - } - SummaryDocument bridgeLine = this.filteredBridges.get(fingerprint); - this.filteredBridges.clear(); - if (bridgeLine != null) { - this.filteredBridges.put(fingerprint, bridgeLine); - } - } - - private void filterByFingerprint() { - if (this.fingerprint == null) { - return; - } - this.filteredRelays.clear(); - this.filteredBridges.clear(); - String fingerprint = this.fingerprint; - SummaryDocument entry = this.documentStore.retrieve( - SummaryDocument.class, true, fingerprint); - if (entry != null) { - if (entry.isRelay()) { - this.filteredRelays.put(fingerprint, entry); - } else { - this.filteredBridges.put(fingerprint, entry); - } - } - } - - private void filterByCountryCode() { - if (this.country == null) { - return; - } - String countryCode = this.country.toLowerCase(); - if (!this.nodeIndex.getRelaysByCountryCode().containsKey( - countryCode)) { - this.filteredRelays.clear(); - } else { - Set<String> relaysWithCountryCode = - this.nodeIndex.getRelaysByCountryCode().get(countryCode); - Set<String> removeRelays = new HashSet<String>(); - for (String fingerprint : this.filteredRelays.keySet()) { - if (!relaysWithCountryCode.contains(fingerprint)) { - removeRelays.add(fingerprint); - } - } - for (String fingerprint : removeRelays) { - this.filteredRelays.remove(fingerprint); - } - } - this.filteredBridges.clear(); - } - - private void filterByASNumber() { - if (this.as == null) { - return; - } - String aSNumber = this.as.toUpperCase(); - if (!aSNumber.startsWith("AS")) { - aSNumber = "AS" + aSNumber; - } - if (!this.nodeIndex.getRelaysByASNumber().containsKey(aSNumber)) { - this.filteredRelays.clear(); - } else { - Set<String> relaysWithASNumber = - this.nodeIndex.getRelaysByASNumber().get(aSNumber); - Set<String> removeRelays = new HashSet<String>(); - for (String fingerprint : this.filteredRelays.keySet()) { - if (!relaysWithASNumber.contains(fingerprint)) { - removeRelays.add(fingerprint); - } - } - for (String fingerprint : removeRelays) { - this.filteredRelays.remove(fingerprint); - } - } - this.filteredBridges.clear(); - } - - private void filterByFlag() { - if (this.flag == null) { - return; - } - String flag = this.flag.toLowerCase(); - if (!this.nodeIndex.getRelaysByFlag().containsKey(flag)) { - this.filteredRelays.clear(); - } else { - Set<String> relaysWithFlag = this.nodeIndex.getRelaysByFlag().get( - flag); - Set<String> removeRelays = new HashSet<String>(); - for (String fingerprint : this.filteredRelays.keySet()) { - if (!relaysWithFlag.contains(fingerprint)) { - removeRelays.add(fingerprint); - } - } - for (String fingerprint : removeRelays) { - this.filteredRelays.remove(fingerprint); - } - } - if (!this.nodeIndex.getBridgesByFlag().containsKey(flag)) { - this.filteredBridges.clear(); - } else { - Set<String> bridgesWithFlag = this.nodeIndex.getBridgesByFlag().get( - flag); - Set<String> removeBridges = new HashSet<String>(); - for (String fingerprint : this.filteredBridges.keySet()) { - if (!bridgesWithFlag.contains(fingerprint)) { - removeBridges.add(fingerprint); - } - } - for (String fingerprint : removeBridges) { - this.filteredBridges.remove(fingerprint); - } - } - } - - private void filterNodesByFirstSeenDays() { - if (this.firstSeenDays == null) { - return; - } - filterNodesByDays(this.filteredRelays, - this.nodeIndex.getRelaysByFirstSeenDays(), this.firstSeenDays); - filterNodesByDays(this.filteredBridges, - this.nodeIndex.getBridgesByFirstSeenDays(), this.firstSeenDays); - } - - private void filterNodesByLastSeenDays() { - if (this.lastSeenDays == null) { - return; - } - filterNodesByDays(this.filteredRelays, - this.nodeIndex.getRelaysByLastSeenDays(), this.lastSeenDays); - filterNodesByDays(this.filteredBridges, - this.nodeIndex.getBridgesByLastSeenDays(), this.lastSeenDays); - } - - private void filterNodesByDays( - Map<String, SummaryDocument> filteredNodes, - SortedMap<Integer, Set<String>> nodesByDays, int[] days) { - Set<String> removeNodes = new HashSet<String>(); - for (Set<String> nodes : nodesByDays.headMap(days[0]).values()) { - removeNodes.addAll(nodes); - } - if (days[1] < Integer.MAX_VALUE) { - for (Set<String> nodes : - nodesByDays.tailMap(days[1] + 1).values()) { - removeNodes.addAll(nodes); - } - } - for (String fingerprint : removeNodes) { - filteredNodes.remove(fingerprint); - } - } - - private void filterByContact() { - if (this.contact == null) { - return; - } - Set<String> removeRelays = new HashSet<String>(); - for (Map.Entry<String, Set<String>> e : - this.nodeIndex.getRelaysByContact().entrySet()) { - String contact = e.getKey(); - for (String contactPart : this.contact) { - if (contact == null || - !contact.contains(contactPart.toLowerCase())) { - removeRelays.addAll(e.getValue()); - break; - } - } - } - for (String fingerprint : removeRelays) { - this.filteredRelays.remove(fingerprint); - } - this.filteredBridges.clear(); - } - - private void filterByFamily() { - if (this.family == null) { - return; - } - Set<String> removeRelays = new HashSet<String>( - this.filteredRelays.keySet()); - removeRelays.remove(this.family); - if (this.nodeIndex.getRelaysByFamily().containsKey(this.family)) { - removeRelays.removeAll(this.nodeIndex.getRelaysByFamily(). - get(this.family)); - } - for (String fingerprint : removeRelays) { - this.filteredRelays.remove(fingerprint); - } - this.filteredBridges.clear(); - } - - private void order() { - if (this.order != null && this.order.length == 1) { - List<String> orderBy = new ArrayList<String>( - this.nodeIndex.getRelaysByConsensusWeight()); - if (this.order[0].startsWith("-")) { - Collections.reverse(orderBy); - } - for (String relay : orderBy) { - if (this.filteredRelays.containsKey(relay) && - !this.orderedRelays.contains(filteredRelays.get(relay))) { - this.orderedRelays.add(this.filteredRelays.remove(relay)); - } - } - for (String relay : this.filteredRelays.keySet()) { - if (!this.orderedRelays.contains(this.filteredRelays.get(relay))) { - this.orderedRelays.add(this.filteredRelays.remove(relay)); - } - } - Set<SummaryDocument> uniqueBridges = new HashSet<SummaryDocument>( - this.filteredBridges.values()); - this.orderedBridges.addAll(uniqueBridges); - } else { - Set<SummaryDocument> uniqueRelays = new HashSet<SummaryDocument>( - this.filteredRelays.values()); - this.orderedRelays.addAll(uniqueRelays); - Set<SummaryDocument> uniqueBridges = new HashSet<SummaryDocument>( - this.filteredBridges.values()); - this.orderedBridges.addAll(uniqueBridges); - } - } - - private void offset() { - if (this.offset == null) { - return; - } - int offsetValue = Integer.parseInt(this.offset); - while (offsetValue-- > 0 && - (!this.orderedRelays.isEmpty() || - !this.orderedBridges.isEmpty())) { - if (!this.orderedRelays.isEmpty()) { - this.orderedRelays.remove(0); - } else { - this.orderedBridges.remove(0); - } - } - } - - private void limit() { - if (this.limit == null) { - return; - } - int limitValue = Integer.parseInt(this.limit); - while (!this.orderedRelays.isEmpty() && - limitValue < this.orderedRelays.size()) { - this.orderedRelays.remove(this.orderedRelays.size() - 1); - } - limitValue -= this.orderedRelays.size(); - while (!this.orderedBridges.isEmpty() && - limitValue < this.orderedBridges.size()) { - this.orderedBridges.remove(this.orderedBridges.size() - 1); - } - } - - private List<SummaryDocument> orderedRelays = - new ArrayList<SummaryDocument>(); - public List<SummaryDocument> getOrderedRelays() { - return this.orderedRelays; - } - - private List<SummaryDocument> orderedBridges = - new ArrayList<SummaryDocument>(); - public List<SummaryDocument> getOrderedBridges() { - return this.orderedBridges; - } - - public String getRelaysPublishedString() { - return this.nodeIndex.getRelaysPublishedString(); - } - - public String getBridgesPublishedString() { - return this.nodeIndex.getBridgesPublishedString(); - } -} diff --git a/src/org/torproject/onionoo/ResourceServlet.java b/src/org/torproject/onionoo/ResourceServlet.java deleted file mode 100644 index efce86e..0000000 --- a/src/org/torproject/onionoo/ResourceServlet.java +++ /dev/null @@ -1,448 +0,0 @@ -/* Copyright 2011, 2012 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.io.IOException; -import java.io.PrintWriter; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.regex.Pattern; - -import javax.servlet.ServletConfig; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public class ResourceServlet extends HttpServlet { - - private static final long serialVersionUID = 7236658979947465319L; - - private boolean maintenanceMode = false; - - /* Called by servlet container, not by test class. */ - public void init(ServletConfig config) throws ServletException { - super.init(config); - this.maintenanceMode = - config.getInitParameter("maintenance") != null && - config.getInitParameter("maintenance").equals("1"); - } - - public long getLastModified(HttpServletRequest request) { - if (this.maintenanceMode) { - return super.getLastModified(request); - } else { - return ApplicationFactory.getNodeIndexer().getLastIndexed( - DateTimeHelper.TEN_SECONDS); - } - } - - protected static class HttpServletRequestWrapper { - private HttpServletRequest request; - protected HttpServletRequestWrapper(HttpServletRequest request) { - this.request = request; - } - protected String getRequestURI() { - return this.request.getRequestURI(); - } - @SuppressWarnings("rawtypes") - protected Map getParameterMap() { - return this.request.getParameterMap(); - } - protected String[] getParameterValues(String parameterKey) { - return this.request.getParameterValues(parameterKey); - } - } - - protected static class HttpServletResponseWrapper { - private HttpServletResponse response = null; - protected HttpServletResponseWrapper(HttpServletResponse response) { - this.response = response; - } - protected void sendError(int errorStatusCode) throws IOException { - this.response.sendError(errorStatusCode); - } - protected void setHeader(String headerName, String headerValue) { - this.response.setHeader(headerName, headerValue); - } - protected void setContentType(String contentType) { - this.response.setContentType(contentType); - } - protected void setCharacterEncoding(String characterEncoding) { - this.response.setCharacterEncoding(characterEncoding); - } - protected PrintWriter getWriter() throws IOException { - return this.response.getWriter(); - } - } - - public void doGet(HttpServletRequest request, - HttpServletResponse response) throws IOException, ServletException { - HttpServletRequestWrapper requestWrapper = - new HttpServletRequestWrapper(request); - HttpServletResponseWrapper responseWrapper = - new HttpServletResponseWrapper(response); - this.doGet(requestWrapper, responseWrapper); - } - - public void doGet(HttpServletRequestWrapper request, - HttpServletResponseWrapper response) throws IOException { - - if (this.maintenanceMode) { - response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); - return; - } - - if (ApplicationFactory.getNodeIndexer().getLastIndexed( - DateTimeHelper.TEN_SECONDS) + DateTimeHelper.SIX_HOURS - < ApplicationFactory.getTime().currentTimeMillis()) { - response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - return; - } - - NodeIndex nodeIndex = ApplicationFactory.getNodeIndexer(). - getLatestNodeIndex(DateTimeHelper.TEN_SECONDS); - if (nodeIndex == null) { - response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - return; - } - - String uri = request.getRequestURI(); - if (uri.startsWith("/onionoo/")) { - uri = uri.substring("/onionoo".length()); - } - String resourceType = null; - if (uri.startsWith("/summary")) { - resourceType = "summary"; - } else if (uri.startsWith("/details")) { - resourceType = "details"; - } else if (uri.startsWith("/bandwidth")) { - resourceType = "bandwidth"; - } else if (uri.startsWith("/weights")) { - resourceType = "weights"; - } else if (uri.startsWith("/clients")) { - resourceType = "clients"; - } else if (uri.startsWith("/uptime")) { - resourceType = "uptime"; - } else { - response.sendError(HttpServletResponse.SC_BAD_REQUEST); - return; - } - - RequestHandler rh = new RequestHandler(nodeIndex); - rh.setResourceType(resourceType); - - /* Extract parameters either from the old-style URI or from request - * parameters. */ - Map<String, String> parameterMap = new HashMap<String, String>(); - for (Object parameterKey : request.getParameterMap().keySet()) { - String[] parameterValues = - request.getParameterValues((String) parameterKey); - parameterMap.put((String) parameterKey, parameterValues[0]); - } - - /* Make sure that the request doesn't contain any unknown - * parameters. */ - Set<String> knownParameters = new HashSet<String>(Arrays.asList(( - "type,running,search,lookup,fingerprint,country,as,flag," - + "first_seen_days,last_seen_days,contact,order,limit,offset," - + "fields,family").split(","))); - for (String parameterKey : parameterMap.keySet()) { - if (!knownParameters.contains(parameterKey)) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST); - return; - } - } - - /* Filter relays and bridges matching the request. */ - if (parameterMap.containsKey("type")) { - String typeParameterValue = parameterMap.get("type").toLowerCase(); - boolean relaysRequested = true; - if (typeParameterValue.equals("bridge")) { - relaysRequested = false; - } else if (!typeParameterValue.equals("relay")) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST); - return; - } - rh.setType(relaysRequested ? "relay" : "bridge"); - } - if (parameterMap.containsKey("running")) { - String runningParameterValue = - parameterMap.get("running").toLowerCase(); - boolean runningRequested = true; - if (runningParameterValue.equals("false")) { - runningRequested = false; - } else if (!runningParameterValue.equals("true")) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST); - return; - } - rh.setRunning(runningRequested ? "true" : "false"); - } - if (parameterMap.containsKey("search")) { - String[] searchTerms = this.parseSearchParameters( - parameterMap.get("search")); - if (searchTerms == null) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST); - return; - } - rh.setSearch(searchTerms); - } - if (parameterMap.containsKey("lookup")) { - String lookupParameter = this.parseFingerprintParameter( - parameterMap.get("lookup")); - if (lookupParameter == null) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST); - return; - } - String fingerprint = lookupParameter.toUpperCase(); - rh.setLookup(fingerprint); - } - if (parameterMap.containsKey("fingerprint")) { - String fingerprintParameter = this.parseFingerprintParameter( - parameterMap.get("fingerprint")); - if (fingerprintParameter == null) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST); - return; - } - String fingerprint = fingerprintParameter.toUpperCase(); - rh.setFingerprint(fingerprint); - } - if (parameterMap.containsKey("country")) { - String countryCodeParameter = this.parseCountryCodeParameter( - parameterMap.get("country")); - if (countryCodeParameter == null) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST); - return; - } - rh.setCountry(countryCodeParameter); - } - if (parameterMap.containsKey("as")) { - String aSNumberParameter = this.parseASNumberParameter( - parameterMap.get("as")); - if (aSNumberParameter == null) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST); - return; - } - rh.setAs(aSNumberParameter); - } - if (parameterMap.containsKey("flag")) { - String flagParameter = this.parseFlagParameter( - parameterMap.get("flag")); - if (flagParameter == null) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST); - return; - } - rh.setFlag(flagParameter); - } - if (parameterMap.containsKey("first_seen_days")) { - int[] days = this.parseDaysParameter( - parameterMap.get("first_seen_days")); - if (days == null) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST); - return; - } - rh.setFirstSeenDays(days); - } - if (parameterMap.containsKey("last_seen_days")) { - int[] days = this.parseDaysParameter( - parameterMap.get("last_seen_days")); - if (days == null) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST); - return; - } - rh.setLastSeenDays(days); - } - if (parameterMap.containsKey("contact")) { - String[] contactParts = this.parseContactParameter( - parameterMap.get("contact")); - if (contactParts == null) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST); - return; - } - rh.setContact(contactParts); - } - if (parameterMap.containsKey("order")) { - String orderParameter = parameterMap.get("order").toLowerCase(); - String orderByField = orderParameter; - if (orderByField.startsWith("-")) { - orderByField = orderByField.substring(1); - } - if (!orderByField.equals("consensus_weight")) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST); - return; - } - rh.setOrder(new String[] { orderParameter }); - } - if (parameterMap.containsKey("offset")) { - String offsetParameter = parameterMap.get("offset"); - if (offsetParameter.length() > 6) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST); - return; - } - try { - Integer.parseInt(offsetParameter); - } catch (NumberFormatException e) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST); - return; - } - rh.setOffset(offsetParameter); - } - if (parameterMap.containsKey("limit")) { - String limitParameter = parameterMap.get("limit"); - if (limitParameter.length() > 6) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST); - return; - } - try { - Integer.parseInt(limitParameter); - } catch (NumberFormatException e) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST); - return; - } - rh.setLimit(limitParameter); - } - if (parameterMap.containsKey("family")) { - String familyParameter = this.parseFingerprintParameter( - parameterMap.get("family")); - if (familyParameter == null) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST); - return; - } - String family = familyParameter.toUpperCase(); - rh.setFamily(family); - } - rh.handleRequest(); - - ResponseBuilder rb = new ResponseBuilder(); - rb.setResourceType(resourceType); - rb.setRelaysPublishedString(rh.getRelaysPublishedString()); - rb.setBridgesPublishedString(rh.getBridgesPublishedString()); - rb.setOrderedRelays(rh.getOrderedRelays()); - rb.setOrderedBridges(rh.getOrderedBridges()); - String[] fields = null; - if (parameterMap.containsKey("fields")) { - fields = this.parseFieldsParameter(parameterMap.get("fields")); - if (fields == null) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST); - return; - } - rb.setFields(fields); - } - - response.setHeader("Access-Control-Allow-Origin", "*"); - response.setContentType("application/json"); - response.setCharacterEncoding("utf-8"); - PrintWriter pw = response.getWriter(); - rb.buildResponse(pw); - pw.flush(); - pw.close(); - } - - private static Pattern searchParameterPattern = - Pattern.compile("^\\$?[0-9a-fA-F]{1,40}$|" /* Fingerprint. */ - + "^[0-9a-zA-Z\\.]{1,19}$|" /* Nickname or IPv4 address. */ - + "^\\[[0-9a-fA-F:\\.]{1,39}\\]?$"); /* IPv6 address. */ - private String[] parseSearchParameters(String parameter) { - String[] searchParameters; - if (parameter.contains(" ")) { - searchParameters = parameter.split(" "); - } else { - searchParameters = new String[] { parameter }; - } - for (String searchParameter : searchParameters) { - if (!searchParameterPattern.matcher(searchParameter).matches()) { - return null; - } - } - return searchParameters; - } - - private static Pattern fingerprintParameterPattern = - Pattern.compile("^[0-9a-zA-Z]{1,40}$"); - private String parseFingerprintParameter(String parameter) { - if (!fingerprintParameterPattern.matcher(parameter).matches()) { - return null; - } - if (parameter.length() != 40) { - return null; - } - return parameter; - } - - private static Pattern countryCodeParameterPattern = - Pattern.compile("^[0-9a-zA-Z]{2}$"); - private String parseCountryCodeParameter(String parameter) { - if (!countryCodeParameterPattern.matcher(parameter).matches()) { - return null; - } - return parameter; - } - - private static Pattern aSNumberParameterPattern = - Pattern.compile("^[asAS]{0,2}[0-9]{1,10}$"); - private String parseASNumberParameter(String parameter) { - if (!aSNumberParameterPattern.matcher(parameter).matches()) { - return null; - } - return parameter; - } - - private static Pattern flagPattern = - Pattern.compile("^[a-zA-Z0-9]{1,20}$"); - private String parseFlagParameter(String parameter) { - if (!flagPattern.matcher(parameter).matches()) { - return null; - } - return parameter; - } - - private static Pattern daysPattern = Pattern.compile("^[0-9-]{1,10}$"); - private int[] parseDaysParameter(String parameter) { - if (!daysPattern.matcher(parameter).matches()) { - return null; - } - int x = 0, y = Integer.MAX_VALUE; - try { - if (!parameter.contains("-")) { - x = Integer.parseInt(parameter); - y = x; - } else { - String[] parts = parameter.split("-", 2); - if (parts[0].length() > 0) { - x = Integer.parseInt(parts[0]); - } - if (parts.length > 1 && parts[1].length() > 0) { - y = Integer.parseInt(parts[1]); - } - } - } catch (NumberFormatException e) { - return null; - } - if (x > y) { - return null; - } - return new int[] { x, y }; - } - - private String[] parseContactParameter(String parameter) { - for (char c : parameter.toCharArray()) { - if (c < 32 || c >= 127) { - return null; - } - } - return parameter.split(" "); - } - - private static Pattern fieldsParameterPattern = - Pattern.compile("^[0-9a-zA-Z_,]*$"); - private String[] parseFieldsParameter(String parameter) { - if (!fieldsParameterPattern.matcher(parameter).matches()) { - return null; - } - return parameter.split(","); - } -} - diff --git a/src/org/torproject/onionoo/ResponseBuilder.java b/src/org/torproject/onionoo/ResponseBuilder.java deleted file mode 100644 index 2086ea8..0000000 --- a/src/org/torproject/onionoo/ResponseBuilder.java +++ /dev/null @@ -1,311 +0,0 @@ -/* Copyright 2011--2013 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.List; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - -public class ResponseBuilder { - - private DocumentStore documentStore; - - public ResponseBuilder() { - this.documentStore = ApplicationFactory.getDocumentStore(); - } - - private String resourceType; - public void setResourceType(String resourceType) { - this.resourceType = resourceType; - } - - private String relaysPublishedString; - public void setRelaysPublishedString(String relaysPublishedString) { - this.relaysPublishedString = relaysPublishedString; - } - - private String bridgesPublishedString; - public void setBridgesPublishedString(String bridgesPublishedString) { - this.bridgesPublishedString = bridgesPublishedString; - } - - private List<SummaryDocument> orderedRelays = - new ArrayList<SummaryDocument>(); - public void setOrderedRelays(List<SummaryDocument> orderedRelays) { - this.orderedRelays = orderedRelays; - } - - private List<SummaryDocument> orderedBridges = - new ArrayList<SummaryDocument>(); - public void setOrderedBridges(List<SummaryDocument> orderedBridges) { - this.orderedBridges = orderedBridges; - } - - private String[] fields; - public void setFields(String[] fields) { - this.fields = new String[fields.length]; - System.arraycopy(fields, 0, this.fields, 0, fields.length); - } - - public void buildResponse(PrintWriter pw) { - writeRelays(orderedRelays, pw); - writeBridges(orderedBridges, pw); - } - - private void writeRelays(List<SummaryDocument> relays, PrintWriter pw) { - pw.write("{\"relays_published\":\"" + relaysPublishedString - + "\",\n\"relays\":["); - int written = 0; - for (SummaryDocument entry : relays) { - String lines = this.formatNodeStatus(entry); - if (lines.length() > 0) { - pw.print((written++ > 0 ? ",\n" : "\n") + lines); - } - } - pw.print("\n],\n"); - } - - private void writeBridges(List<SummaryDocument> bridges, - PrintWriter pw) { - pw.write("\"bridges_published\":\"" + bridgesPublishedString - + "\",\n\"bridges\":["); - int written = 0; - for (SummaryDocument entry : bridges) { - String lines = this.formatNodeStatus(entry); - if (lines.length() > 0) { - pw.print((written++ > 0 ? ",\n" : "\n") + lines); - } - } - pw.print("\n]}\n"); - } - - private String formatNodeStatus(SummaryDocument entry) { - if (this.resourceType == null) { - return ""; - } else if (this.resourceType.equals("summary")) { - return this.writeSummaryLine(entry); - } else if (this.resourceType.equals("details")) { - return this.writeDetailsLines(entry); - } else if (this.resourceType.equals("bandwidth")) { - return this.writeBandwidthLines(entry); - } else if (this.resourceType.equals("weights")) { - return this.writeWeightsLines(entry); - } else if (this.resourceType.equals("clients")) { - return this.writeClientsLines(entry); - } else if (this.resourceType.equals("uptime")) { - return this.writeUptimeLines(entry); - } else { - return ""; - } - } - - private String writeSummaryLine(SummaryDocument entry) { - return entry.isRelay() ? writeRelaySummaryLine(entry) - : writeBridgeSummaryLine(entry); - } - - private String writeRelaySummaryLine(SummaryDocument entry) { - String nickname = !entry.getNickname().equals("Unnamed") ? - entry.getNickname() : null; - String fingerprint = entry.getFingerprint(); - String running = entry.isRunning() ? "true" : "false"; - List<String> addresses = entry.getAddresses(); - StringBuilder addressesBuilder = new StringBuilder(); - int written = 0; - for (String address : addresses) { - addressesBuilder.append((written++ > 0 ? "," : "") + "\"" - + address.toLowerCase() + "\""); - } - return String.format("{%s\"f\":\"%s\",\"a\":[%s],\"r\":%s}", - (nickname == null ? "" : "\"n\":\"" + nickname + "\","), - fingerprint, addressesBuilder.toString(), running); - } - - private String writeBridgeSummaryLine(SummaryDocument entry) { - String nickname = !entry.getNickname().equals("Unnamed") ? - entry.getNickname() : null; - String hashedFingerprint = entry.getFingerprint(); - String running = entry.isRunning() ? "true" : "false"; - return String.format("{%s\"h\":\"%s\",\"r\":%s}", - (nickname == null ? "" : "\"n\":\"" + nickname + "\","), - hashedFingerprint, running); - } - - private String writeDetailsLines(SummaryDocument entry) { - String fingerprint = entry.getFingerprint(); - if (this.fields != null) { - /* TODO Maybe there's a more elegant way (more maintainable, more - * efficient, etc.) to implement this? */ - DetailsDocument detailsDocument = documentStore.retrieve( - DetailsDocument.class, true, fingerprint); - if (detailsDocument != null) { - DetailsDocument dd = new DetailsDocument(); - for (String field : this.fields) { - if (field.equals("nickname")) { - dd.setNickname(detailsDocument.getNickname()); - } else if (field.equals("fingerprint")) { - dd.setFingerprint(detailsDocument.getFingerprint()); - } else if (field.equals("hashed_fingerprint")) { - dd.setHashedFingerprint( - detailsDocument.getHashedFingerprint()); - } else if (field.equals("or_addresses")) { - dd.setOrAddresses(detailsDocument.getOrAddresses()); - } else if (field.equals("exit_addresses")) { - dd.setExitAddresses(detailsDocument.getExitAddresses()); - } else if (field.equals("dir_address")) { - dd.setDirAddress(detailsDocument.getDirAddress()); - } else if (field.equals("last_seen")) { - dd.setLastSeen(detailsDocument.getLastSeen()); - } else if (field.equals("last_changed_address_or_port")) { - dd.setLastChangedAddressOrPort( - detailsDocument.getLastChangedAddressOrPort()); - } else if (field.equals("first_seen")) { - dd.setFirstSeen(detailsDocument.getFirstSeen()); - } else if (field.equals("running")) { - dd.setRunning(detailsDocument.getRunning()); - } else if (field.equals("flags")) { - dd.setFlags(detailsDocument.getFlags()); - } else if (field.equals("country")) { - dd.setCountry(detailsDocument.getCountry()); - } else if (field.equals("country_name")) { - dd.setCountryName(detailsDocument.getCountryName()); - } else if (field.equals("region_name")) { - dd.setRegionName(detailsDocument.getRegionName()); - } else if (field.equals("city_name")) { - dd.setCityName(detailsDocument.getCityName()); - } else if (field.equals("latitude")) { - dd.setLatitude(detailsDocument.getLatitude()); - } else if (field.equals("longitude")) { - dd.setLongitude(detailsDocument.getLongitude()); - } else if (field.equals("as_number")) { - dd.setAsNumber(detailsDocument.getAsNumber()); - } else if (field.equals("as_name")) { - dd.setAsName(detailsDocument.getAsName()); - } else if (field.equals("consensus_weight")) { - dd.setConsensusWeight(detailsDocument.getConsensusWeight()); - } else if (field.equals("host_name")) { - dd.setHostName(detailsDocument.getHostName()); - } else if (field.equals("last_restarted")) { - dd.setLastRestarted(detailsDocument.getLastRestarted()); - } else if (field.equals("bandwidth_rate")) { - dd.setBandwidthRate(detailsDocument.getBandwidthRate()); - } else if (field.equals("bandwidth_burst")) { - dd.setBandwidthBurst(detailsDocument.getBandwidthBurst()); - } else if (field.equals("observed_bandwidth")) { - dd.setObservedBandwidth( - detailsDocument.getObservedBandwidth()); - } else if (field.equals("advertised_bandwidth")) { - dd.setAdvertisedBandwidth( - detailsDocument.getAdvertisedBandwidth()); - } else if (field.equals("exit_policy")) { - dd.setExitPolicy(detailsDocument.getExitPolicy()); - } else if (field.equals("exit_policy_summary")) { - dd.setExitPolicySummary( - detailsDocument.getExitPolicySummary()); - } else if (field.equals("exit_policy_v6_summary")) { - dd.setExitPolicyV6Summary( - detailsDocument.getExitPolicyV6Summary()); - } else if (field.equals("contact")) { - dd.setContact(detailsDocument.getContact()); - } else if (field.equals("platform")) { - dd.setPlatform(detailsDocument.getPlatform()); - } else if (field.equals("family")) { - dd.setFamily(detailsDocument.getFamily()); - } else if (field.equals("advertised_bandwidth_fraction")) { - dd.setAdvertisedBandwidthFraction( - detailsDocument.getAdvertisedBandwidthFraction()); - } else if (field.equals("consensus_weight_fraction")) { - dd.setConsensusWeightFraction( - detailsDocument.getConsensusWeightFraction()); - } else if (field.equals("guard_probability")) { - dd.setGuardProbability(detailsDocument.getGuardProbability()); - } else if (field.equals("middle_probability")) { - dd.setMiddleProbability( - detailsDocument.getMiddleProbability()); - } else if (field.equals("exit_probability")) { - dd.setExitProbability(detailsDocument.getExitProbability()); - } else if (field.equals("recommended_version")) { - dd.setRecommendedVersion( - detailsDocument.getRecommendedVersion()); - } else if (field.equals("hibernating")) { - dd.setHibernating(detailsDocument.getHibernating()); - } else if (field.equals("pool_assignment")) { - dd.setPoolAssignment(detailsDocument.getPoolAssignment()); - } - } - /* Don't escape HTML characters, like < and >, contained in - * strings. */ - Gson gson = new GsonBuilder().disableHtmlEscaping().create(); - /* Whenever we provide Gson with a string containing an escaped - * non-ASCII character like \u00F2, it escapes the \ to \\, which - * we need to undo before including the string in a response. */ - return gson.toJson(dd).replaceAll("\\\\\\\\u", "\\\\u"); - } else { - // TODO We should probably log that we didn't find a details - // document that we expected to exist. - return ""; - } - } else { - DetailsDocument detailsDocument = documentStore.retrieve( - DetailsDocument.class, false, fingerprint); - if (detailsDocument != null) { - return detailsDocument.getDocumentString(); - } else { - // TODO We should probably log that we didn't find a details - // document that we expected to exist. - return ""; - } - } - } - - private String writeBandwidthLines(SummaryDocument entry) { - String fingerprint = entry.getFingerprint(); - BandwidthDocument bandwidthDocument = this.documentStore.retrieve( - BandwidthDocument.class, false, fingerprint); - if (bandwidthDocument != null && - bandwidthDocument.getDocumentString() != null) { - return bandwidthDocument.getDocumentString(); - } else { - return "{\"fingerprint\":\"" + fingerprint.toUpperCase() + "\"}"; - } - } - - private String writeWeightsLines(SummaryDocument entry) { - String fingerprint = entry.getFingerprint(); - WeightsDocument weightsDocument = this.documentStore.retrieve( - WeightsDocument.class, false, fingerprint); - if (weightsDocument != null && - weightsDocument.getDocumentString() != null) { - return weightsDocument.getDocumentString(); - } else { - return "{\"fingerprint\":\"" + fingerprint.toUpperCase() + "\"}"; - } - } - - private String writeClientsLines(SummaryDocument entry) { - String fingerprint = entry.getFingerprint(); - ClientsDocument clientsDocument = this.documentStore.retrieve( - ClientsDocument.class, false, fingerprint); - if (clientsDocument != null && - clientsDocument.getDocumentString() != null) { - return clientsDocument.getDocumentString(); - } else { - return "{\"fingerprint\":\"" + fingerprint.toUpperCase() + "\"}"; - } - } - - private String writeUptimeLines(SummaryDocument entry) { - String fingerprint = entry.getFingerprint(); - UptimeDocument uptimeDocument = this.documentStore.retrieve( - UptimeDocument.class, false, fingerprint); - if (uptimeDocument != null && - uptimeDocument.getDocumentString() != null) { - return uptimeDocument.getDocumentString(); - } else { - return "{\"fingerprint\":\"" + fingerprint.toUpperCase() + "\"}"; - } - } -} diff --git a/src/org/torproject/onionoo/ReverseDomainNameResolver.java b/src/org/torproject/onionoo/ReverseDomainNameResolver.java deleted file mode 100644 index 3dbf9d1..0000000 --- a/src/org/torproject/onionoo/ReverseDomainNameResolver.java +++ /dev/null @@ -1,174 +0,0 @@ -/* Copyright 2013 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -public class ReverseDomainNameResolver { - - private class RdnsLookupWorker extends Thread { - public void run() { - while (time.currentTimeMillis() - RDNS_LOOKUP_MAX_DURATION_MILLIS - <= startedRdnsLookups) { - String rdnsLookupJob = null; - synchronized (rdnsLookupJobs) { - for (String job : rdnsLookupJobs) { - rdnsLookupJob = job; - rdnsLookupJobs.remove(job); - break; - } - } - if (rdnsLookupJob == null) { - break; - } - RdnsLookupRequest request = new RdnsLookupRequest(this, - rdnsLookupJob); - request.setDaemon(true); - request.start(); - try { - Thread.sleep(RDNS_LOOKUP_MAX_REQUEST_MILLIS); - } catch (InterruptedException e) { - /* Getting interrupted should be the default case. */ - } - String hostName = request.getHostName(); - if (hostName != null) { - synchronized (rdnsLookupResults) { - rdnsLookupResults.put(rdnsLookupJob, hostName); - } - } - long lookupMillis = request.getLookupMillis(); - if (lookupMillis >= 0L) { - synchronized (rdnsLookupMillis) { - rdnsLookupMillis.add(lookupMillis); - } - } - } - } - } - - private class RdnsLookupRequest extends Thread { - private RdnsLookupWorker parent; - private String address, hostName; - private long lookupStartedMillis = -1L, lookupCompletedMillis = -1L; - public RdnsLookupRequest(RdnsLookupWorker parent, String address) { - this.parent = parent; - this.address = address; - } - public void run() { - this.lookupStartedMillis = time.currentTimeMillis(); - try { - String result = InetAddress.getByName(this.address).getHostName(); - synchronized (this) { - this.hostName = result; - } - } catch (UnknownHostException e) { - /* We'll try again the next time. */ - } - this.lookupCompletedMillis = time.currentTimeMillis(); - this.parent.interrupt(); - } - public synchronized String getHostName() { - return hostName; - } - public synchronized long getLookupMillis() { - return this.lookupCompletedMillis - this.lookupStartedMillis; - } - } - - private Time time; - - public ReverseDomainNameResolver() { - this.time = ApplicationFactory.getTime(); - } - - private static final long RDNS_LOOKUP_MAX_REQUEST_MILLIS = - DateTimeHelper.TEN_SECONDS; - private static final long RDNS_LOOKUP_MAX_DURATION_MILLIS = - DateTimeHelper.FIVE_MINUTES; - private static final long RDNS_LOOKUP_MAX_AGE_MILLIS = - DateTimeHelper.TWELVE_HOURS; - private static final int RDNS_LOOKUP_WORKERS_NUM = 5; - - private Map<String, Long> addressLastLookupTimes; - - private Set<String> rdnsLookupJobs; - - private Map<String, String> rdnsLookupResults; - - private List<Long> rdnsLookupMillis; - - private long startedRdnsLookups; - - private List<RdnsLookupWorker> rdnsLookupWorkers; - - public void setAddresses(Map<String, Long> addressLastLookupTimes) { - this.addressLastLookupTimes = addressLastLookupTimes; - } - - public void startReverseDomainNameLookups() { - this.startedRdnsLookups = this.time.currentTimeMillis(); - this.rdnsLookupJobs = new HashSet<String>(); - for (Map.Entry<String, Long> e : - this.addressLastLookupTimes.entrySet()) { - if (e.getValue() < this.startedRdnsLookups - - RDNS_LOOKUP_MAX_AGE_MILLIS) { - this.rdnsLookupJobs.add(e.getKey()); - } - } - this.rdnsLookupResults = new HashMap<String, String>(); - this.rdnsLookupMillis = new ArrayList<Long>(); - this.rdnsLookupWorkers = new ArrayList<RdnsLookupWorker>(); - for (int i = 0; i < RDNS_LOOKUP_WORKERS_NUM; i++) { - RdnsLookupWorker rdnsLookupWorker = new RdnsLookupWorker(); - this.rdnsLookupWorkers.add(rdnsLookupWorker); - rdnsLookupWorker.setDaemon(true); - rdnsLookupWorker.start(); - } - } - - public void finishReverseDomainNameLookups() { - for (RdnsLookupWorker rdnsLookupWorker : this.rdnsLookupWorkers) { - try { - rdnsLookupWorker.join(); - } catch (InterruptedException e) { - /* This is not something that we can take care of. Just leave the - * worker thread alone. */ - } - } - } - - public Map<String, String> getLookupResults() { - synchronized (this.rdnsLookupResults) { - return new HashMap<String, String>(this.rdnsLookupResults); - } - } - - public long getLookupStartMillis() { - return this.startedRdnsLookups; - } - - public String getStatsString() { - StringBuilder sb = new StringBuilder(); - sb.append(" " + Logger.formatDecimalNumber(rdnsLookupMillis.size()) - + " lookups performed\n"); - if (rdnsLookupMillis.size() > 0) { - Collections.sort(rdnsLookupMillis); - sb.append(" " + Logger.formatMillis(rdnsLookupMillis.get(0)) - + " minimum lookup time\n"); - sb.append(" " + Logger.formatMillis(rdnsLookupMillis.get( - rdnsLookupMillis.size() / 2)) + " median lookup time\n"); - sb.append(" " + Logger.formatMillis(rdnsLookupMillis.get( - rdnsLookupMillis.size() - 1)) + " maximum lookup time\n"); - } - return sb.toString(); - } -} - diff --git a/src/org/torproject/onionoo/StatusUpdater.java b/src/org/torproject/onionoo/StatusUpdater.java deleted file mode 100644 index fb82182..0000000 --- a/src/org/torproject/onionoo/StatusUpdater.java +++ /dev/null @@ -1,11 +0,0 @@ -/* Copyright 2014 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -public interface StatusUpdater { - - public abstract void updateStatuses(); - - public abstract String getStatsString(); -} - diff --git a/src/org/torproject/onionoo/SummaryDocument.java b/src/org/torproject/onionoo/SummaryDocument.java deleted file mode 100644 index 61a3ec6..0000000 --- a/src/org/torproject/onionoo/SummaryDocument.java +++ /dev/null @@ -1,201 +0,0 @@ -/* Copyright 2013--2014 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.SortedSet; -import java.util.TreeSet; -import java.util.regex.Pattern; - -import org.apache.commons.codec.DecoderException; -import org.apache.commons.codec.binary.Hex; -import org.apache.commons.codec.digest.DigestUtils; - -public class SummaryDocument extends Document { - - private boolean t; - public void setRelay(boolean isRelay) { - this.t = isRelay; - } - public boolean isRelay() { - return this.t; - } - - private String f; - public void setFingerprint(String fingerprint) { - if (fingerprint != null) { - Pattern fingerprintPattern = Pattern.compile("^[0-9a-fA-F]{40}$"); - if (!fingerprintPattern.matcher(fingerprint).matches()) { - throw new IllegalArgumentException("Fingerprint '" + fingerprint - + "' is not a valid fingerprint."); - } - } - this.f = fingerprint; - } - public String getFingerprint() { - return this.f; - } - - public String getHashedFingerprint() { - String hashedFingerprint = null; - if (this.f != null) { - try { - hashedFingerprint = DigestUtils.shaHex(Hex.decodeHex( - this.f.toCharArray())).toUpperCase(); - } catch (DecoderException e) { - /* Format tested in setFingerprint(). */ - } - } - return hashedFingerprint; - } - - private String n; - public void setNickname(String nickname) { - if (nickname == null || nickname.equals("Unnamed")) { - this.n = null; - } else { - this.n = nickname; - } - } - public String getNickname() { - return this.n == null ? "Unnamed" : this.n; - } - - private String[] ad; - public void setAddresses(List<String> addresses) { - this.ad = this.collectionToStringArray(addresses); - } - public List<String> getAddresses() { - return this.stringArrayToList(this.ad); - } - - private String[] collectionToStringArray( - Collection<String> collection) { - String[] stringArray = null; - if (collection != null && !collection.isEmpty()) { - stringArray = new String[collection.size()]; - int i = 0; - for (String string : collection) { - stringArray[i++] = string; - } - } - return stringArray; - } - private List<String> stringArrayToList(String[] stringArray) { - List<String> list; - if (stringArray == null) { - list = new ArrayList<String>(); - } else { - list = Arrays.asList(stringArray); - } - return list; - } - private SortedSet<String> stringArrayToSortedSet(String[] stringArray) { - SortedSet<String> sortedSet = new TreeSet<String>(); - if (stringArray != null) { - sortedSet.addAll(Arrays.asList(stringArray)); - } - return sortedSet; - } - - private String cc; - public void setCountryCode(String countryCode) { - this.cc = countryCode; - } - public String getCountryCode() { - return this.cc; - } - - private String as; - public void setASNumber(String aSNumber) { - this.as = aSNumber; - } - public String getASNumber() { - return this.as; - } - - private String fs; - public void setFirstSeenMillis(long firstSeenMillis) { - this.fs = DateTimeHelper.format(firstSeenMillis); - } - public long getFirstSeenMillis() { - return DateTimeHelper.parse(this.fs); - } - - private String ls; - public void setLastSeenMillis(long lastSeenMillis) { - this.ls = DateTimeHelper.format(lastSeenMillis); - } - public long getLastSeenMillis() { - return DateTimeHelper.parse(this.ls); - } - - private String[] rf; - public void setRelayFlags(SortedSet<String> relayFlags) { - this.rf = this.collectionToStringArray(relayFlags); - } - public SortedSet<String> getRelayFlags() { - return this.stringArrayToSortedSet(this.rf); - } - - private long cw; - public void setConsensusWeight(long consensusWeight) { - this.cw = consensusWeight; - } - public long getConsensusWeight() { - return this.cw; - } - - private boolean r; - public void setRunning(boolean isRunning) { - this.r = isRunning; - } - public boolean isRunning() { - return this.r; - } - - private String c; - public void setContact(String contact) { - if (contact != null && contact.length() == 0) { - this.c = null; - } else { - this.c = contact; - } - } - public String getContact() { - return this.c; - } - - private String[] ff; - public void setFamilyFingerprints( - SortedSet<String> familyFingerprints) { - this.ff = this.collectionToStringArray(familyFingerprints); - } - public SortedSet<String> getFamilyFingerprints() { - return this.stringArrayToSortedSet(this.ff); - } - - public SummaryDocument(boolean isRelay, String nickname, - String fingerprint, List<String> addresses, long lastSeenMillis, - boolean running, SortedSet<String> relayFlags, long consensusWeight, - String countryCode, long firstSeenMillis, String aSNumber, - String contact, SortedSet<String> familyFingerprints) { - this.setRelay(isRelay); - this.setNickname(nickname); - this.setFingerprint(fingerprint); - this.setAddresses(addresses); - this.setLastSeenMillis(lastSeenMillis); - this.setRunning(running); - this.setRelayFlags(relayFlags); - this.setConsensusWeight(consensusWeight); - this.setCountryCode(countryCode); - this.setFirstSeenMillis(firstSeenMillis); - this.setASNumber(aSNumber); - this.setContact(contact); - this.setFamilyFingerprints(familyFingerprints); - } -} - diff --git a/src/org/torproject/onionoo/SummaryDocumentWriter.java b/src/org/torproject/onionoo/SummaryDocumentWriter.java deleted file mode 100644 index 7b257f5..0000000 --- a/src/org/torproject/onionoo/SummaryDocumentWriter.java +++ /dev/null @@ -1,87 +0,0 @@ -/* Copyright 2014 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.util.ArrayList; -import java.util.List; -import java.util.SortedSet; - -public class SummaryDocumentWriter implements DocumentWriter { - - private DocumentStore documentStore; - - public SummaryDocumentWriter() { - this.documentStore = ApplicationFactory.getDocumentStore(); - } - - private int writtenDocuments = 0, deletedDocuments = 0; - - public void writeDocuments() { - long maxLastSeenMillis = 0L; - for (String fingerprint : this.documentStore.list(NodeStatus.class)) { - NodeStatus nodeStatus = this.documentStore.retrieve( - NodeStatus.class, true, fingerprint); - if (nodeStatus != null && - nodeStatus.getLastSeenMillis() > maxLastSeenMillis) { - maxLastSeenMillis = nodeStatus.getLastSeenMillis(); - } - } - long cutoff = maxLastSeenMillis - DateTimeHelper.ONE_WEEK; - for (String fingerprint : this.documentStore.list(NodeStatus.class)) { - NodeStatus nodeStatus = this.documentStore.retrieve( - NodeStatus.class, - true, fingerprint); - if (nodeStatus == null) { - continue; - } - if (nodeStatus.getLastSeenMillis() < cutoff) { - if (this.documentStore.remove(SummaryDocument.class, - fingerprint)) { - this.deletedDocuments++; - } - continue; - } - boolean isRelay = nodeStatus.isRelay(); - String nickname = nodeStatus.getNickname(); - List<String> addresses = new ArrayList<String>(); - addresses.add(nodeStatus.getAddress()); - for (String orAddress : nodeStatus.getOrAddresses()) { - if (!addresses.contains(orAddress)) { - addresses.add(orAddress); - } - } - for (String exitAddress : nodeStatus.getExitAddresses()) { - if (!addresses.contains(exitAddress)) { - addresses.add(exitAddress); - } - } - long lastSeenMillis = nodeStatus.getLastSeenMillis(); - boolean running = nodeStatus.getRunning(); - SortedSet<String> relayFlags = nodeStatus.getRelayFlags(); - long consensusWeight = nodeStatus.getConsensusWeight(); - String countryCode = nodeStatus.getCountryCode(); - long firstSeenMillis = nodeStatus.getFirstSeenMillis(); - String aSNumber = nodeStatus.getASNumber(); - String contact = nodeStatus.getContact(); - SortedSet<String> familyFingerprints = - nodeStatus.getFamilyFingerprints(); - SummaryDocument summaryDocument = new SummaryDocument(isRelay, - nickname, fingerprint, addresses, lastSeenMillis, running, - relayFlags, consensusWeight, countryCode, firstSeenMillis, - aSNumber, contact, familyFingerprints); - if (this.documentStore.store(summaryDocument, fingerprint)) { - this.writtenDocuments++; - }; - } - Logger.printStatusTime("Wrote summary document files"); - } - - public String getStatsString() { - StringBuilder sb = new StringBuilder(); - sb.append(" " + Logger.formatDecimalNumber(this.writtenDocuments) - + " summary document files written\n"); - sb.append(" " + Logger.formatDecimalNumber(this.deletedDocuments) - + " summary document files deleted\n"); - return sb.toString(); - } -} diff --git a/src/org/torproject/onionoo/Time.java b/src/org/torproject/onionoo/Time.java deleted file mode 100644 index c969556..0000000 --- a/src/org/torproject/onionoo/Time.java +++ /dev/null @@ -1,14 +0,0 @@ -/* Copyright 2013 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -/* - * Wrapper for System.currentTimeMillis() that can be replaced with a - * custom time source for testing. - */ -public class Time { - public long currentTimeMillis() { - return System.currentTimeMillis(); - } -} - diff --git a/src/org/torproject/onionoo/UpdateStatus.java b/src/org/torproject/onionoo/UpdateStatus.java deleted file mode 100644 index a697cbf..0000000 --- a/src/org/torproject/onionoo/UpdateStatus.java +++ /dev/null @@ -1,7 +0,0 @@ -/* Copyright 2013 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -public class UpdateStatus extends Document { -} - diff --git a/src/org/torproject/onionoo/UptimeDocument.java b/src/org/torproject/onionoo/UptimeDocument.java deleted file mode 100644 index 1946e2c..0000000 --- a/src/org/torproject/onionoo/UptimeDocument.java +++ /dev/null @@ -1,23 +0,0 @@ -/* Copyright 2014 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.util.Map; - -public class UptimeDocument extends Document { - - @SuppressWarnings("unused") - private String fingerprint; - public void setFingerprint(String fingerprint) { - this.fingerprint = fingerprint; - } - - private Map<String, GraphHistory> uptime; - public void setUptime(Map<String, GraphHistory> uptime) { - this.uptime = uptime; - } - public Map<String, GraphHistory> getUptime() { - return this.uptime; - } -} - diff --git a/src/org/torproject/onionoo/UptimeDocumentWriter.java b/src/org/torproject/onionoo/UptimeDocumentWriter.java deleted file mode 100644 index 8293f77..0000000 --- a/src/org/torproject/onionoo/UptimeDocumentWriter.java +++ /dev/null @@ -1,291 +0,0 @@ -/* Copyright 2014 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.SortedSet; -import java.util.TreeSet; - -public class UptimeDocumentWriter implements FingerprintListener, - DocumentWriter { - - private DescriptorSource descriptorSource; - - private DocumentStore documentStore; - - private long now; - - public UptimeDocumentWriter() { - this.descriptorSource = ApplicationFactory.getDescriptorSource(); - this.documentStore = ApplicationFactory.getDocumentStore(); - this.now = ApplicationFactory.getTime().currentTimeMillis(); - this.registerFingerprintListeners(); - } - - private void registerFingerprintListeners() { - this.descriptorSource.registerFingerprintListener(this, - DescriptorType.RELAY_CONSENSUSES); - this.descriptorSource.registerFingerprintListener(this, - DescriptorType.BRIDGE_STATUSES); - } - - private SortedSet<String> newRelayFingerprints = new TreeSet<String>(), - newBridgeFingerprints = new TreeSet<String>(); - - public void processFingerprints(SortedSet<String> fingerprints, - boolean relay) { - if (relay) { - this.newRelayFingerprints.addAll(fingerprints); - } else { - this.newBridgeFingerprints.addAll(fingerprints); - } - } - - public void writeDocuments() { - UptimeStatus uptimeStatus = this.documentStore.retrieve( - UptimeStatus.class, true); - if (uptimeStatus == null) { - return; - } - for (String fingerprint : this.newRelayFingerprints) { - this.updateDocument(true, fingerprint, - uptimeStatus.getRelayHistory()); - } - for (String fingerprint : this.newBridgeFingerprints) { - this.updateDocument(false, fingerprint, - uptimeStatus.getBridgeHistory()); - } - Logger.printStatusTime("Wrote uptime document files"); - } - - private int writtenDocuments = 0; - - private void updateDocument(boolean relay, String fingerprint, - SortedSet<UptimeHistory> knownStatuses) { - UptimeStatus uptimeStatus = this.documentStore.retrieve( - UptimeStatus.class, true, fingerprint); - if (uptimeStatus != null) { - SortedSet<UptimeHistory> history = relay - ? uptimeStatus.getRelayHistory() - : uptimeStatus.getBridgeHistory(); - UptimeDocument uptimeDocument = this.compileUptimeDocument(relay, - fingerprint, history, knownStatuses); - this.documentStore.store(uptimeDocument, fingerprint); - this.writtenDocuments++; - } - } - - private String[] graphNames = new String[] { - "1_week", - "1_month", - "3_months", - "1_year", - "5_years" }; - - private long[] graphIntervals = new long[] { - DateTimeHelper.ONE_WEEK, - DateTimeHelper.ROUGHLY_ONE_MONTH, - DateTimeHelper.ROUGHLY_THREE_MONTHS, - DateTimeHelper.ROUGHLY_ONE_YEAR, - DateTimeHelper.ROUGHLY_FIVE_YEARS }; - - private long[] dataPointIntervals = new long[] { - DateTimeHelper.ONE_HOUR, - DateTimeHelper.FOUR_HOURS, - DateTimeHelper.TWELVE_HOURS, - DateTimeHelper.TWO_DAYS, - DateTimeHelper.TEN_DAYS }; - - private UptimeDocument compileUptimeDocument(boolean relay, - String fingerprint, SortedSet<UptimeHistory> history, - SortedSet<UptimeHistory> knownStatuses) { - UptimeDocument uptimeDocument = new UptimeDocument(); - uptimeDocument.setFingerprint(fingerprint); - Map<String, GraphHistory> uptime = - new LinkedHashMap<String, GraphHistory>(); - for (int graphIntervalIndex = 0; graphIntervalIndex < - this.graphIntervals.length; graphIntervalIndex++) { - String graphName = this.graphNames[graphIntervalIndex]; - GraphHistory graphHistory = this.compileUptimeHistory( - graphIntervalIndex, relay, history, knownStatuses); - if (graphHistory != null) { - uptime.put(graphName, graphHistory); - } - } - uptimeDocument.setUptime(uptime); - return uptimeDocument; - } - - private GraphHistory compileUptimeHistory(int graphIntervalIndex, - boolean relay, SortedSet<UptimeHistory> history, - SortedSet<UptimeHistory> knownStatuses) { - long graphInterval = this.graphIntervals[graphIntervalIndex]; - long dataPointInterval = - this.dataPointIntervals[graphIntervalIndex]; - int dataPointIntervalHours = (int) (dataPointInterval - / DateTimeHelper.ONE_HOUR); - List<Integer> uptimeDataPoints = new ArrayList<Integer>(); - long intervalStartMillis = ((this.now - graphInterval) - / dataPointInterval) * dataPointInterval; - int uptimeHours = 0; - long firstStatusStartMillis = -1L; - for (UptimeHistory hist : history) { - if (hist.isRelay() != relay) { - continue; - } - if (firstStatusStartMillis < 0L) { - firstStatusStartMillis = hist.getStartMillis(); - } - long histEndMillis = hist.getStartMillis() + DateTimeHelper.ONE_HOUR - * hist.getUptimeHours(); - if (histEndMillis < intervalStartMillis) { - continue; - } - while (hist.getStartMillis() >= intervalStartMillis - + dataPointInterval) { - if (firstStatusStartMillis < intervalStartMillis - + dataPointInterval) { - uptimeDataPoints.add(uptimeHours); - } else { - uptimeDataPoints.add(-1); - } - uptimeHours = 0; - intervalStartMillis += dataPointInterval; - } - while (histEndMillis >= intervalStartMillis + dataPointInterval) { - uptimeHours += (int) ((intervalStartMillis + dataPointInterval - - Math.max(hist.getStartMillis(), intervalStartMillis)) - / DateTimeHelper.ONE_HOUR); - uptimeDataPoints.add(uptimeHours); - uptimeHours = 0; - intervalStartMillis += dataPointInterval; - } - uptimeHours += (int) ((histEndMillis - Math.max( - hist.getStartMillis(), intervalStartMillis)) - / DateTimeHelper.ONE_HOUR); - } - uptimeDataPoints.add(uptimeHours); - List<Integer> statusDataPoints = new ArrayList<Integer>(); - intervalStartMillis = ((this.now - graphInterval) - / dataPointInterval) * dataPointInterval; - int statusHours = -1; - for (UptimeHistory hist : knownStatuses) { - if (hist.isRelay() != relay) { - continue; - } - long histEndMillis = hist.getStartMillis() + DateTimeHelper.ONE_HOUR - * hist.getUptimeHours(); - if (histEndMillis < intervalStartMillis) { - continue; - } - while (hist.getStartMillis() >= intervalStartMillis - + dataPointInterval) { - statusDataPoints.add(statusHours * 5 > dataPointIntervalHours - ? statusHours : -1); - statusHours = -1; - intervalStartMillis += dataPointInterval; - } - while (histEndMillis >= intervalStartMillis + dataPointInterval) { - if (statusHours < 0) { - statusHours = 0; - } - statusHours += (int) ((intervalStartMillis + dataPointInterval - - Math.max(Math.max(hist.getStartMillis(), - firstStatusStartMillis), intervalStartMillis)) - / DateTimeHelper.ONE_HOUR); - statusDataPoints.add(statusHours * 5 > dataPointIntervalHours - ? statusHours : -1); - statusHours = -1; - intervalStartMillis += dataPointInterval; - } - if (statusHours < 0) { - statusHours = 0; - } - statusHours += (int) ((histEndMillis - Math.max(Math.max( - hist.getStartMillis(), firstStatusStartMillis), - intervalStartMillis)) / DateTimeHelper.ONE_HOUR); - } - if (statusHours > 0) { - statusDataPoints.add(statusHours * 5 > dataPointIntervalHours - ? statusHours : -1); - } - List<Double> dataPoints = new ArrayList<Double>(); - for (int dataPointIndex = 0; dataPointIndex < statusDataPoints.size(); - dataPointIndex++) { - if (dataPointIndex >= uptimeDataPoints.size()) { - dataPoints.add(0.0); - } else if (uptimeDataPoints.get(dataPointIndex) >= 0 && - statusDataPoints.get(dataPointIndex) > 0) { - dataPoints.add(((double) uptimeDataPoints.get(dataPointIndex)) - / ((double) statusDataPoints.get(dataPointIndex))); - } else { - dataPoints.add(-1.0); - } - } - int firstNonNullIndex = -1, lastNonNullIndex = -1; - for (int dataPointIndex = 0; dataPointIndex < dataPoints.size(); - dataPointIndex++) { - double dataPoint = dataPoints.get(dataPointIndex); - if (dataPoint >= 0.0) { - if (firstNonNullIndex < 0) { - firstNonNullIndex = dataPointIndex; - } - lastNonNullIndex = dataPointIndex; - } - } - if (firstNonNullIndex < 0) { - return null; - } - long firstDataPointMillis = (((this.now - graphInterval) - / dataPointInterval) + firstNonNullIndex) - * dataPointInterval + dataPointInterval / 2L; - if (graphIntervalIndex > 0 && firstDataPointMillis >= - this.now - graphIntervals[graphIntervalIndex - 1]) { - /* Skip uptime history object, because it doesn't contain - * anything new that wasn't already contained in the last - * uptime history object(s). */ - return null; - } - long lastDataPointMillis = firstDataPointMillis - + (lastNonNullIndex - firstNonNullIndex) * dataPointInterval; - int count = lastNonNullIndex - firstNonNullIndex + 1; - GraphHistory graphHistory = new GraphHistory(); - graphHistory.setFirst(DateTimeHelper.format(firstDataPointMillis)); - graphHistory.setLast(DateTimeHelper.format(lastDataPointMillis)); - graphHistory.setInterval((int) (dataPointInterval - / DateTimeHelper.ONE_SECOND)); - graphHistory.setFactor(1.0 / 999.0); - graphHistory.setCount(count); - int previousNonNullIndex = -2; - boolean foundTwoAdjacentDataPoints = false; - List<Integer> values = new ArrayList<Integer>(); - for (int dataPointIndex = firstNonNullIndex; dataPointIndex <= - lastNonNullIndex; dataPointIndex++) { - double dataPoint = dataPoints.get(dataPointIndex); - if (dataPoint >= 0.0) { - if (dataPointIndex - previousNonNullIndex == 1) { - foundTwoAdjacentDataPoints = true; - } - previousNonNullIndex = dataPointIndex; - } - values.add(dataPoint < -0.5 ? null : ((int) (dataPoint * 999.0))); - } - graphHistory.setValues(values); - if (foundTwoAdjacentDataPoints) { - return graphHistory; - } else { - return null; - } - } - - public String getStatsString() { - StringBuilder sb = new StringBuilder(); - sb.append(" " + Logger.formatDecimalNumber(this.writtenDocuments) - + " uptime document files written\n"); - return sb.toString(); - } -} - diff --git a/src/org/torproject/onionoo/UptimeStatus.java b/src/org/torproject/onionoo/UptimeStatus.java deleted file mode 100644 index 92ca629..0000000 --- a/src/org/torproject/onionoo/UptimeStatus.java +++ /dev/null @@ -1,226 +0,0 @@ -/* Copyright 2014 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.util.Scanner; -import java.util.SortedSet; -import java.util.TreeSet; - -class UptimeHistory - implements Comparable<UptimeHistory> { - - private boolean relay; - public boolean isRelay() { - return this.relay; - } - - private long startMillis; - public long getStartMillis() { - return this.startMillis; - } - - private int uptimeHours; - public int getUptimeHours() { - return this.uptimeHours; - } - - UptimeHistory(boolean relay, long startMillis, - int uptimeHours) { - this.relay = relay; - this.startMillis = startMillis; - this.uptimeHours = uptimeHours; - } - - public static UptimeHistory fromString(String uptimeHistoryString) { - String[] parts = uptimeHistoryString.split(" ", 3); - if (parts.length != 3) { - return null; - } - boolean relay = false; - if (parts[0].equals("r")) { - relay = true; - } else if (!parts[0].equals("b")) { - return null; - } - long startMillis = DateTimeHelper.parse(parts[1], - DateTimeHelper.DATEHOUR_NOSPACE_FORMAT); - if (startMillis < 0L) { - return null; - } - int uptimeHours = -1; - try { - uptimeHours = Integer.parseInt(parts[2]); - } catch (NumberFormatException e) { - return null; - } - return new UptimeHistory(relay, startMillis, uptimeHours); - } - - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(this.relay ? "r" : "b"); - sb.append(" " + DateTimeHelper.format(this.startMillis, - DateTimeHelper.DATEHOUR_NOSPACE_FORMAT)); - sb.append(" " + String.format("%d", this.uptimeHours)); - return sb.toString(); - } - - public void addUptime(UptimeHistory other) { - this.uptimeHours += other.uptimeHours; - if (this.startMillis > other.startMillis) { - this.startMillis = other.startMillis; - } - } - - public int compareTo(UptimeHistory other) { - if (this.relay && !other.relay) { - return -1; - } else if (!this.relay && other.relay) { - return 1; - } - return this.startMillis < other.startMillis ? -1 : - this.startMillis > other.startMillis ? 1 : 0; - } - - public boolean equals(Object other) { - return other instanceof UptimeHistory && - this.relay == ((UptimeHistory) other).relay && - this.startMillis == ((UptimeHistory) other).startMillis; - } - - public int hashCode() { - return (int) this.startMillis + (this.relay ? 1 : 0); - } -} - -public class UptimeStatus extends Document { - - private transient String fingerprint; - - private transient boolean isDirty = false; - - private SortedSet<UptimeHistory> relayHistory = - new TreeSet<UptimeHistory>(); - public void setRelayHistory(SortedSet<UptimeHistory> relayHistory) { - this.relayHistory = relayHistory; - } - public SortedSet<UptimeHistory> getRelayHistory() { - return this.relayHistory; - } - - private SortedSet<UptimeHistory> bridgeHistory = - new TreeSet<UptimeHistory>(); - public void setBridgeHistory(SortedSet<UptimeHistory> bridgeHistory) { - this.bridgeHistory = bridgeHistory; - } - public SortedSet<UptimeHistory> getBridgeHistory() { - return this.bridgeHistory; - } - - public static UptimeStatus loadOrCreate(String fingerprint) { - UptimeStatus uptimeStatus = (fingerprint == null) ? - ApplicationFactory.getDocumentStore().retrieve( - UptimeStatus.class, true) : - ApplicationFactory.getDocumentStore().retrieve( - UptimeStatus.class, true, fingerprint); - if (uptimeStatus == null) { - uptimeStatus = new UptimeStatus(); - } - uptimeStatus.fingerprint = fingerprint; - return uptimeStatus; - } - - public void fromDocumentString(String documentString) { - Scanner s = new Scanner(documentString); - while (s.hasNextLine()) { - String line = s.nextLine(); - UptimeHistory parsedLine = UptimeHistory.fromString(line); - if (parsedLine != null) { - if (parsedLine.isRelay()) { - this.relayHistory.add(parsedLine); - } else { - this.bridgeHistory.add(parsedLine); - } - } else { - System.err.println("Could not parse uptime history line '" - + line + "'. Skipping."); - } - } - s.close(); - } - - public void addToHistory(boolean relay, SortedSet<Long> newIntervals) { - for (long startMillis : newIntervals) { - SortedSet<UptimeHistory> history = relay ? this.relayHistory - : this.bridgeHistory; - UptimeHistory interval = new UptimeHistory(relay, startMillis, 1); - if (!history.headSet(interval).isEmpty()) { - UptimeHistory prev = history.headSet(interval).last(); - if (prev.isRelay() == interval.isRelay() && - prev.getStartMillis() + DateTimeHelper.ONE_HOUR - * prev.getUptimeHours() > interval.getStartMillis()) { - continue; - } - } - if (!history.tailSet(interval).isEmpty()) { - UptimeHistory next = history.tailSet(interval).first(); - if (next.isRelay() == interval.isRelay() && - next.getStartMillis() < interval.getStartMillis() - + DateTimeHelper.ONE_HOUR) { - continue; - } - } - history.add(interval); - this.isDirty = true; - } - } - - public void storeIfChanged() { - if (this.isDirty) { - this.compressHistory(this.relayHistory); - this.compressHistory(this.bridgeHistory); - if (fingerprint == null) { - ApplicationFactory.getDocumentStore().store(this); - } else { - ApplicationFactory.getDocumentStore().store(this, - this.fingerprint); - } - this.isDirty = false; - } - } - - private void compressHistory(SortedSet<UptimeHistory> history) { - SortedSet<UptimeHistory> uncompressedHistory = - new TreeSet<UptimeHistory>(history); - history.clear(); - UptimeHistory lastInterval = null; - for (UptimeHistory interval : uncompressedHistory) { - if (lastInterval != null && - lastInterval.getStartMillis() + DateTimeHelper.ONE_HOUR - * lastInterval.getUptimeHours() == interval.getStartMillis() && - lastInterval.isRelay() == interval.isRelay()) { - lastInterval.addUptime(interval); - } else { - if (lastInterval != null) { - history.add(lastInterval); - } - lastInterval = interval; - } - } - if (lastInterval != null) { - history.add(lastInterval); - } - } - - public String toDocumentString() { - StringBuilder sb = new StringBuilder(); - for (UptimeHistory interval : this.relayHistory) { - sb.append(interval.toString() + "\n"); - } - for (UptimeHistory interval : this.bridgeHistory) { - sb.append(interval.toString() + "\n"); - } - return sb.toString(); - } -} - diff --git a/src/org/torproject/onionoo/UptimeStatusUpdater.java b/src/org/torproject/onionoo/UptimeStatusUpdater.java deleted file mode 100644 index eccc2f2..0000000 --- a/src/org/torproject/onionoo/UptimeStatusUpdater.java +++ /dev/null @@ -1,126 +0,0 @@ -/* Copyright 2014 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.util.Map; -import java.util.SortedMap; -import java.util.SortedSet; -import java.util.TreeMap; -import java.util.TreeSet; - -import org.torproject.descriptor.BridgeNetworkStatus; -import org.torproject.descriptor.Descriptor; -import org.torproject.descriptor.NetworkStatusEntry; -import org.torproject.descriptor.RelayNetworkStatusConsensus; - -public class UptimeStatusUpdater implements DescriptorListener, - StatusUpdater { - - private DescriptorSource descriptorSource; - - public UptimeStatusUpdater() { - this.descriptorSource = ApplicationFactory.getDescriptorSource(); - this.registerDescriptorListeners(); - } - - private void registerDescriptorListeners() { - this.descriptorSource.registerDescriptorListener(this, - DescriptorType.RELAY_CONSENSUSES); - this.descriptorSource.registerDescriptorListener(this, - DescriptorType.BRIDGE_STATUSES); - } - - public void processDescriptor(Descriptor descriptor, boolean relay) { - if (descriptor instanceof RelayNetworkStatusConsensus) { - this.processRelayNetworkStatusConsensus( - (RelayNetworkStatusConsensus) descriptor); - } else if (descriptor instanceof BridgeNetworkStatus) { - this.processBridgeNetworkStatus( - (BridgeNetworkStatus) descriptor); - } - } - - private SortedSet<Long> newRelayStatuses = new TreeSet<Long>(), - newBridgeStatuses = new TreeSet<Long>(); - private SortedMap<String, SortedSet<Long>> - newRunningRelays = new TreeMap<String, SortedSet<Long>>(), - newRunningBridges = new TreeMap<String, SortedSet<Long>>(); - - private void processRelayNetworkStatusConsensus( - RelayNetworkStatusConsensus consensus) { - SortedSet<String> fingerprints = new TreeSet<String>(); - for (NetworkStatusEntry entry : - consensus.getStatusEntries().values()) { - if (entry.getFlags().contains("Running")) { - fingerprints.add(entry.getFingerprint()); - } - } - if (!fingerprints.isEmpty()) { - long dateHourMillis = (consensus.getValidAfterMillis() - / DateTimeHelper.ONE_HOUR) * DateTimeHelper.ONE_HOUR; - for (String fingerprint : fingerprints) { - if (!this.newRunningRelays.containsKey(fingerprint)) { - this.newRunningRelays.put(fingerprint, new TreeSet<Long>()); - } - this.newRunningRelays.get(fingerprint).add(dateHourMillis); - } - this.newRelayStatuses.add(dateHourMillis); - } - } - - private void processBridgeNetworkStatus(BridgeNetworkStatus status) { - SortedSet<String> fingerprints = new TreeSet<String>(); - for (NetworkStatusEntry entry : - status.getStatusEntries().values()) { - if (entry.getFlags().contains("Running")) { - fingerprints.add(entry.getFingerprint()); - } - } - if (!fingerprints.isEmpty()) { - long dateHourMillis = (status.getPublishedMillis() - / DateTimeHelper.ONE_HOUR) * DateTimeHelper.ONE_HOUR; - for (String fingerprint : fingerprints) { - if (!this.newRunningBridges.containsKey(fingerprint)) { - this.newRunningBridges.put(fingerprint, new TreeSet<Long>()); - } - this.newRunningBridges.get(fingerprint).add(dateHourMillis); - } - this.newBridgeStatuses.add(dateHourMillis); - } - } - - public void updateStatuses() { - for (Map.Entry<String, SortedSet<Long>> e : - this.newRunningRelays.entrySet()) { - this.updateStatus(true, e.getKey(), e.getValue()); - } - this.updateStatus(true, null, this.newRelayStatuses); - for (Map.Entry<String, SortedSet<Long>> e : - this.newRunningBridges.entrySet()) { - this.updateStatus(false, e.getKey(), e.getValue()); - } - this.updateStatus(false, null, this.newBridgeStatuses); - } - - private void updateStatus(boolean relay, String fingerprint, - SortedSet<Long> newUptimeHours) { - UptimeStatus uptimeStatus = UptimeStatus.loadOrCreate(fingerprint); - uptimeStatus.addToHistory(relay, newUptimeHours); - uptimeStatus.storeIfChanged(); - } - - public String getStatsString() { - StringBuilder sb = new StringBuilder(); - sb.append(" " + Logger.formatDecimalNumber( - this.newRelayStatuses.size()) + " hours of relay uptimes " - + "processed\n"); - sb.append(" " + Logger.formatDecimalNumber( - this.newBridgeStatuses.size()) + " hours of bridge uptimes " - + "processed\n"); - sb.append(" " + Logger.formatDecimalNumber( - this.newRunningRelays.size() + this.newRunningBridges.size()) - + " uptime status files updated\n"); - return sb.toString(); - } -} - diff --git a/src/org/torproject/onionoo/WeightsDocument.java b/src/org/torproject/onionoo/WeightsDocument.java deleted file mode 100644 index 4cd0021..0000000 --- a/src/org/torproject/onionoo/WeightsDocument.java +++ /dev/null @@ -1,64 +0,0 @@ -/* Copyright 2014 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.util.Map; - -public class WeightsDocument extends Document { - - @SuppressWarnings("unused") - private String fingerprint; - public void setFingerprint(String fingerprint) { - this.fingerprint = fingerprint; - } - - @SuppressWarnings("unused") - private Map<String, GraphHistory> advertised_bandwidth_fraction; - public void setAdvertisedBandwidthFraction( - Map<String, GraphHistory> advertisedBandwidthFraction) { - this.advertised_bandwidth_fraction = advertisedBandwidthFraction; - } - - @SuppressWarnings("unused") - private Map<String, GraphHistory> consensus_weight_fraction; - public void setConsensusWeightFraction( - Map<String, GraphHistory> consensusWeightFraction) { - this.consensus_weight_fraction = consensusWeightFraction; - } - - @SuppressWarnings("unused") - private Map<String, GraphHistory> guard_probability; - public void setGuardProbability( - Map<String, GraphHistory> guardProbability) { - this.guard_probability = guardProbability; - } - - @SuppressWarnings("unused") - private Map<String, GraphHistory> middle_probability; - public void setMiddleProbability( - Map<String, GraphHistory> middleProbability) { - this.middle_probability = middleProbability; - } - - @SuppressWarnings("unused") - private Map<String, GraphHistory> exit_probability; - public void setExitProbability( - Map<String, GraphHistory> exitProbability) { - this.exit_probability = exitProbability; - } - - @SuppressWarnings("unused") - private Map<String, GraphHistory> advertised_bandwidth; - public void setAdvertisedBandwidth( - Map<String, GraphHistory> advertisedBandwidth) { - this.advertised_bandwidth = advertisedBandwidth; - } - - @SuppressWarnings("unused") - private Map<String, GraphHistory> consensus_weight; - public void setConsensusWeight( - Map<String, GraphHistory> consensusWeight) { - this.consensus_weight = consensusWeight; - } -} - diff --git a/src/org/torproject/onionoo/WeightsDocumentWriter.java b/src/org/torproject/onionoo/WeightsDocumentWriter.java deleted file mode 100644 index 2e0d465..0000000 --- a/src/org/torproject/onionoo/WeightsDocumentWriter.java +++ /dev/null @@ -1,222 +0,0 @@ -/* Copyright 2012--2014 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.SortedMap; -import java.util.SortedSet; - -public class WeightsDocumentWriter implements FingerprintListener, - DocumentWriter { - - private DescriptorSource descriptorSource; - - private DocumentStore documentStore; - - private long now; - - public WeightsDocumentWriter() { - this.descriptorSource = ApplicationFactory.getDescriptorSource(); - this.documentStore = ApplicationFactory.getDocumentStore(); - this.now = ApplicationFactory.getTime().currentTimeMillis(); - this.registerFingerprintListeners(); - } - - private void registerFingerprintListeners() { - this.descriptorSource.registerFingerprintListener(this, - DescriptorType.RELAY_CONSENSUSES); - this.descriptorSource.registerFingerprintListener(this, - DescriptorType.RELAY_SERVER_DESCRIPTORS); - } - - private Set<String> updateWeightsDocuments = new HashSet<String>(); - - public void processFingerprints(SortedSet<String> fingerprints, - boolean relay) { - if (relay) { - this.updateWeightsDocuments.addAll(fingerprints); - } - } - - public void writeDocuments() { - this.writeWeightsDataFiles(); - Logger.printStatusTime("Wrote weights document files"); - } - - private void writeWeightsDataFiles() { - for (String fingerprint : this.updateWeightsDocuments) { - WeightsStatus weightsStatus = this.documentStore.retrieve( - WeightsStatus.class, true, fingerprint); - if (weightsStatus == null) { - continue; - } - SortedMap<long[], double[]> history = weightsStatus.getHistory(); - WeightsDocument weightsDocument = this.compileWeightsDocument( - fingerprint, history); - this.documentStore.store(weightsDocument, fingerprint); - } - Logger.printStatusTime("Wrote weights document files"); - } - - private String[] graphNames = new String[] { - "1_week", - "1_month", - "3_months", - "1_year", - "5_years" }; - - private long[] graphIntervals = new long[] { - DateTimeHelper.ONE_WEEK, - DateTimeHelper.ROUGHLY_ONE_MONTH, - DateTimeHelper.ROUGHLY_THREE_MONTHS, - DateTimeHelper.ROUGHLY_ONE_YEAR, - DateTimeHelper.ROUGHLY_FIVE_YEARS }; - - private long[] dataPointIntervals = new long[] { - DateTimeHelper.ONE_HOUR, - DateTimeHelper.FOUR_HOURS, - DateTimeHelper.TWELVE_HOURS, - DateTimeHelper.TWO_DAYS, - DateTimeHelper.TEN_DAYS }; - - private WeightsDocument compileWeightsDocument(String fingerprint, - SortedMap<long[], double[]> history) { - WeightsDocument weightsDocument = new WeightsDocument(); - weightsDocument.setFingerprint(fingerprint); - weightsDocument.setAdvertisedBandwidthFraction( - this.compileGraphType(history, 0)); - weightsDocument.setConsensusWeightFraction( - this.compileGraphType(history, 1)); - weightsDocument.setGuardProbability( - this.compileGraphType(history, 2)); - weightsDocument.setMiddleProbability( - this.compileGraphType(history, 3)); - weightsDocument.setExitProbability( - this.compileGraphType(history, 4)); - weightsDocument.setAdvertisedBandwidth( - this.compileGraphType(history, 5)); - weightsDocument.setConsensusWeight( - this.compileGraphType(history, 6)); - return weightsDocument; - } - - private Map<String, GraphHistory> compileGraphType( - SortedMap<long[], double[]> history, int graphTypeIndex) { - Map<String, GraphHistory> graphs = - new LinkedHashMap<String, GraphHistory>(); - for (int graphIntervalIndex = 0; graphIntervalIndex < - this.graphIntervals.length; graphIntervalIndex++) { - String graphName = this.graphNames[graphIntervalIndex]; - GraphHistory graphHistory = this.compileWeightsHistory( - graphTypeIndex, graphIntervalIndex, history); - if (graphHistory != null) { - graphs.put(graphName, graphHistory); - } - } - return graphs; - } - - private GraphHistory compileWeightsHistory(int graphTypeIndex, - int graphIntervalIndex, SortedMap<long[], double[]> history) { - long graphInterval = this.graphIntervals[graphIntervalIndex]; - long dataPointInterval = - this.dataPointIntervals[graphIntervalIndex]; - List<Double> dataPoints = new ArrayList<Double>(); - long intervalStartMillis = ((this.now - graphInterval) - / dataPointInterval) * dataPointInterval; - long totalMillis = 0L; - double totalWeightTimesMillis = 0.0; - for (Map.Entry<long[], double[]> e : history.entrySet()) { - long startMillis = e.getKey()[0], endMillis = e.getKey()[1]; - double weight = e.getValue()[graphTypeIndex]; - if (endMillis < intervalStartMillis) { - continue; - } - while ((intervalStartMillis / dataPointInterval) != - (endMillis / dataPointInterval)) { - dataPoints.add(totalMillis * 5L < dataPointInterval - ? -1.0 : totalWeightTimesMillis / (double) totalMillis); - totalWeightTimesMillis = 0.0; - totalMillis = 0L; - intervalStartMillis += dataPointInterval; - } - if (weight >= 0.0) { - totalWeightTimesMillis += weight - * ((double) (endMillis - startMillis)); - totalMillis += (endMillis - startMillis); - } - } - dataPoints.add(totalMillis * 5L < dataPointInterval - ? -1.0 : totalWeightTimesMillis / (double) totalMillis); - double maxValue = 0.0; - int firstNonNullIndex = -1, lastNonNullIndex = -1; - for (int dataPointIndex = 0; dataPointIndex < dataPoints.size(); - dataPointIndex++) { - double dataPoint = dataPoints.get(dataPointIndex); - if (dataPoint >= 0.0) { - if (firstNonNullIndex < 0) { - firstNonNullIndex = dataPointIndex; - } - lastNonNullIndex = dataPointIndex; - if (dataPoint > maxValue) { - maxValue = dataPoint; - } - } - } - if (firstNonNullIndex < 0) { - return null; - } - long firstDataPointMillis = (((this.now - graphInterval) - / dataPointInterval) + firstNonNullIndex) * dataPointInterval - + dataPointInterval / 2L; - if (graphIntervalIndex > 0 && firstDataPointMillis >= - this.now - graphIntervals[graphIntervalIndex - 1]) { - /* Skip weights history object, because it doesn't contain - * anything new that wasn't already contained in the last - * weights history object(s). */ - return null; - } - long lastDataPointMillis = firstDataPointMillis - + (lastNonNullIndex - firstNonNullIndex) * dataPointInterval; - double factor = ((double) maxValue) / 999.0; - int count = lastNonNullIndex - firstNonNullIndex + 1; - GraphHistory graphHistory = new GraphHistory(); - graphHistory.setFirst(DateTimeHelper.format(firstDataPointMillis)); - graphHistory.setLast(DateTimeHelper.format(lastDataPointMillis)); - graphHistory.setInterval((int) (dataPointInterval - / DateTimeHelper.ONE_SECOND)); - graphHistory.setFactor(factor); - graphHistory.setCount(count); - int previousNonNullIndex = -2; - boolean foundTwoAdjacentDataPoints = false; - List<Integer> values = new ArrayList<Integer>(); - for (int dataPointIndex = firstNonNullIndex; dataPointIndex <= - lastNonNullIndex; dataPointIndex++) { - double dataPoint = dataPoints.get(dataPointIndex); - if (dataPoint >= 0.0) { - if (dataPointIndex - previousNonNullIndex == 1) { - foundTwoAdjacentDataPoints = true; - } - previousNonNullIndex = dataPointIndex; - } - values.add(dataPoint < 0.0 ? null : - (int) ((dataPoint * 999.0) / maxValue)); - } - graphHistory.setValues(values); - if (foundTwoAdjacentDataPoints) { - return graphHistory; - } else { - return null; - } - } - - public String getStatsString() { - /* TODO Add statistics string. */ - return null; - } -} diff --git a/src/org/torproject/onionoo/WeightsStatus.java b/src/org/torproject/onionoo/WeightsStatus.java deleted file mode 100644 index 6d06ca4..0000000 --- a/src/org/torproject/onionoo/WeightsStatus.java +++ /dev/null @@ -1,97 +0,0 @@ -package org.torproject.onionoo; - -import java.util.Comparator; -import java.util.HashMap; -import java.util.Map; -import java.util.Scanner; -import java.util.SortedMap; -import java.util.TreeMap; - -public class WeightsStatus extends Document { - - private SortedMap<long[], double[]> history = - new TreeMap<long[], double[]>(new Comparator<long[]>() { - public int compare(long[] a, long[] b) { - return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0; - } - }); - public void setHistory(SortedMap<long[], double[]> history) { - this.history = history; - } - public SortedMap<long[], double[]> getHistory() { - return this.history; - } - - private Map<String, Integer> advertisedBandwidths = - new HashMap<String, Integer>(); - public Map<String, Integer> getAdvertisedBandwidths() { - return this.advertisedBandwidths; - } - - public void fromDocumentString(String documentString) { - Scanner s = new Scanner(documentString); - while (s.hasNextLine()) { - String line = s.nextLine(); - String[] parts = line.split(" "); - if (parts.length == 2) { - String descriptorDigest = parts[0]; - int advertisedBandwidth = Integer.parseInt(parts[1]); - this.advertisedBandwidths.put(descriptorDigest, - advertisedBandwidth); - continue; - } - if (parts.length != 9 && parts.length != 11) { - System.err.println("Illegal line '" + line + "' in weights " - + "status file. Skipping this line."); - continue; - } - if (parts[4].equals("NaN")) { - /* Remove corrupt lines written on 2013-07-07 and the days - * after. */ - continue; - } - long validAfterMillis = DateTimeHelper.parse(parts[0] + " " - + parts[1]); - long freshUntilMillis = DateTimeHelper.parse(parts[2] + " " - + parts[3]); - if (validAfterMillis < 0L || freshUntilMillis < 0L) { - System.err.println("Could not parse timestamp while reading " - + "weights status file. Skipping."); - break; - } - long[] interval = new long[] { validAfterMillis, freshUntilMillis }; - double[] weights = new double[] { - Double.parseDouble(parts[4]), - Double.parseDouble(parts[5]), - Double.parseDouble(parts[6]), - Double.parseDouble(parts[7]), - Double.parseDouble(parts[8]), -1.0, -1.0 }; - if (parts.length == 11) { - weights[5] = Double.parseDouble(parts[9]); - weights[6] = Double.parseDouble(parts[10]); - } - this.history.put(interval, weights); - } - s.close(); - } - - public String toDocumentString() { - StringBuilder sb = new StringBuilder(); - for (Map.Entry<String, Integer> e : - this.advertisedBandwidths.entrySet()) { - sb.append(e.getKey() + " " + String.valueOf(e.getValue()) + "\n"); - } - for (Map.Entry<long[], double[]> e : history.entrySet()) { - long[] fresh = e.getKey(); - double[] weights = e.getValue(); - sb.append(DateTimeHelper.format(fresh[0]) + " " - + DateTimeHelper.format(fresh[1])); - for (double weight : weights) { - sb.append(String.format(" %.12f", weight)); - } - sb.append("\n"); - } - return sb.toString(); - } -} - diff --git a/src/org/torproject/onionoo/WeightsStatusUpdater.java b/src/org/torproject/onionoo/WeightsStatusUpdater.java deleted file mode 100644 index 5932da6..0000000 --- a/src/org/torproject/onionoo/WeightsStatusUpdater.java +++ /dev/null @@ -1,328 +0,0 @@ -/* Copyright 2012--2014 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.onionoo; - -import java.util.Arrays; -import java.util.Map; -import java.util.SortedMap; -import java.util.SortedSet; -import java.util.TreeMap; -import java.util.TreeSet; - -import org.torproject.descriptor.Descriptor; -import org.torproject.descriptor.NetworkStatusEntry; -import org.torproject.descriptor.RelayNetworkStatusConsensus; -import org.torproject.descriptor.ServerDescriptor; - -public class WeightsStatusUpdater implements DescriptorListener, - StatusUpdater { - - private DescriptorSource descriptorSource; - - private DocumentStore documentStore; - - private long now; - - public WeightsStatusUpdater() { - this.descriptorSource = ApplicationFactory.getDescriptorSource(); - this.documentStore = ApplicationFactory.getDocumentStore(); - this.now = ApplicationFactory.getTime().currentTimeMillis(); - this.registerDescriptorListeners(); - } - - private void registerDescriptorListeners() { - this.descriptorSource.registerDescriptorListener(this, - DescriptorType.RELAY_CONSENSUSES); - this.descriptorSource.registerDescriptorListener(this, - DescriptorType.RELAY_SERVER_DESCRIPTORS); - } - - public void processDescriptor(Descriptor descriptor, boolean relay) { - if (descriptor instanceof ServerDescriptor) { - this.processRelayServerDescriptor((ServerDescriptor) descriptor); - } else if (descriptor instanceof RelayNetworkStatusConsensus) { - this.processRelayNetworkConsensus( - (RelayNetworkStatusConsensus) descriptor); - } - } - - public void updateStatuses() { - /* Nothing to do. */ - } - - private void processRelayNetworkConsensus( - RelayNetworkStatusConsensus consensus) { - long validAfterMillis = consensus.getValidAfterMillis(), - freshUntilMillis = consensus.getFreshUntilMillis(); - SortedMap<String, double[]> pathSelectionWeights = - this.calculatePathSelectionProbabilities(consensus); - this.updateWeightsHistory(validAfterMillis, freshUntilMillis, - pathSelectionWeights); - } - - private void processRelayServerDescriptor( - ServerDescriptor serverDescriptor) { - String digest = serverDescriptor.getServerDescriptorDigest(). - toUpperCase(); - int advertisedBandwidth = Math.min(Math.min( - serverDescriptor.getBandwidthBurst(), - serverDescriptor.getBandwidthObserved()), - serverDescriptor.getBandwidthRate()); - String fingerprint = serverDescriptor.getFingerprint(); - WeightsStatus weightsStatus = this.documentStore.retrieve( - WeightsStatus.class, true, fingerprint); - if (weightsStatus == null) { - weightsStatus = new WeightsStatus(); - } - weightsStatus.getAdvertisedBandwidths().put(digest, - advertisedBandwidth); - this.documentStore.store(weightsStatus, fingerprint); -} - - private void updateWeightsHistory(long validAfterMillis, - long freshUntilMillis, - SortedMap<String, double[]> pathSelectionWeights) { - String fingerprint = null; - double[] weights = null; - do { - fingerprint = null; - synchronized (pathSelectionWeights) { - if (!pathSelectionWeights.isEmpty()) { - fingerprint = pathSelectionWeights.firstKey(); - weights = pathSelectionWeights.remove(fingerprint); - } - } - if (fingerprint != null) { - this.addToHistory(fingerprint, validAfterMillis, - freshUntilMillis, weights); - } - } while (fingerprint != null); - } - - private SortedMap<String, double[]> calculatePathSelectionProbabilities( - RelayNetworkStatusConsensus consensus) { - boolean containsBandwidthWeights = false; - double wgg = 1.0, wgd = 1.0, wmg = 1.0, wmm = 1.0, wme = 1.0, - wmd = 1.0, wee = 1.0, wed = 1.0; - SortedMap<String, Integer> bandwidthWeights = - consensus.getBandwidthWeights(); - if (bandwidthWeights != null) { - SortedSet<String> missingWeightKeys = new TreeSet<String>( - Arrays.asList("Wgg,Wgd,Wmg,Wmm,Wme,Wmd,Wee,Wed".split(","))); - missingWeightKeys.removeAll(bandwidthWeights.keySet()); - if (missingWeightKeys.isEmpty()) { - wgg = ((double) bandwidthWeights.get("Wgg")) / 10000.0; - wgd = ((double) bandwidthWeights.get("Wgd")) / 10000.0; - wmg = ((double) bandwidthWeights.get("Wmg")) / 10000.0; - wmm = ((double) bandwidthWeights.get("Wmm")) / 10000.0; - wme = ((double) bandwidthWeights.get("Wme")) / 10000.0; - wmd = ((double) bandwidthWeights.get("Wmd")) / 10000.0; - wee = ((double) bandwidthWeights.get("Wee")) / 10000.0; - wed = ((double) bandwidthWeights.get("Wed")) / 10000.0; - containsBandwidthWeights = true; - } - } - SortedMap<String, Double> - advertisedBandwidths = new TreeMap<String, Double>(), - consensusWeights = new TreeMap<String, Double>(), - guardWeights = new TreeMap<String, Double>(), - middleWeights = new TreeMap<String, Double>(), - exitWeights = new TreeMap<String, Double>(); - double totalAdvertisedBandwidth = 0.0; - double totalConsensusWeight = 0.0; - double totalGuardWeight = 0.0; - double totalMiddleWeight = 0.0; - double totalExitWeight = 0.0; - for (NetworkStatusEntry relay : - consensus.getStatusEntries().values()) { - String fingerprint = relay.getFingerprint(); - if (!relay.getFlags().contains("Running")) { - continue; - } - String digest = relay.getDescriptor().toUpperCase(); - WeightsStatus weightsStatus = this.documentStore.retrieve( - WeightsStatus.class, true, fingerprint); - if (weightsStatus != null && - weightsStatus.getAdvertisedBandwidths() != null && - weightsStatus.getAdvertisedBandwidths().containsKey(digest)) { - /* Read advertised bandwidth from weights status file. Server - * descriptors are parsed before consensuses, so we're sure that - * if there's a server descriptor for this relay, it'll be - * contained in the weights status file by now. */ - double advertisedBandwidth = - (double) weightsStatus.getAdvertisedBandwidths().get(digest); - advertisedBandwidths.put(fingerprint, advertisedBandwidth); - totalAdvertisedBandwidth += advertisedBandwidth; - } - if (relay.getBandwidth() >= 0L) { - double consensusWeight = (double) relay.getBandwidth(); - consensusWeights.put(fingerprint, consensusWeight); - totalConsensusWeight += consensusWeight; - if (containsBandwidthWeights) { - double guardWeight = (double) relay.getBandwidth(); - double middleWeight = (double) relay.getBandwidth(); - double exitWeight = (double) relay.getBandwidth(); - boolean isExit = relay.getFlags().contains("Exit") && - !relay.getFlags().contains("BadExit"); - boolean isGuard = relay.getFlags().contains("Guard"); - if (isGuard && isExit) { - guardWeight *= wgd; - middleWeight *= wmd; - exitWeight *= wed; - } else if (isGuard) { - guardWeight *= wgg; - middleWeight *= wmg; - exitWeight = 0.0; - } else if (isExit) { - guardWeight = 0.0; - middleWeight *= wme; - exitWeight *= wee; - } else { - guardWeight = 0.0; - middleWeight *= wmm; - exitWeight = 0.0; - } - guardWeights.put(fingerprint, guardWeight); - middleWeights.put(fingerprint, middleWeight); - exitWeights.put(fingerprint, exitWeight); - totalGuardWeight += guardWeight; - totalMiddleWeight += middleWeight; - totalExitWeight += exitWeight; - } - } - } - SortedMap<String, double[]> pathSelectionProbabilities = - new TreeMap<String, double[]>(); - SortedSet<String> fingerprints = new TreeSet<String>(); - fingerprints.addAll(consensusWeights.keySet()); - fingerprints.addAll(advertisedBandwidths.keySet()); - for (String fingerprint : fingerprints) { - double[] probabilities = new double[] { -1.0, -1.0, -1.0, -1.0, - -1.0, -1.0, -1.0 }; - if (consensusWeights.containsKey(fingerprint) && - totalConsensusWeight > 0.0) { - probabilities[1] = consensusWeights.get(fingerprint) / - totalConsensusWeight; - probabilities[6] = consensusWeights.get(fingerprint); - } - if (guardWeights.containsKey(fingerprint) && - totalGuardWeight > 0.0) { - probabilities[2] = guardWeights.get(fingerprint) / - totalGuardWeight; - } - if (middleWeights.containsKey(fingerprint) && - totalMiddleWeight > 0.0) { - probabilities[3] = middleWeights.get(fingerprint) / - totalMiddleWeight; - } - if (exitWeights.containsKey(fingerprint) && - totalExitWeight > 0.0) { - probabilities[4] = exitWeights.get(fingerprint) / - totalExitWeight; - } - if (advertisedBandwidths.containsKey(fingerprint) && - totalAdvertisedBandwidth > 0.0) { - probabilities[0] = advertisedBandwidths.get(fingerprint) - / totalAdvertisedBandwidth; - probabilities[5] = advertisedBandwidths.get(fingerprint); - } - pathSelectionProbabilities.put(fingerprint, probabilities); - } - return pathSelectionProbabilities; - } - - private void addToHistory(String fingerprint, long validAfterMillis, - long freshUntilMillis, double[] weights) { - WeightsStatus weightsStatus = this.documentStore.retrieve( - WeightsStatus.class, true, fingerprint); - if (weightsStatus == null) { - weightsStatus = new WeightsStatus(); - } - SortedMap<long[], double[]> history = weightsStatus.getHistory(); - long[] interval = new long[] { validAfterMillis, freshUntilMillis }; - if ((history.headMap(interval).isEmpty() || - history.headMap(interval).lastKey()[1] <= validAfterMillis) && - (history.tailMap(interval).isEmpty() || - history.tailMap(interval).firstKey()[0] >= freshUntilMillis)) { - history.put(interval, weights); - this.compressHistory(weightsStatus); - this.documentStore.store(weightsStatus, fingerprint); - } - } - - private void compressHistory(WeightsStatus weightsStatus) { - SortedMap<long[], double[]> history = weightsStatus.getHistory(); - SortedMap<long[], double[]> compressedHistory = - new TreeMap<long[], double[]>(history.comparator()); - long lastStartMillis = 0L, lastEndMillis = 0L; - double[] lastWeights = null; - String lastMonthString = "1970-01"; - int lastMissingValues = -1; - for (Map.Entry<long[], double[]> e : history.entrySet()) { - long startMillis = e.getKey()[0], endMillis = e.getKey()[1]; - double[] weights = e.getValue(); - long intervalLengthMillis; - if (this.now - endMillis <= DateTimeHelper.ONE_WEEK) { - intervalLengthMillis = DateTimeHelper.ONE_HOUR; - } else if (this.now - endMillis <= - DateTimeHelper.ROUGHLY_ONE_MONTH) { - intervalLengthMillis = DateTimeHelper.FOUR_HOURS; - } else if (this.now - endMillis <= - DateTimeHelper.ROUGHLY_THREE_MONTHS) { - intervalLengthMillis = DateTimeHelper.TWELVE_HOURS; - } else if (this.now - endMillis <= - DateTimeHelper.ROUGHLY_ONE_YEAR) { - intervalLengthMillis = DateTimeHelper.TWO_DAYS; - } else { - intervalLengthMillis = DateTimeHelper.TEN_DAYS; - } - String monthString = DateTimeHelper.format(startMillis, - DateTimeHelper.ISO_YEARMONTH_FORMAT); - int missingValues = 0; - for (int i = 0; i < weights.length; i++) { - if (weights[i] < -0.5) { - missingValues += 1 << i; - } - } - if (lastEndMillis == startMillis && - ((lastEndMillis - 1L) / intervalLengthMillis) == - ((endMillis - 1L) / intervalLengthMillis) && - lastMonthString.equals(monthString) && - lastMissingValues == missingValues) { - double lastIntervalInHours = (double) ((lastEndMillis - - lastStartMillis) / DateTimeHelper.ONE_HOUR); - double currentIntervalInHours = (double) ((endMillis - - startMillis) / DateTimeHelper.ONE_HOUR); - double newIntervalInHours = (double) ((endMillis - - lastStartMillis) / DateTimeHelper.ONE_HOUR); - for (int i = 0; i < lastWeights.length; i++) { - lastWeights[i] *= lastIntervalInHours; - lastWeights[i] += weights[i] * currentIntervalInHours; - lastWeights[i] /= newIntervalInHours; - } - lastEndMillis = endMillis; - } else { - if (lastStartMillis > 0L) { - compressedHistory.put(new long[] { lastStartMillis, - lastEndMillis }, lastWeights); - } - lastStartMillis = startMillis; - lastEndMillis = endMillis; - lastWeights = weights; - } - lastMonthString = monthString; - lastMissingValues = missingValues; - } - if (lastStartMillis > 0L) { - compressedHistory.put(new long[] { lastStartMillis, lastEndMillis }, - lastWeights); - } - weightsStatus.setHistory(compressedHistory); - } - - public String getStatsString() { - /* TODO Add statistics string. */ - return null; - } -} - diff --git a/src/org/torproject/onionoo/cron/Main.java b/src/org/torproject/onionoo/cron/Main.java new file mode 100644 index 0000000..94dfa00 --- /dev/null +++ b/src/org/torproject/onionoo/cron/Main.java @@ -0,0 +1,140 @@ +/* Copyright 2011, 2012 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.cron; + +import java.io.File; + +import org.torproject.onionoo.docs.DocumentStore; +import org.torproject.onionoo.updater.BandwidthStatusUpdater; +import org.torproject.onionoo.updater.ClientsStatusUpdater; +import org.torproject.onionoo.updater.DescriptorSource; +import org.torproject.onionoo.updater.LookupService; +import org.torproject.onionoo.updater.NodeDetailsStatusUpdater; +import org.torproject.onionoo.updater.ReverseDomainNameResolver; +import org.torproject.onionoo.updater.StatusUpdater; +import org.torproject.onionoo.updater.UptimeStatusUpdater; +import org.torproject.onionoo.updater.WeightsStatusUpdater; +import org.torproject.onionoo.util.ApplicationFactory; +import org.torproject.onionoo.util.LockFile; +import org.torproject.onionoo.util.Logger; +import org.torproject.onionoo.writer.BandwidthDocumentWriter; +import org.torproject.onionoo.writer.ClientsDocumentWriter; +import org.torproject.onionoo.writer.DetailsDocumentWriter; +import org.torproject.onionoo.writer.DocumentWriter; +import org.torproject.onionoo.writer.SummaryDocumentWriter; +import org.torproject.onionoo.writer.UptimeDocumentWriter; +import org.torproject.onionoo.writer.WeightsDocumentWriter; + +/* Update search data and status data files. */ +public class Main { + + private Main() { + } + + public static void main(String[] args) { + + LockFile lf = new LockFile(); + Logger.setTime(); + Logger.printStatus("Initializing."); + if (lf.acquireLock()) { + Logger.printStatusTime("Acquired lock"); + } else { + Logger.printErrorTime("Could not acquire lock. Is Onionoo " + + "already running? Terminating"); + return; + } + + DescriptorSource dso = ApplicationFactory.getDescriptorSource(); + Logger.printStatusTime("Initialized descriptor source"); + DocumentStore ds = ApplicationFactory.getDocumentStore(); + Logger.printStatusTime("Initialized document store"); + LookupService ls = new LookupService(new File("geoip")); + Logger.printStatusTime("Initialized Geoip lookup service"); + ReverseDomainNameResolver rdnr = new ReverseDomainNameResolver(); + Logger.printStatusTime("Initialized reverse domain name resolver"); + NodeDetailsStatusUpdater ndsu = new NodeDetailsStatusUpdater(rdnr, + ls); + Logger.printStatusTime("Initialized node data writer"); + BandwidthStatusUpdater bsu = new BandwidthStatusUpdater(); + Logger.printStatusTime("Initialized bandwidth status updater"); + WeightsStatusUpdater wsu = new WeightsStatusUpdater(); + Logger.printStatusTime("Initialized weights status updater"); + ClientsStatusUpdater csu = new ClientsStatusUpdater(); + Logger.printStatusTime("Initialized clients status updater"); + UptimeStatusUpdater usu = new UptimeStatusUpdater(); + Logger.printStatusTime("Initialized uptime status updater"); + StatusUpdater[] sus = new StatusUpdater[] { ndsu, bsu, wsu, csu, + usu }; + + SummaryDocumentWriter sdw = new SummaryDocumentWriter(); + Logger.printStatusTime("Initialized summary document writer"); + DetailsDocumentWriter ddw = new DetailsDocumentWriter(); + Logger.printStatusTime("Initialized details document writer"); + BandwidthDocumentWriter bdw = new BandwidthDocumentWriter(); + Logger.printStatusTime("Initialized bandwidth document writer"); + WeightsDocumentWriter wdw = new WeightsDocumentWriter(); + Logger.printStatusTime("Initialized weights document writer"); + ClientsDocumentWriter cdw = new ClientsDocumentWriter(); + Logger.printStatusTime("Initialized clients document writer"); + UptimeDocumentWriter udw = new UptimeDocumentWriter(); + Logger.printStatusTime("Initialized uptime document writer"); + DocumentWriter[] dws = new DocumentWriter[] { sdw, ddw, bdw, wdw, cdw, + udw }; + + Logger.printStatus("Downloading descriptors."); + dso.downloadDescriptors(); + + Logger.printStatus("Reading descriptors."); + dso.readDescriptors(); + + Logger.printStatus("Updating internal status files."); + for (StatusUpdater su : sus) { + su.updateStatuses(); + Logger.printStatusTime(su.getClass().getSimpleName() + + " updated status files"); + } + + Logger.printStatus("Updating document files."); + for (DocumentWriter dw : dws) { + dw.writeDocuments(); + } + + Logger.printStatus("Shutting down."); + dso.writeHistoryFiles(); + Logger.printStatusTime("Wrote parse histories"); + ds.flushDocumentCache(); + Logger.printStatusTime("Flushed document cache"); + + Logger.printStatus("Gathering statistics."); + for (StatusUpdater su : sus) { + String statsString = su.getStatsString(); + if (statsString != null) { + Logger.printStatistics(su.getClass().getSimpleName(), + statsString); + } + } + for (DocumentWriter dw : dws) { + String statsString = dw.getStatsString(); + if (statsString != null) { + Logger.printStatistics(dw.getClass().getSimpleName(), + statsString); + } + } + Logger.printStatistics("Descriptor source", dso.getStatsString()); + Logger.printStatistics("Document store", ds.getStatsString()); + Logger.printStatistics("GeoIP lookup service", ls.getStatsString()); + Logger.printStatistics("Reverse domain name resolver", + rdnr.getStatsString()); + + Logger.printStatus("Releasing lock."); + if (lf.releaseLock()) { + Logger.printStatusTime("Released lock"); + } else { + Logger.printErrorTime("Could not release lock. The next " + + "execution may not start as expected"); + } + + Logger.printStatus("Terminating."); + } +} + diff --git a/src/org/torproject/onionoo/docs/BandwidthDocument.java b/src/org/torproject/onionoo/docs/BandwidthDocument.java new file mode 100644 index 0000000..ea20a5e --- /dev/null +++ b/src/org/torproject/onionoo/docs/BandwidthDocument.java @@ -0,0 +1,27 @@ +/* Copyright 2013 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.docs; + +import java.util.Map; + +public class BandwidthDocument extends Document { + + @SuppressWarnings("unused") + private String fingerprint; + public void setFingerprint(String fingerprint) { + this.fingerprint = fingerprint; + } + + @SuppressWarnings("unused") + private Map<String, GraphHistory> write_history; + public void setWriteHistory(Map<String, GraphHistory> writeHistory) { + this.write_history = writeHistory; + } + + @SuppressWarnings("unused") + private Map<String, GraphHistory> read_history; + public void setReadHistory(Map<String, GraphHistory> readHistory) { + this.read_history = readHistory; + } +} + diff --git a/src/org/torproject/onionoo/docs/BandwidthStatus.java b/src/org/torproject/onionoo/docs/BandwidthStatus.java new file mode 100644 index 0000000..a2980e5 --- /dev/null +++ b/src/org/torproject/onionoo/docs/BandwidthStatus.java @@ -0,0 +1,80 @@ +/* Copyright 2013--2014 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.docs; + +import java.util.Scanner; +import java.util.SortedMap; +import java.util.TreeMap; + +import org.torproject.onionoo.util.DateTimeHelper; + +public class BandwidthStatus extends Document { + + private SortedMap<Long, long[]> writeHistory = + new TreeMap<Long, long[]>(); + public void setWriteHistory(SortedMap<Long, long[]> writeHistory) { + this.writeHistory = writeHistory; + } + public SortedMap<Long, long[]> getWriteHistory() { + return this.writeHistory; + } + + private SortedMap<Long, long[]> readHistory = + new TreeMap<Long, long[]>(); + public void setReadHistory(SortedMap<Long, long[]> readHistory) { + this.readHistory = readHistory; + } + public SortedMap<Long, long[]> getReadHistory() { + return this.readHistory; + } + + public void fromDocumentString(String documentString) { + Scanner s = new Scanner(documentString); + while (s.hasNextLine()) { + String line = s.nextLine(); + String[] parts = line.split(" "); + if (parts.length != 6) { + System.err.println("Illegal line '" + line + "' in bandwidth " + + "history. Skipping this line."); + continue; + } + SortedMap<Long, long[]> history = parts[0].equals("r") + ? readHistory : writeHistory; + long startMillis = DateTimeHelper.parse(parts[1] + " " + parts[2]); + long endMillis = DateTimeHelper.parse(parts[3] + " " + parts[4]); + if (startMillis < 0L || endMillis < 0L) { + System.err.println("Could not parse timestamp while reading " + + "bandwidth history. Skipping."); + break; + } + long bandwidth = Long.parseLong(parts[5]); + long previousEndMillis = history.headMap(startMillis).isEmpty() + ? startMillis + : history.get(history.headMap(startMillis).lastKey())[1]; + long nextStartMillis = history.tailMap(startMillis).isEmpty() + ? endMillis : history.tailMap(startMillis).firstKey(); + if (previousEndMillis <= startMillis && + nextStartMillis >= endMillis) { + history.put(startMillis, new long[] { startMillis, endMillis, + bandwidth }); + } + } + s.close(); + } + + public String toDocumentString() { + StringBuilder sb = new StringBuilder(); + for (long[] v : writeHistory.values()) { + sb.append("w " + DateTimeHelper.format(v[0]) + " " + + DateTimeHelper.format(v[1]) + " " + String.valueOf(v[2]) + + "\n"); + } + for (long[] v : readHistory.values()) { + sb.append("r " + DateTimeHelper.format(v[0]) + " " + + DateTimeHelper.format(v[1]) + " " + String.valueOf(v[2]) + + "\n"); + } + return sb.toString(); + } +} + diff --git a/src/org/torproject/onionoo/docs/ClientsDocument.java b/src/org/torproject/onionoo/docs/ClientsDocument.java new file mode 100644 index 0000000..27b1588 --- /dev/null +++ b/src/org/torproject/onionoo/docs/ClientsDocument.java @@ -0,0 +1,22 @@ +/* Copyright 2014 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.docs; + +import java.util.Map; + +public class ClientsDocument extends Document { + + @SuppressWarnings("unused") + private String fingerprint; + public void setFingerprint(String fingerprint) { + this.fingerprint = fingerprint; + } + + @SuppressWarnings("unused") + private Map<String, ClientsGraphHistory> average_clients; + public void setAverageClients( + Map<String, ClientsGraphHistory> averageClients) { + this.average_clients = averageClients; + } +} + diff --git a/src/org/torproject/onionoo/docs/ClientsGraphHistory.java b/src/org/torproject/onionoo/docs/ClientsGraphHistory.java new file mode 100644 index 0000000..e1db663 --- /dev/null +++ b/src/org/torproject/onionoo/docs/ClientsGraphHistory.java @@ -0,0 +1,83 @@ +/* Copyright 2014 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.docs; + +import java.util.ArrayList; +import java.util.List; +import java.util.SortedMap; + +public class ClientsGraphHistory { + + private String first; + public void setFirst(String first) { + this.first = first; + } + public String getFirst() { + return this.first; + } + + private String last; + public void setLast(String last) { + this.last = last; + } + public String getLast() { + return this.last; + } + + private Integer interval; + public void setInterval(Integer interval) { + this.interval = interval; + } + public Integer getInterval() { + return this.interval; + } + + private Double factor; + public void setFactor(Double factor) { + this.factor = factor; + } + public Double getFactor() { + return this.factor; + } + + private Integer count; + public void setCount(Integer count) { + this.count = count; + } + public Integer getCount() { + return this.count; + } + + private List<Integer> values = new ArrayList<Integer>(); + public void setValues(List<Integer> values) { + this.values = values; + } + public List<Integer> getValues() { + return this.values; + } + + private SortedMap<String, Float> countries; + public void setCountries(SortedMap<String, Float> countries) { + this.countries = countries; + } + public SortedMap<String, Float> getCountries() { + return this.countries; + } + + private SortedMap<String, Float> transports; + public void setTransports(SortedMap<String, Float> transports) { + this.transports = transports; + } + public SortedMap<String, Float> getTransports() { + return this.transports; + } + + private SortedMap<String, Float> versions; + public void setVersions(SortedMap<String, Float> versions) { + this.versions = versions; + } + public SortedMap<String, Float> getVersions() { + return this.versions; + } +} + diff --git a/src/org/torproject/onionoo/docs/ClientsHistory.java b/src/org/torproject/onionoo/docs/ClientsHistory.java new file mode 100644 index 0000000..446dd10 --- /dev/null +++ b/src/org/torproject/onionoo/docs/ClientsHistory.java @@ -0,0 +1,174 @@ +/* Copyright 2014 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.docs; + +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +import org.torproject.onionoo.util.DateTimeHelper; + +public class ClientsHistory implements Comparable<ClientsHistory> { + + private long startMillis; + public long getStartMillis() { + return this.startMillis; + } + + private long endMillis; + public long getEndMillis() { + return this.endMillis; + } + + private double totalResponses; + public double getTotalResponses() { + return this.totalResponses; + } + + private SortedMap<String, Double> responsesByCountry; + public SortedMap<String, Double> getResponsesByCountry() { + return this.responsesByCountry; + } + + private SortedMap<String, Double> responsesByTransport; + public SortedMap<String, Double> getResponsesByTransport() { + return this.responsesByTransport; + } + + private SortedMap<String, Double> responsesByVersion; + public SortedMap<String, Double> getResponsesByVersion() { + return this.responsesByVersion; + } + + public ClientsHistory(long startMillis, long endMillis, + double totalResponses, + SortedMap<String, Double> responsesByCountry, + SortedMap<String, Double> responsesByTransport, + SortedMap<String, Double> responsesByVersion) { + this.startMillis = startMillis; + this.endMillis = endMillis; + this.totalResponses = totalResponses; + this.responsesByCountry = responsesByCountry; + this.responsesByTransport = responsesByTransport; + this.responsesByVersion = responsesByVersion; + } + + public static ClientsHistory fromString( + String responseHistoryString) { + String[] parts = responseHistoryString.split(" ", 8); + if (parts.length != 8) { + return null; + } + long startMillis = DateTimeHelper.parse(parts[0] + " " + parts[1]); + long endMillis = DateTimeHelper.parse(parts[2] + " " + parts[3]); + if (startMillis < 0L || endMillis < 0L) { + return null; + } + if (startMillis >= endMillis) { + return null; + } + double totalResponses = 0.0; + try { + totalResponses = Double.parseDouble(parts[4]); + } catch (NumberFormatException e) { + return null; + } + SortedMap<String, Double> responsesByCountry = + parseResponses(parts[5]); + SortedMap<String, Double> responsesByTransport = + parseResponses(parts[6]); + SortedMap<String, Double> responsesByVersion = + parseResponses(parts[7]); + if (responsesByCountry == null || responsesByTransport == null || + responsesByVersion == null) { + return null; + } + return new ClientsHistory(startMillis, endMillis, totalResponses, + responsesByCountry, responsesByTransport, responsesByVersion); + } + + private static SortedMap<String, Double> parseResponses( + String responsesString) { + SortedMap<String, Double> responses = new TreeMap<String, Double>(); + if (responsesString.length() > 0) { + for (String pair : responsesString.split(",")) { + String[] keyValue = pair.split("="); + if (keyValue.length != 2) { + return null; + } + double value = 0.0; + try { + value = Double.parseDouble(keyValue[1]); + } catch (NumberFormatException e) { + return null; + } + responses.put(keyValue[0], value); + } + } + return responses; + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(DateTimeHelper.format(startMillis)); + sb.append(" " + DateTimeHelper.format(endMillis)); + sb.append(" " + String.format("%.3f", this.totalResponses)); + this.appendResponses(sb, this.responsesByCountry); + this.appendResponses(sb, this.responsesByTransport); + this.appendResponses(sb, this.responsesByVersion); + return sb.toString(); + } + + private void appendResponses(StringBuilder sb, + SortedMap<String, Double> responses) { + sb.append(" "); + int written = 0; + for (Map.Entry<String, Double> e : responses.entrySet()) { + sb.append((written++ > 0 ? "," : "") + e.getKey() + "=" + + String.format("%.3f", e.getValue())); + } + } + + public void addResponses(ClientsHistory other) { + this.totalResponses += other.totalResponses; + this.addResponsesByCategory(this.responsesByCountry, + other.responsesByCountry); + this.addResponsesByCategory(this.responsesByTransport, + other.responsesByTransport); + this.addResponsesByCategory(this.responsesByVersion, + other.responsesByVersion); + if (this.startMillis > other.startMillis) { + this.startMillis = other.startMillis; + } + if (this.endMillis < other.endMillis) { + this.endMillis = other.endMillis; + } + } + + private void addResponsesByCategory( + SortedMap<String, Double> thisResponses, + SortedMap<String, Double> otherResponses) { + for (Map.Entry<String, Double> e : otherResponses.entrySet()) { + if (thisResponses.containsKey(e.getKey())) { + thisResponses.put(e.getKey(), thisResponses.get(e.getKey()) + + e.getValue()); + } else { + thisResponses.put(e.getKey(), e.getValue()); + } + } + } + + public int compareTo(ClientsHistory other) { + return this.startMillis < other.startMillis ? -1 : + this.startMillis > other.startMillis ? 1 : 0; + } + + public boolean equals(Object other) { + return other instanceof ClientsHistory && + this.startMillis == ((ClientsHistory) other).startMillis; + } + + public int hashCode() { + return (int) this.startMillis; + } +} \ No newline at end of file diff --git a/src/org/torproject/onionoo/docs/ClientsStatus.java b/src/org/torproject/onionoo/docs/ClientsStatus.java new file mode 100644 index 0000000..2bd2168 --- /dev/null +++ b/src/org/torproject/onionoo/docs/ClientsStatus.java @@ -0,0 +1,43 @@ +/* Copyright 2014 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.docs; + +import java.util.Scanner; +import java.util.SortedSet; +import java.util.TreeSet; + +public class ClientsStatus extends Document { + + private SortedSet<ClientsHistory> history = + new TreeSet<ClientsHistory>(); + public void setHistory(SortedSet<ClientsHistory> history) { + this.history = history; + } + public SortedSet<ClientsHistory> getHistory() { + return this.history; + } + + public void fromDocumentString(String documentString) { + Scanner s = new Scanner(documentString); + while (s.hasNextLine()) { + String line = s.nextLine(); + ClientsHistory parsedLine = ClientsHistory.fromString(line); + if (parsedLine != null) { + this.history.add(parsedLine); + } else { + System.err.println("Could not parse clients history line '" + + line + "'. Skipping."); + } + } + s.close(); + } + + public String toDocumentString() { + StringBuilder sb = new StringBuilder(); + for (ClientsHistory interval : this.history) { + sb.append(interval.toString() + "\n"); + } + return sb.toString(); + } +} + diff --git a/src/org/torproject/onionoo/docs/DetailsDocument.java b/src/org/torproject/onionoo/docs/DetailsDocument.java new file mode 100644 index 0000000..142b591 --- /dev/null +++ b/src/org/torproject/onionoo/docs/DetailsDocument.java @@ -0,0 +1,365 @@ +/* Copyright 2013--2014 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.docs; + +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang.StringEscapeUtils; + +public class DetailsDocument extends Document { + + /* We must ensure that details files only contain ASCII characters + * and no UTF-8 characters. While UTF-8 characters are perfectly + * valid in JSON, this would break compatibility with existing files + * pretty badly. We do this by escaping non-ASCII characters, e.g., + * \u00F2. Gson won't treat this as UTF-8, but will think that we want + * to write six characters '\', 'u', '0', '0', 'F', '2'. The only thing + * we'll have to do is to change back the '\\' that Gson writes for the + * '\'. */ + private static String escapeJSON(String s) { + return s == null ? null : + StringEscapeUtils.escapeJavaScript(s).replaceAll("\\\\'", "'"); + } + private static String unescapeJSON(String s) { + return s == null ? null : + StringEscapeUtils.unescapeJavaScript(s.replaceAll("'", "\\'")); + } + + private String nickname; + public void setNickname(String nickname) { + this.nickname = nickname; + } + public String getNickname() { + return this.nickname; + } + + private String fingerprint; + public void setFingerprint(String fingerprint) { + this.fingerprint = fingerprint; + } + public String getFingerprint() { + return this.fingerprint; + } + + private String hashed_fingerprint; + public void setHashedFingerprint(String hashedFingerprint) { + this.hashed_fingerprint = hashedFingerprint; + } + public String getHashedFingerprint() { + return this.hashed_fingerprint; + } + + private List<String> or_addresses; + public void setOrAddresses(List<String> orAddresses) { + this.or_addresses = orAddresses; + } + public List<String> getOrAddresses() { + return this.or_addresses; + } + + private List<String> exit_addresses; + public void setExitAddresses(List<String> exitAddresses) { + this.exit_addresses = exitAddresses; + } + public List<String> getExitAddresses() { + return this.exit_addresses; + } + + private String dir_address; + public void setDirAddress(String dirAddress) { + this.dir_address = dirAddress; + } + public String getDirAddress() { + return this.dir_address; + } + + private String last_seen; + public void setLastSeen(String lastSeen) { + this.last_seen = lastSeen; + } + public String getLastSeen() { + return this.last_seen; + } + + private String last_changed_address_or_port; + public void setLastChangedAddressOrPort( + String lastChangedAddressOrPort) { + this.last_changed_address_or_port = lastChangedAddressOrPort; + } + public String getLastChangedAddressOrPort() { + return this.last_changed_address_or_port; + } + + private String first_seen; + public void setFirstSeen(String firstSeen) { + this.first_seen = firstSeen; + } + public String getFirstSeen() { + return this.first_seen; + } + + private Boolean running; + public void setRunning(Boolean running) { + this.running = running; + } + public Boolean getRunning() { + return this.running; + } + + private List<String> flags; + public void setFlags(List<String> flags) { + this.flags = flags; + } + public List<String> getFlags() { + return this.flags; + } + + private String country; + public void setCountry(String country) { + this.country = country; + } + public String getCountry() { + return this.country; + } + + private String country_name; + public void setCountryName(String countryName) { + this.country_name = escapeJSON(countryName); + } + public String getCountryName() { + return unescapeJSON(this.country_name); + } + + private String region_name; + public void setRegionName(String regionName) { + this.region_name = escapeJSON(regionName); + } + public String getRegionName() { + return unescapeJSON(this.region_name); + } + + private String city_name; + public void setCityName(String cityName) { + this.city_name = escapeJSON(cityName); + } + public String getCityName() { + return unescapeJSON(this.city_name); + } + + private Float latitude; + public void setLatitude(Float latitude) { + this.latitude = latitude; + } + public Float getLatitude() { + return this.latitude; + } + + private Float longitude; + public void setLongitude(Float longitude) { + this.longitude = longitude; + } + public Float getLongitude() { + return this.longitude; + } + + private String as_number; + public void setAsNumber(String asNumber) { + this.as_number = escapeJSON(asNumber); + } + public String getAsNumber() { + return unescapeJSON(this.as_number); + } + + private String as_name; + public void setAsName(String asName) { + this.as_name = escapeJSON(asName); + } + public String getAsName() { + return unescapeJSON(this.as_name); + } + + private Long consensus_weight; + public void setConsensusWeight(Long consensusWeight) { + this.consensus_weight = consensusWeight; + } + public Long getConsensusWeight() { + return this.consensus_weight; + } + + private String host_name; + public void setHostName(String hostName) { + this.host_name = escapeJSON(hostName); + } + public String getHostName() { + return unescapeJSON(this.host_name); + } + + private String last_restarted; + public void setLastRestarted(String lastRestarted) { + this.last_restarted = lastRestarted; + } + public String getLastRestarted() { + return this.last_restarted; + } + + private Integer bandwidth_rate; + public void setBandwidthRate(Integer bandwidthRate) { + this.bandwidth_rate = bandwidthRate; + } + public Integer getBandwidthRate() { + return this.bandwidth_rate; + } + + private Integer bandwidth_burst; + public void setBandwidthBurst(Integer bandwidthBurst) { + this.bandwidth_burst = bandwidthBurst; + } + public Integer getBandwidthBurst() { + return this.bandwidth_burst; + } + + private Integer observed_bandwidth; + public void setObservedBandwidth(Integer observedBandwidth) { + this.observed_bandwidth = observedBandwidth; + } + public Integer getObservedBandwidth() { + return this.observed_bandwidth; + } + + private Integer advertised_bandwidth; + public void setAdvertisedBandwidth(Integer advertisedBandwidth) { + this.advertised_bandwidth = advertisedBandwidth; + } + public Integer getAdvertisedBandwidth() { + return this.advertised_bandwidth; + } + + private List<String> exit_policy; + public void setExitPolicy(List<String> exitPolicy) { + this.exit_policy = exitPolicy; + } + public List<String> getExitPolicy() { + return this.exit_policy; + } + + private Map<String, List<String>> exit_policy_summary; + public void setExitPolicySummary( + Map<String, List<String>> exitPolicySummary) { + this.exit_policy_summary = exitPolicySummary; + } + public Map<String, List<String>> getExitPolicySummary() { + return this.exit_policy_summary; + } + + private Map<String, List<String>> exit_policy_v6_summary; + public void setExitPolicyV6Summary( + Map<String, List<String>> exitPolicyV6Summary) { + this.exit_policy_v6_summary = exitPolicyV6Summary; + } + public Map<String, List<String>> getExitPolicyV6Summary() { + return this.exit_policy_v6_summary; + } + + private String contact; + public void setContact(String contact) { + this.contact = escapeJSON(contact); + } + public String getContact() { + return unescapeJSON(this.contact); + } + + private String platform; + public void setPlatform(String platform) { + this.platform = escapeJSON(platform); + } + public String getPlatform() { + return unescapeJSON(this.platform); + } + + private List<String> family; + public void setFamily(List<String> family) { + this.family = family; + } + public List<String> getFamily() { + return this.family; + } + + private Float advertised_bandwidth_fraction; + public void setAdvertisedBandwidthFraction( + Float advertisedBandwidthFraction) { + if (advertisedBandwidthFraction == null || + advertisedBandwidthFraction >= 0.0) { + this.advertised_bandwidth_fraction = advertisedBandwidthFraction; + } + } + public Float getAdvertisedBandwidthFraction() { + return this.advertised_bandwidth_fraction; + } + + private Float consensus_weight_fraction; + public void setConsensusWeightFraction(Float consensusWeightFraction) { + if (consensusWeightFraction == null || + consensusWeightFraction >= 0.0) { + this.consensus_weight_fraction = consensusWeightFraction; + } + } + public Float getConsensusWeightFraction() { + return this.consensus_weight_fraction; + } + + private Float guard_probability; + public void setGuardProbability(Float guardProbability) { + if (guardProbability == null || guardProbability >= 0.0) { + this.guard_probability = guardProbability; + } + } + public Float getGuardProbability() { + return this.guard_probability; + } + + private Float middle_probability; + public void setMiddleProbability(Float middleProbability) { + if (middleProbability == null || middleProbability >= 0.0) { + this.middle_probability = middleProbability; + } + } + public Float getMiddleProbability() { + return this.middle_probability; + } + + private Float exit_probability; + public void setExitProbability(Float exitProbability) { + if (exitProbability == null || exitProbability >= 0.0) { + this.exit_probability = exitProbability; + } + } + public Float getExitProbability() { + return this.exit_probability; + } + + private Boolean recommended_version; + public void setRecommendedVersion(Boolean recommendedVersion) { + this.recommended_version = recommendedVersion; + } + public Boolean getRecommendedVersion() { + return this.recommended_version; + } + + private Boolean hibernating; + public void setHibernating(Boolean hibernating) { + this.hibernating = hibernating; + } + public Boolean getHibernating() { + return this.hibernating; + } + + private String pool_assignment; + public void setPoolAssignment(String poolAssignment) { + this.pool_assignment = poolAssignment; + } + public String getPoolAssignment() { + return this.pool_assignment; + } +} + diff --git a/src/org/torproject/onionoo/docs/DetailsStatus.java b/src/org/torproject/onionoo/docs/DetailsStatus.java new file mode 100644 index 0000000..a19b4b9 --- /dev/null +++ b/src/org/torproject/onionoo/docs/DetailsStatus.java @@ -0,0 +1,141 @@ +/* Copyright 2013--2014 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.docs; + +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang.StringEscapeUtils; + +public class DetailsStatus extends Document { + + /* We must ensure that details files only contain ASCII characters + * and no UTF-8 characters. While UTF-8 characters are perfectly + * valid in JSON, this would break compatibility with existing files + * pretty badly. We do this by escaping non-ASCII characters, e.g., + * \u00F2. Gson won't treat this as UTF-8, but will think that we want + * to write six characters '\', 'u', '0', '0', 'F', '2'. The only thing + * we'll have to do is to change back the '\\' that Gson writes for the + * '\'. */ + private static String escapeJSON(String s) { + return s == null ? null : + StringEscapeUtils.escapeJavaScript(s).replaceAll("\\\\'", "'"); + } + private static String unescapeJSON(String s) { + return s == null ? null : + StringEscapeUtils.unescapeJavaScript(s.replaceAll("'", "\\'")); + } + + private String desc_published; + public void setDescPublished(String descPublished) { + this.desc_published = descPublished; + } + public String getDescPublished() { + return this.desc_published; + } + + private String last_restarted; + public void setLastRestarted(String lastRestarted) { + this.last_restarted = lastRestarted; + } + public String getLastRestarted() { + return this.last_restarted; + } + + private Integer bandwidth_rate; + public void setBandwidthRate(Integer bandwidthRate) { + this.bandwidth_rate = bandwidthRate; + } + public Integer getBandwidthRate() { + return this.bandwidth_rate; + } + + private Integer bandwidth_burst; + public void setBandwidthBurst(Integer bandwidthBurst) { + this.bandwidth_burst = bandwidthBurst; + } + public Integer getBandwidthBurst() { + return this.bandwidth_burst; + } + + private Integer observed_bandwidth; + public void setObservedBandwidth(Integer observedBandwidth) { + this.observed_bandwidth = observedBandwidth; + } + public Integer getObservedBandwidth() { + return this.observed_bandwidth; + } + + private Integer advertised_bandwidth; + public void setAdvertisedBandwidth(Integer advertisedBandwidth) { + this.advertised_bandwidth = advertisedBandwidth; + } + public Integer getAdvertisedBandwidth() { + return this.advertised_bandwidth; + } + + private List<String> exit_policy; + public void setExitPolicy(List<String> exitPolicy) { + this.exit_policy = exitPolicy; + } + public List<String> getExitPolicy() { + return this.exit_policy; + } + + private String contact; + public void setContact(String contact) { + this.contact = escapeJSON(contact); + } + public String getContact() { + return unescapeJSON(this.contact); + } + + private String platform; + public void setPlatform(String platform) { + this.platform = escapeJSON(platform); + } + public String getPlatform() { + return unescapeJSON(this.platform); + } + + private List<String> family; + public void setFamily(List<String> family) { + this.family = family; + } + public List<String> getFamily() { + return this.family; + } + + private Map<String, List<String>> exit_policy_v6_summary; + public void setExitPolicyV6Summary( + Map<String, List<String>> exitPolicyV6Summary) { + this.exit_policy_v6_summary = exitPolicyV6Summary; + } + public Map<String, List<String>> getExitPolicyV6Summary() { + return this.exit_policy_v6_summary; + } + + private Boolean hibernating; + public void setHibernating(Boolean hibernating) { + this.hibernating = hibernating; + } + public Boolean getHibernating() { + return this.hibernating; + } + + private String pool_assignment; + public void setPoolAssignment(String poolAssignment) { + this.pool_assignment = poolAssignment; + } + public String getPoolAssignment() { + return this.pool_assignment; + } + + private Map<String, Long> exit_addresses; + public void setExitAddresses(Map<String, Long> exitAddresses) { + this.exit_addresses = exitAddresses; + } + public Map<String, Long> getExitAddresses() { + return this.exit_addresses; + } +} diff --git a/src/org/torproject/onionoo/docs/Document.java b/src/org/torproject/onionoo/docs/Document.java new file mode 100644 index 0000000..a581795 --- /dev/null +++ b/src/org/torproject/onionoo/docs/Document.java @@ -0,0 +1,24 @@ +/* Copyright 2013 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.docs; + +public abstract class Document { + + private transient String documentString; + public void setDocumentString(String documentString) { + this.documentString = documentString; + } + public String getDocumentString() { + return this.documentString; + } + + public void fromDocumentString(String documentString) { + /* Subclasses may override this method to parse documentString. */ + } + + public String toDocumentString() { + /* Subclasses may override this method to write documentString. */ + return null; + } +} + diff --git a/src/org/torproject/onionoo/docs/DocumentStore.java b/src/org/torproject/onionoo/docs/DocumentStore.java new file mode 100644 index 0000000..c4fe965 --- /dev/null +++ b/src/org/torproject/onionoo/docs/DocumentStore.java @@ -0,0 +1,748 @@ +/* Copyright 2013 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.docs; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.Stack; +import java.util.TreeMap; +import java.util.TreeSet; + +import org.torproject.onionoo.util.ApplicationFactory; +import org.torproject.onionoo.util.Logger; +import org.torproject.onionoo.util.Time; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; + +// TODO For later migration from disk to database, do the following: +// - read from database and then from disk if not found +// - write only to database, delete from disk once in database +// - move entirely to database once disk is "empty" +// TODO Also look into simple key-value stores instead of real databases. +public class DocumentStore { + + private final File statusDir = new File("status"); + + private File outDir = new File("out"); + public void setOutDir(File outDir) { + this.outDir = outDir; + } + + private Time time; + + public DocumentStore() { + this.time = ApplicationFactory.getTime(); + } + + private long listOperations = 0L, listedFiles = 0L, storedFiles = 0L, + storedBytes = 0L, retrievedFiles = 0L, retrievedBytes = 0L, + removedFiles = 0L; + + /* Node statuses and summary documents are cached in memory, as opposed + * to all other document types. These caches are initialized when first + * accessing or modifying a NodeStatus or SummaryDocument document, + * respectively. */ + private SortedMap<String, NodeStatus> cachedNodeStatuses; + private SortedMap<String, SummaryDocument> cachedSummaryDocuments; + + public <T extends Document> SortedSet<String> list( + Class<T> documentType) { + if (documentType.equals(NodeStatus.class)) { + return this.listNodeStatuses(); + } else if (documentType.equals(SummaryDocument.class)) { + return this.listSummaryDocuments(); + } else { + return this.listDocumentFiles(documentType); + } + } + + private SortedSet<String> listNodeStatuses() { + if (this.cachedNodeStatuses == null) { + this.cacheNodeStatuses(); + } + return new TreeSet<String>(this.cachedNodeStatuses.keySet()); + } + + private void cacheNodeStatuses() { + SortedMap<String, NodeStatus> parsedNodeStatuses = + new TreeMap<String, NodeStatus>(); + File directory = this.statusDir; + if (directory != null) { + File summaryFile = new File(directory, "summary"); + if (summaryFile.exists()) { + try { + BufferedReader br = new BufferedReader(new FileReader( + summaryFile)); + String line; + while ((line = br.readLine()) != null) { + if (line.length() == 0) { + continue; + } + NodeStatus node = NodeStatus.fromString(line); + if (node != null) { + parsedNodeStatuses.put(node.getFingerprint(), node); + } + } + br.close(); + this.listedFiles += parsedNodeStatuses.size(); + this.listOperations++; + } catch (IOException e) { + System.err.println("Could not read file '" + + summaryFile.getAbsolutePath() + "'."); + e.printStackTrace(); + } + } + } + this.cachedNodeStatuses = parsedNodeStatuses; + } + + private SortedSet<String> listSummaryDocuments() { + if (this.cachedSummaryDocuments == null) { + this.cacheSummaryDocuments(); + } + return new TreeSet<String>(this.cachedSummaryDocuments.keySet()); + } + + private void cacheSummaryDocuments() { + SortedMap<String, SummaryDocument> parsedSummaryDocuments = + new TreeMap<String, SummaryDocument>(); + File directory = this.outDir; + if (directory != null) { + File summaryFile = new File(directory, "summary"); + if (summaryFile.exists()) { + String line = null; + try { + Gson gson = new Gson(); + BufferedReader br = new BufferedReader(new FileReader( + summaryFile)); + while ((line = br.readLine()) != null) { + if (line.length() == 0) { + continue; + } + SummaryDocument summaryDocument = gson.fromJson(line, + SummaryDocument.class); + if (summaryDocument != null) { + parsedSummaryDocuments.put(summaryDocument.getFingerprint(), + summaryDocument); + } + } + br.close(); + this.listedFiles += parsedSummaryDocuments.size(); + this.listOperations++; + } catch (IOException e) { + System.err.println("Could not read file '" + + summaryFile.getAbsolutePath() + "'."); + e.printStackTrace(); + } catch (JsonParseException e) { + System.err.println("Could not parse summary document '" + line + + "' in file '" + summaryFile.getAbsolutePath() + "'."); + e.printStackTrace(); + } + } + } + this.cachedSummaryDocuments = parsedSummaryDocuments; + } + + private <T extends Document> SortedSet<String> listDocumentFiles( + Class<T> documentType) { + SortedSet<String> fingerprints = new TreeSet<String>(); + File directory = null; + String subdirectory = null; + if (documentType.equals(DetailsStatus.class)) { + directory = this.statusDir; + subdirectory = "details"; + } else if (documentType.equals(BandwidthStatus.class)) { + directory = this.statusDir; + subdirectory = "bandwidth"; + } else if (documentType.equals(WeightsStatus.class)) { + directory = this.statusDir; + subdirectory = "weights"; + } else if (documentType.equals(ClientsStatus.class)) { + directory = this.statusDir; + subdirectory = "clients"; + } else if (documentType.equals(UptimeStatus.class)) { + directory = this.statusDir; + subdirectory = "uptimes"; + } else if (documentType.equals(DetailsDocument.class)) { + directory = this.outDir; + subdirectory = "details"; + } else if (documentType.equals(BandwidthDocument.class)) { + directory = this.outDir; + subdirectory = "bandwidth"; + } else if (documentType.equals(WeightsDocument.class)) { + directory = this.outDir; + subdirectory = "weights"; + } else if (documentType.equals(ClientsDocument.class)) { + directory = this.outDir; + subdirectory = "clients"; + } else if (documentType.equals(UptimeDocument.class)) { + directory = this.outDir; + subdirectory = "uptimes"; + } + if (directory != null && subdirectory != null) { + Stack<File> files = new Stack<File>(); + files.add(new File(directory, subdirectory)); + while (!files.isEmpty()) { + File file = files.pop(); + if (file.isDirectory()) { + files.addAll(Arrays.asList(file.listFiles())); + } else if (file.getName().length() == 40) { + fingerprints.add(file.getName()); + } + } + } + this.listOperations++; + this.listedFiles += fingerprints.size(); + return fingerprints; + } + + public <T extends Document> boolean store(T document) { + return this.store(document, null); + } + + public <T extends Document> boolean store(T document, + String fingerprint) { + if (document instanceof NodeStatus) { + return this.storeNodeStatus((NodeStatus) document, fingerprint); + } else if (document instanceof SummaryDocument) { + return this.storeSummaryDocument((SummaryDocument) document, + fingerprint); + } else { + return this.storeDocumentFile(document, fingerprint); + } + } + + private <T extends Document> boolean storeNodeStatus( + NodeStatus nodeStatus, String fingerprint) { + if (this.cachedNodeStatuses == null) { + this.cacheNodeStatuses(); + } + this.cachedNodeStatuses.put(fingerprint, nodeStatus); + return true; + } + + private <T extends Document> boolean storeSummaryDocument( + SummaryDocument summaryDocument, String fingerprint) { + if (this.cachedSummaryDocuments == null) { + this.cacheSummaryDocuments(); + } + this.cachedSummaryDocuments.put(fingerprint, summaryDocument); + return true; + } + + private <T extends Document> boolean storeDocumentFile(T document, + String fingerprint) { + File documentFile = this.getDocumentFile(document.getClass(), + fingerprint); + if (documentFile == null) { + return false; + } + String documentString; + if (document.getDocumentString() != null) { + documentString = document.getDocumentString(); + } else if (document instanceof BandwidthDocument || + document instanceof WeightsDocument || + document instanceof ClientsDocument || + document instanceof UptimeDocument) { + Gson gson = new Gson(); + documentString = gson.toJson(document); + } else if (document instanceof DetailsStatus || + document instanceof DetailsDocument) { + /* Don't escape HTML characters, like < and >, contained in + * strings. */ + Gson gson = new GsonBuilder().disableHtmlEscaping().create(); + /* We must ensure that details files only contain ASCII characters + * and no UTF-8 characters. While UTF-8 characters are perfectly + * valid in JSON, this would break compatibility with existing files + * pretty badly. We already make sure that all strings in details + * objects are escaped JSON, e.g., \u00F2. When Gson serlializes + * this string, it escapes the \ to \\, hence writes \\u00F2. We + * need to undo this and change \\u00F2 back to \u00F2. */ + documentString = gson.toJson(document).replaceAll("\\\\\\\\u", + "\\\\u"); + /* Existing details statuses don't contain opening and closing curly + * brackets, so we should remove them from new details statuses, + * too. */ + if (document instanceof DetailsStatus) { + documentString = documentString.substring( + documentString.indexOf("{") + 1, + documentString.lastIndexOf("}")); + } + } else if (document instanceof BandwidthStatus || + document instanceof WeightsStatus || + document instanceof ClientsStatus || + document instanceof UptimeStatus) { + documentString = document.toDocumentString(); + } else { + System.err.println("Serializing is not supported for type " + + document.getClass().getName() + "."); + return false; + } + try { + documentFile.getParentFile().mkdirs(); + File documentTempFile = new File( + documentFile.getAbsolutePath() + ".tmp"); + BufferedWriter bw = new BufferedWriter(new FileWriter( + documentTempFile)); + bw.write(documentString); + bw.close(); + documentFile.delete(); + documentTempFile.renameTo(documentFile); + this.storedFiles++; + this.storedBytes += documentString.length(); + } catch (IOException e) { + System.err.println("Could not write file '" + + documentFile.getAbsolutePath() + "'."); + e.printStackTrace(); + return false; + } + return true; + } + + public <T extends Document> T retrieve(Class<T> documentType, + boolean parse) { + return this.retrieve(documentType, parse, null); + } + + public <T extends Document> T retrieve(Class<T> documentType, + boolean parse, String fingerprint) { + if (documentType.equals(NodeStatus.class)) { + return documentType.cast(this.retrieveNodeStatus(fingerprint)); + } else if (documentType.equals(SummaryDocument.class)) { + return documentType.cast(this.retrieveSummaryDocument(fingerprint)); + } else { + return this.retrieveDocumentFile(documentType, parse, fingerprint); + } + } + + private NodeStatus retrieveNodeStatus(String fingerprint) { + if (this.cachedNodeStatuses == null) { + this.cacheNodeStatuses(); + } + return this.cachedNodeStatuses.get(fingerprint); + } + + private SummaryDocument retrieveSummaryDocument(String fingerprint) { + if (this.cachedSummaryDocuments == null) { + this.cacheSummaryDocuments(); + } + if (this.cachedSummaryDocuments.containsKey(fingerprint)) { + return this.cachedSummaryDocuments.get(fingerprint); + } + /* TODO This is an evil hack to support looking up relays or bridges + * that haven't been running for a week without having to load + * 500,000 NodeStatus instances into memory. Maybe there's a better + * way? Or do we need to switch to a real database for this? */ + DetailsDocument detailsDocument = this.retrieveDocumentFile( + DetailsDocument.class, true, fingerprint); + if (detailsDocument == null) { + return null; + } + boolean isRelay = detailsDocument.getHashedFingerprint() == null; + boolean running = false; + String nickname = detailsDocument.getNickname(); + List<String> addresses = new ArrayList<String>(); + String countryCode = null, aSNumber = null, contact = null; + for (String orAddressAndPort : detailsDocument.getOrAddresses()) { + if (!orAddressAndPort.contains(":")) { + return null; + } + String orAddress = orAddressAndPort.substring(0, + orAddressAndPort.lastIndexOf(":")); + if (!addresses.contains(orAddress)) { + addresses.add(orAddress); + } + } + if (detailsDocument.getExitAddresses() != null) { + for (String exitAddress : detailsDocument.getExitAddresses()) { + if (!addresses.contains(exitAddress)) { + addresses.add(exitAddress); + } + } + } + SortedSet<String> relayFlags = new TreeSet<String>(), family = null; + long lastSeenMillis = -1L, consensusWeight = -1L, + firstSeenMillis = -1L; + SummaryDocument summaryDocument = new SummaryDocument(isRelay, + nickname, fingerprint, addresses, lastSeenMillis, running, + relayFlags, consensusWeight, countryCode, firstSeenMillis, + aSNumber, contact, family); + return summaryDocument; + } + + private <T extends Document> T retrieveDocumentFile( + Class<T> documentType, boolean parse, String fingerprint) { + File documentFile = this.getDocumentFile(documentType, fingerprint); + if (documentFile == null || !documentFile.exists()) { + return null; + } else if (documentFile.isDirectory()) { + System.err.println("Could not read file '" + + documentFile.getAbsolutePath() + "', because it is a " + + "directory."); + return null; + } + String documentString = null; + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + BufferedInputStream bis = new BufferedInputStream( + new FileInputStream(documentFile)); + int len; + byte[] data = new byte[1024]; + while ((len = bis.read(data, 0, 1024)) >= 0) { + baos.write(data, 0, len); + } + bis.close(); + byte[] allData = baos.toByteArray(); + if (allData.length == 0) { + return null; + } + documentString = new String(allData, "US-ASCII"); + this.retrievedFiles++; + this.retrievedBytes += documentString.length(); + } catch (IOException e) { + System.err.println("Could not read file '" + + documentFile.getAbsolutePath() + "'."); + e.printStackTrace(); + return null; + } + T result = null; + if (!parse) { + return this.retrieveUnparsedDocumentFile(documentType, + documentString); + } else if (documentType.equals(DetailsDocument.class) || + documentType.equals(BandwidthDocument.class) || + documentType.equals(WeightsDocument.class) || + documentType.equals(ClientsDocument.class) || + documentType.equals(UptimeDocument.class)) { + return this.retrieveParsedDocumentFile(documentType, + documentString); + } else if (documentType.equals(BandwidthStatus.class) || + documentType.equals(WeightsStatus.class) || + documentType.equals(ClientsStatus.class) || + documentType.equals(UptimeStatus.class)) { + return this.retrieveParsedStatusFile(documentType, documentString); + } else if (documentType.equals(DetailsStatus.class)) { + return this.retrieveParsedDocumentFile(documentType, "{" + + documentString + "}"); + } else { + System.err.println("Parsing is not supported for type " + + documentType.getName() + "."); + } + return result; + } + + private <T extends Document> T retrieveParsedStatusFile( + Class<T> documentType, String documentString) { + T result = null; + try { + result = documentType.newInstance(); + result.fromDocumentString(documentString); + } catch (InstantiationException e) { + /* Handle below. */ + e.printStackTrace(); + } catch (IllegalAccessException e) { + /* Handle below. */ + e.printStackTrace(); + } + if (result == null) { + System.err.println("Could not initialize parsed status file of " + + "type " + documentType.getName() + "."); + } + return result; + } + + private <T extends Document> T retrieveParsedDocumentFile( + Class<T> documentType, String documentString) { + T result = null; + Gson gson = new Gson(); + try { + result = gson.fromJson(documentString, documentType); + } catch (JsonParseException e) { + /* Handle below. */ + e.printStackTrace(); + } + if (result == null) { + System.err.println("Could not initialize parsed document of type " + + documentType.getName() + "."); + } + return result; + } + + private <T extends Document> T retrieveUnparsedDocumentFile( + Class<T> documentType, String documentString) { + T result = null; + try { + result = documentType.newInstance(); + result.setDocumentString(documentString); + } catch (InstantiationException e) { + /* Handle below. */ + e.printStackTrace(); + } catch (IllegalAccessException e) { + /* Handle below. */ + e.printStackTrace(); + } + if (result == null) { + System.err.println("Could not initialize unparsed document of type " + + documentType.getName() + "."); + } + return result; + } + + public <T extends Document> boolean remove(Class<T> documentType) { + return this.remove(documentType, null); + } + + public <T extends Document> boolean remove(Class<T> documentType, + String fingerprint) { + if (documentType.equals(NodeStatus.class)) { + return this.removeNodeStatus(fingerprint); + } else if (documentType.equals(SummaryDocument.class)) { + return this.removeSummaryDocument(fingerprint); + } else { + return this.removeDocumentFile(documentType, fingerprint); + } + } + + private boolean removeNodeStatus(String fingerprint) { + if (this.cachedNodeStatuses == null) { + this.cacheNodeStatuses(); + } + return this.cachedNodeStatuses.remove(fingerprint) != null; + } + + private boolean removeSummaryDocument(String fingerprint) { + if (this.cachedSummaryDocuments == null) { + this.cacheSummaryDocuments(); + } + return this.cachedSummaryDocuments.remove(fingerprint) != null; + } + + private <T extends Document> boolean removeDocumentFile( + Class<T> documentType, String fingerprint) { + File documentFile = this.getDocumentFile(documentType, fingerprint); + if (documentFile == null || !documentFile.delete()) { + System.err.println("Could not delete file '" + + documentFile.getAbsolutePath() + "'."); + return false; + } + this.removedFiles++; + return true; + } + + private <T extends Document> File getDocumentFile(Class<T> documentType, + String fingerprint) { + File documentFile = null; + if (fingerprint == null && !documentType.equals(UpdateStatus.class) && + !documentType.equals(UptimeStatus.class)) { + // TODO Instead of using the update file workaround, add new method + // lastModified(Class<T> documentType) that serves a similar + // purpose. + return null; + } + File directory = null; + String fileName = null; + if (documentType.equals(DetailsStatus.class)) { + directory = this.statusDir; + fileName = String.format("details/%s/%s/%s", + fingerprint.substring(0, 1), fingerprint.substring(1, 2), + fingerprint); + } else if (documentType.equals(BandwidthStatus.class)) { + directory = this.statusDir; + fileName = String.format("bandwidth/%s/%s/%s", + fingerprint.substring(0, 1), fingerprint.substring(1, 2), + fingerprint); + } else if (documentType.equals(WeightsStatus.class)) { + directory = this.statusDir; + fileName = String.format("weights/%s/%s/%s", + fingerprint.substring(0, 1), fingerprint.substring(1, 2), + fingerprint); + } else if (documentType.equals(ClientsStatus.class)) { + directory = this.statusDir; + fileName = String.format("clients/%s/%s/%s", + fingerprint.substring(0, 1), fingerprint.substring(1, 2), + fingerprint); + } else if (documentType.equals(UptimeStatus.class)) { + directory = this.statusDir; + if (fingerprint == null) { + fileName = "uptime"; + } else { + fileName = String.format("uptimes/%s/%s/%s", + fingerprint.substring(0, 1), fingerprint.substring(1, 2), + fingerprint); + } + } else if (documentType.equals(UpdateStatus.class)) { + directory = this.outDir; + fileName = "update"; + } else if (documentType.equals(DetailsDocument.class)) { + directory = this.outDir; + fileName = String.format("details/%s", fingerprint); + } else if (documentType.equals(BandwidthDocument.class)) { + directory = this.outDir; + fileName = String.format("bandwidth/%s", fingerprint); + } else if (documentType.equals(WeightsDocument.class)) { + directory = this.outDir; + fileName = String.format("weights/%s", fingerprint); + } else if (documentType.equals(ClientsDocument.class)) { + directory = this.outDir; + fileName = String.format("clients/%s", fingerprint); + } else if (documentType.equals(UptimeDocument.class)) { + directory = this.outDir; + fileName = String.format("uptimes/%s", fingerprint); + } + if (directory != null && fileName != null) { + documentFile = new File(directory, fileName); + } + return documentFile; + } + + public void flushDocumentCache() { + /* Write cached node statuses to disk, and write update file + * containing current time. It's important to write the update file + * now, not earlier, because the front-end should not read new node + * statuses until all details, bandwidths, and weights are ready. */ + if (this.cachedNodeStatuses != null || + this.cachedSummaryDocuments != null) { + if (this.cachedNodeStatuses != null) { + this.writeNodeStatuses(); + } + if (this.cachedSummaryDocuments != null) { + this.writeSummaryDocuments(); + } + this.writeUpdateStatus(); + } + } + + private void writeNodeStatuses() { + File directory = this.statusDir; + if (directory == null) { + return; + } + File summaryFile = new File(directory, "summary"); + SortedMap<String, NodeStatus> + cachedRelays = new TreeMap<String, NodeStatus>(), + cachedBridges = new TreeMap<String, NodeStatus>(); + for (Map.Entry<String, NodeStatus> e : + this.cachedNodeStatuses.entrySet()) { + if (e.getValue().isRelay()) { + cachedRelays.put(e.getKey(), e.getValue()); + } else { + cachedBridges.put(e.getKey(), e.getValue()); + } + } + StringBuilder sb = new StringBuilder(); + for (NodeStatus relay : cachedRelays.values()) { + String line = relay.toString(); + if (line != null) { + sb.append(line + "\n"); + } else { + System.err.println("Could not serialize relay node status '" + + relay.getFingerprint() + "'"); + } + } + for (NodeStatus bridge : cachedBridges.values()) { + String line = bridge.toString(); + if (line != null) { + sb.append(line + "\n"); + } else { + System.err.println("Could not serialize bridge node status '" + + bridge.getFingerprint() + "'"); + } + } + String documentString = sb.toString(); + try { + summaryFile.getParentFile().mkdirs(); + BufferedWriter bw = new BufferedWriter(new FileWriter(summaryFile)); + bw.write(documentString); + bw.close(); + this.storedFiles++; + this.storedBytes += documentString.length(); + } catch (IOException e) { + System.err.println("Could not write file '" + + summaryFile.getAbsolutePath() + "'."); + e.printStackTrace(); + } + } + + private void writeSummaryDocuments() { + StringBuilder sb = new StringBuilder(); + Gson gson = new Gson(); + for (SummaryDocument summaryDocument : + this.cachedSummaryDocuments.values()) { + String line = gson.toJson(summaryDocument); + if (line != null) { + sb.append(line + "\n"); + } else { + System.err.println("Could not serialize relay summary document '" + + summaryDocument.getFingerprint() + "'"); + } + } + String documentString = sb.toString(); + File summaryFile = new File(this.outDir, "summary"); + try { + summaryFile.getParentFile().mkdirs(); + BufferedWriter bw = new BufferedWriter(new FileWriter(summaryFile)); + bw.write(documentString); + bw.close(); + this.storedFiles++; + this.storedBytes += documentString.length(); + } catch (IOException e) { + System.err.println("Could not write file '" + + summaryFile.getAbsolutePath() + "'."); + e.printStackTrace(); + } + } + + private void writeUpdateStatus() { + if (this.outDir == null) { + return; + } + File updateFile = new File(this.outDir, "update"); + String documentString = String.valueOf(this.time.currentTimeMillis()); + try { + updateFile.getParentFile().mkdirs(); + BufferedWriter bw = new BufferedWriter(new FileWriter(updateFile)); + bw.write(documentString); + bw.close(); + this.storedFiles++; + this.storedBytes += documentString.length(); + } catch (IOException e) { + System.err.println("Could not write file '" + + updateFile.getAbsolutePath() + "'."); + e.printStackTrace(); + } + } + + public String getStatsString() { + StringBuilder sb = new StringBuilder(); + sb.append(" " + Logger.formatDecimalNumber(listOperations) + + " list operations performed\n"); + sb.append(" " + Logger.formatDecimalNumber(listedFiles) + + " files listed\n"); + sb.append(" " + Logger.formatDecimalNumber(storedFiles) + + " files stored\n"); + sb.append(" " + Logger.formatBytes(storedBytes) + " stored\n"); + sb.append(" " + Logger.formatDecimalNumber(retrievedFiles) + + " files retrieved\n"); + sb.append(" " + Logger.formatBytes(retrievedBytes) + + " retrieved\n"); + sb.append(" " + Logger.formatDecimalNumber(removedFiles) + + " files removed\n"); + return sb.toString(); + } +} + diff --git a/src/org/torproject/onionoo/docs/GraphHistory.java b/src/org/torproject/onionoo/docs/GraphHistory.java new file mode 100644 index 0000000..19ace31 --- /dev/null +++ b/src/org/torproject/onionoo/docs/GraphHistory.java @@ -0,0 +1,56 @@ +/* Copyright 2014 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.docs; + +import java.util.List; + +public class GraphHistory { + + private String first; + public void setFirst(String first) { + this.first = first; + } + public String getFirst() { + return this.first; + } + + private String last; + public void setLast(String last) { + this.last = last; + } + public String getLast() { + return this.last; + } + + private Integer interval; + public void setInterval(Integer interval) { + this.interval = interval; + } + public Integer getInterval() { + return this.interval; + } + + private Double factor; + public void setFactor(Double factor) { + this.factor = factor; + } + public Double getFactor() { + return this.factor; + } + + private Integer count; + public void setCount(Integer count) { + this.count = count; + } + public Integer getCount() { + return this.count; + } + + private List<Integer> values; + public void setValues(List<Integer> values) { + this.values = values; + } + public List<Integer> getValues() { + return this.values; + } +} diff --git a/src/org/torproject/onionoo/docs/NodeStatus.java b/src/org/torproject/onionoo/docs/NodeStatus.java new file mode 100644 index 0000000..41292fd --- /dev/null +++ b/src/org/torproject/onionoo/docs/NodeStatus.java @@ -0,0 +1,582 @@ +/* Copyright 2011, 2012 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.docs; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; + +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.codec.digest.DigestUtils; +import org.torproject.onionoo.util.DateTimeHelper; + +/* Store search data of a single relay that was running in the past seven + * days. */ +public class NodeStatus extends Document { + + private boolean isRelay; + public boolean isRelay() { + return this.isRelay; + } + + private String fingerprint; + public String getFingerprint() { + return this.fingerprint; + } + + private String hashedFingerprint; + public String getHashedFingerprint() { + return this.hashedFingerprint; + } + + private String nickname; + public String getNickname() { + return this.nickname; + } + + private String address; + public String getAddress() { + return this.address; + } + + private SortedSet<String> orAddresses; + private SortedSet<String> orAddressesAndPorts; + public SortedSet<String> getOrAddresses() { + return new TreeSet<String>(this.orAddresses); + } + public void addOrAddressAndPort(String orAddressAndPort) { + if (orAddressAndPort.contains(":") && orAddressAndPort.length() > 0) { + String orAddress = orAddressAndPort.substring(0, + orAddressAndPort.lastIndexOf(':')); + if (this.exitAddresses.contains(orAddress)) { + this.exitAddresses.remove(orAddress); + } + this.orAddresses.add(orAddress); + this.orAddressesAndPorts.add(orAddressAndPort); + } + } + public SortedSet<String> getOrAddressesAndPorts() { + return new TreeSet<String>(this.orAddressesAndPorts); + } + + private SortedSet<String> exitAddresses; + public void addExitAddress(String exitAddress) { + if (exitAddress.length() > 0 && !this.address.equals(exitAddress) && + !this.orAddresses.contains(exitAddress)) { + this.exitAddresses.add(exitAddress); + } + } + public SortedSet<String> getExitAddresses() { + return new TreeSet<String>(this.exitAddresses); + } + + private Float latitude; + public void setLatitude(Float latitude) { + this.latitude = latitude; + } + public Float getLatitude() { + return this.latitude; + } + + private Float longitude; + public void setLongitude(Float longitude) { + this.longitude = longitude; + } + public Float getLongitude() { + return this.longitude; + } + + private String countryCode; + public void setCountryCode(String countryCode) { + this.countryCode = countryCode; + } + public String getCountryCode() { + return this.countryCode; + } + + private String countryName; + public void setCountryName(String countryName) { + this.countryName = countryName; + } + public String getCountryName() { + return this.countryName; + } + + private String regionName; + public void setRegionName(String regionName) { + this.regionName = regionName; + } + public String getRegionName() { + return this.regionName; + } + + private String cityName; + public void setCityName(String cityName) { + this.cityName = cityName; + } + public String getCityName() { + return this.cityName; + } + + private String aSName; + public void setASName(String aSName) { + this.aSName = aSName; + } + public String getASName() { + return this.aSName; + } + + private String aSNumber; + public void setASNumber(String aSNumber) { + this.aSNumber = aSNumber; + } + public String getASNumber() { + return this.aSNumber; + } + + private long firstSeenMillis; + public long getFirstSeenMillis() { + return this.firstSeenMillis; + } + + private long lastSeenMillis; + public long getLastSeenMillis() { + return this.lastSeenMillis; + } + + private int orPort; + public int getOrPort() { + return this.orPort; + } + + private int dirPort; + public int getDirPort() { + return this.dirPort; + } + + private SortedSet<String> relayFlags; + public SortedSet<String> getRelayFlags() { + return this.relayFlags; + } + + private long consensusWeight; + public long getConsensusWeight() { + return this.consensusWeight; + } + + private boolean running; + public void setRunning(boolean running) { + this.running = running; + } + public boolean getRunning() { + return this.running; + } + + private String hostName; + public void setHostName(String hostName) { + this.hostName = hostName; + } + public String getHostName() { + return this.hostName; + } + + private long lastRdnsLookup = -1L; + public void setLastRdnsLookup(long lastRdnsLookup) { + this.lastRdnsLookup = lastRdnsLookup; + } + public long getLastRdnsLookup() { + return this.lastRdnsLookup; + } + + private double advertisedBandwidthFraction = -1.0; + public void setAdvertisedBandwidthFraction( + double advertisedBandwidthFraction) { + this.advertisedBandwidthFraction = advertisedBandwidthFraction; + } + public double getAdvertisedBandwidthFraction() { + return this.advertisedBandwidthFraction; + } + + private double consensusWeightFraction = -1.0; + public void setConsensusWeightFraction(double consensusWeightFraction) { + this.consensusWeightFraction = consensusWeightFraction; + } + public double getConsensusWeightFraction() { + return this.consensusWeightFraction; + } + + private double guardProbability = -1.0; + public void setGuardProbability(double guardProbability) { + this.guardProbability = guardProbability; + } + public double getGuardProbability() { + return this.guardProbability; + } + + private double middleProbability = -1.0; + public void setMiddleProbability(double middleProbability) { + this.middleProbability = middleProbability; + } + public double getMiddleProbability() { + return this.middleProbability; + } + + private double exitProbability = -1.0; + public void setExitProbability(double exitProbability) { + this.exitProbability = exitProbability; + } + public double getExitProbability() { + return this.exitProbability; + } + + private String defaultPolicy; + public String getDefaultPolicy() { + return this.defaultPolicy; + } + + private String portList; + public String getPortList() { + return this.portList; + } + + private SortedMap<Long, Set<String>> lastAddresses; + public SortedMap<Long, Set<String>> getLastAddresses() { + return this.lastAddresses == null ? null : + new TreeMap<Long, Set<String>>(this.lastAddresses); + } + public long getLastChangedOrAddress() { + long lastChangedAddressesMillis = -1L; + if (this.lastAddresses != null) { + Set<String> lastAddresses = null; + for (Map.Entry<Long, Set<String>> e : this.lastAddresses.entrySet()) { + if (lastAddresses != null) { + for (String address : e.getValue()) { + if (!lastAddresses.contains(address)) { + return lastChangedAddressesMillis; + } + } + } + lastChangedAddressesMillis = e.getKey(); + lastAddresses = e.getValue(); + } + } + return lastChangedAddressesMillis; + } + + private String contact; + public void setContact(String contact) { + if (contact == null) { + this.contact = null; + } else { + StringBuilder sb = new StringBuilder(); + for (char c : contact.toLowerCase().toCharArray()) { + if (c >= 32 && c < 127) { + sb.append(c); + } else { + sb.append(" "); + } + } + this.contact = sb.toString(); + } + } + public String getContact() { + return this.contact; + } + + private Boolean recommendedVersion; + public Boolean getRecommendedVersion() { + return this.recommendedVersion; + } + + private SortedSet<String> familyFingerprints; + public void setFamilyFingerprints( + SortedSet<String> familyFingerprints) { + this.familyFingerprints = familyFingerprints; + } + public SortedSet<String> getFamilyFingerprints() { + return this.familyFingerprints; + } + + public NodeStatus(boolean isRelay, String nickname, String fingerprint, + String address, SortedSet<String> orAddressesAndPorts, + SortedSet<String> exitAddresses, long lastSeenMillis, int orPort, + int dirPort, SortedSet<String> relayFlags, long consensusWeight, + String countryCode, String hostName, long lastRdnsLookup, + String defaultPolicy, String portList, long firstSeenMillis, + long lastChangedAddresses, String aSNumber, String contact, + Boolean recommendedVersion, SortedSet<String> familyFingerprints) { + this.isRelay = isRelay; + this.nickname = nickname; + this.fingerprint = fingerprint; + try { + this.hashedFingerprint = DigestUtils.shaHex(Hex.decodeHex( + this.fingerprint.toCharArray())).toUpperCase(); + } catch (DecoderException e) { + throw new IllegalArgumentException("Fingerprint '" + fingerprint + + "' is not a valid fingerprint.", e); + } + this.address = address; + this.exitAddresses = new TreeSet<String>(); + if (exitAddresses != null) { + this.exitAddresses.addAll(exitAddresses); + } + this.exitAddresses.remove(this.address); + this.orAddresses = new TreeSet<String>(); + this.orAddressesAndPorts = new TreeSet<String>(); + if (orAddressesAndPorts != null) { + for (String orAddressAndPort : orAddressesAndPorts) { + this.addOrAddressAndPort(orAddressAndPort); + } + } + this.lastSeenMillis = lastSeenMillis; + this.orPort = orPort; + this.dirPort = dirPort; + this.relayFlags = relayFlags; + this.consensusWeight = consensusWeight; + this.countryCode = countryCode; + this.hostName = hostName; + this.lastRdnsLookup = lastRdnsLookup; + this.defaultPolicy = defaultPolicy; + this.portList = portList; + this.firstSeenMillis = firstSeenMillis; + this.lastAddresses = + new TreeMap<Long, Set<String>>(Collections.reverseOrder()); + Set<String> addresses = new HashSet<String>(); + addresses.add(address + ":" + orPort); + if (dirPort > 0) { + addresses.add(address + ":" + dirPort); + } + addresses.addAll(orAddressesAndPorts); + this.lastAddresses.put(lastChangedAddresses, addresses); + this.aSNumber = aSNumber; + this.contact = contact; + this.recommendedVersion = recommendedVersion; + this.familyFingerprints = familyFingerprints; + } + + public static NodeStatus fromString(String documentString) { + boolean isRelay = false; + String nickname = null, fingerprint = null, address = null, + countryCode = null, hostName = null, defaultPolicy = null, + portList = null, aSNumber = null, contact = null; + SortedSet<String> orAddressesAndPorts = null, exitAddresses = null, + relayFlags = null, familyFingerprints = null; + long lastSeenMillis = -1L, consensusWeight = -1L, + lastRdnsLookup = -1L, firstSeenMillis = -1L, + lastChangedAddresses = -1L; + int orPort = -1, dirPort = -1; + Boolean recommendedVersion = null; + try { + String separator = documentString.contains("\t") ? "\t" : " "; + String[] parts = documentString.trim().split(separator); + isRelay = parts[0].equals("r"); + if (parts.length < 9) { + System.err.println("Too few space-separated values in line '" + + documentString.trim() + "'. Skipping."); + return null; + } + nickname = parts[1]; + fingerprint = parts[2]; + orAddressesAndPorts = new TreeSet<String>(); + exitAddresses = new TreeSet<String>(); + String addresses = parts[3]; + if (addresses.contains(";")) { + String[] addressParts = addresses.split(";", -1); + if (addressParts.length != 3) { + System.err.println("Invalid addresses entry in line '" + + documentString.trim() + "'. Skipping."); + return null; + } + address = addressParts[0]; + if (addressParts[1].length() > 0) { + orAddressesAndPorts.addAll(Arrays.asList( + addressParts[1].split("\\+"))); + } + if (addressParts[2].length() > 0) { + exitAddresses.addAll(Arrays.asList( + addressParts[2].split("\\+"))); + } + } else { + address = addresses; + } + lastSeenMillis = DateTimeHelper.parse(parts[4] + " " + parts[5]); + if (lastSeenMillis < 0L) { + System.err.println("Parse exception while parsing node status " + + "line '" + documentString + "'. Skipping."); + return null; + } + orPort = Integer.parseInt(parts[6]); + dirPort = Integer.parseInt(parts[7]); + relayFlags = new TreeSet<String>(); + if (parts[8].length() > 0) { + relayFlags.addAll(Arrays.asList(parts[8].split(","))); + } + if (parts.length > 9) { + consensusWeight = Long.parseLong(parts[9]); + } + if (parts.length > 10) { + countryCode = parts[10]; + } + if (parts.length > 12) { + hostName = parts[11].equals("null") ? null : parts[11]; + lastRdnsLookup = Long.parseLong(parts[12]); + } + if (parts.length > 14) { + if (!parts[13].equals("null")) { + defaultPolicy = parts[13]; + } + if (!parts[14].equals("null")) { + portList = parts[14]; + } + } + firstSeenMillis = lastSeenMillis; + if (parts.length > 16) { + firstSeenMillis = DateTimeHelper.parse(parts[15] + " " + + parts[16]); + if (firstSeenMillis < 0L) { + System.err.println("Parse exception while parsing node status " + + "line '" + documentString + "'. Skipping."); + return null; + } + } + lastChangedAddresses = lastSeenMillis; + if (parts.length > 18 && !parts[17].equals("null")) { + lastChangedAddresses = DateTimeHelper.parse(parts[17] + " " + + parts[18]); + if (lastChangedAddresses < 0L) { + System.err.println("Parse exception while parsing node status " + + "line '" + documentString + "'. Skipping."); + return null; + } + } + if (parts.length > 19) { + aSNumber = parts[19]; + } + if (parts.length > 20) { + contact = parts[20]; + } + if (parts.length > 21) { + recommendedVersion = parts[21].equals("null") ? null : + parts[21].equals("true"); + } + if (parts.length > 22 && !parts[22].equals("null")) { + familyFingerprints = new TreeSet<String>(Arrays.asList( + parts[22].split(";"))); + } + } catch (NumberFormatException e) { + System.err.println("Number format exception while parsing node " + + "status line '" + documentString + "': " + e.getMessage() + + ". Skipping."); + return null; + } catch (Exception e) { + /* This catch block is only here to handle yet unknown errors. It + * should go away once we're sure what kind of errors can occur. */ + System.err.println("Unknown exception while parsing node status " + + "line '" + documentString + "': " + e.getMessage() + ". " + + "Skipping."); + return null; + } + NodeStatus newNodeStatus = new NodeStatus(isRelay, nickname, + fingerprint, address, orAddressesAndPorts, exitAddresses, + lastSeenMillis, orPort, dirPort, relayFlags, consensusWeight, + countryCode, hostName, lastRdnsLookup, defaultPolicy, portList, + firstSeenMillis, lastChangedAddresses, aSNumber, contact, + recommendedVersion, familyFingerprints); + return newNodeStatus; + } + + public void update(NodeStatus newNodeStatus) { + if (newNodeStatus.lastSeenMillis > this.lastSeenMillis) { + this.nickname = newNodeStatus.nickname; + this.address = newNodeStatus.address; + this.orAddressesAndPorts = newNodeStatus.orAddressesAndPorts; + this.lastSeenMillis = newNodeStatus.lastSeenMillis; + this.orPort = newNodeStatus.orPort; + this.dirPort = newNodeStatus.dirPort; + this.relayFlags = newNodeStatus.relayFlags; + this.consensusWeight = newNodeStatus.consensusWeight; + this.countryCode = newNodeStatus.countryCode; + this.defaultPolicy = newNodeStatus.defaultPolicy; + this.portList = newNodeStatus.portList; + this.aSNumber = newNodeStatus.aSNumber; + this.recommendedVersion = newNodeStatus.recommendedVersion; + } + if (this.isRelay && newNodeStatus.isRelay) { + this.lastAddresses.putAll(newNodeStatus.lastAddresses); + } + this.firstSeenMillis = Math.min(newNodeStatus.firstSeenMillis, + this.firstSeenMillis); + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(this.isRelay ? "r" : "b"); + sb.append("\t" + this.nickname); + sb.append("\t" + this.fingerprint); + sb.append("\t" + this.address + ";"); + int written = 0; + for (String orAddressAndPort : this.orAddressesAndPorts) { + sb.append((written++ > 0 ? "+" : "") + orAddressAndPort); + } + sb.append(";"); + if (this.isRelay) { + written = 0; + for (String exitAddress : this.exitAddresses) { + sb.append((written++ > 0 ? "+" : "") + + exitAddress); + } + } + sb.append("\t" + DateTimeHelper.format(this.lastSeenMillis, + DateTimeHelper.ISO_DATETIME_TAB_FORMAT)); + sb.append("\t" + this.orPort); + sb.append("\t" + this.dirPort + "\t"); + written = 0; + for (String relayFlag : this.relayFlags) { + sb.append((written++ > 0 ? "," : "") + relayFlag); + } + if (this.isRelay) { + sb.append("\t" + String.valueOf(this.consensusWeight)); + sb.append("\t" + + (this.countryCode != null ? this.countryCode : "??")); + sb.append("\t" + (this.hostName != null ? this.hostName : "null")); + sb.append("\t" + String.valueOf(this.lastRdnsLookup)); + sb.append("\t" + (this.defaultPolicy != null ? this.defaultPolicy + : "null")); + sb.append("\t" + (this.portList != null ? this.portList : "null")); + } else { + sb.append("\t-1\t??\tnull\t-1\tnull\tnull"); + } + sb.append("\t" + DateTimeHelper.format(this.firstSeenMillis, + DateTimeHelper.ISO_DATETIME_TAB_FORMAT)); + if (this.isRelay) { + sb.append("\t" + DateTimeHelper.format( + this.getLastChangedOrAddress(), + DateTimeHelper.ISO_DATETIME_TAB_FORMAT)); + sb.append("\t" + (this.aSNumber != null ? this.aSNumber : "null")); + } else { + sb.append("\tnull\tnull\tnull"); + } + sb.append("\t" + (this.contact != null ? this.contact : "")); + sb.append("\t" + (this.recommendedVersion == null ? "null" : + this.recommendedVersion ? "true" : "false")); + if (this.familyFingerprints == null || + this.familyFingerprints.isEmpty()) { + sb.append("\tnull"); + } else { + sb.append("\t"); + written = 0; + for (String familyFingerprint : this.familyFingerprints) { + sb.append((written++ > 0 ? ";" : "") + familyFingerprint); + } + } + return sb.toString(); + } +} + diff --git a/src/org/torproject/onionoo/docs/SummaryDocument.java b/src/org/torproject/onionoo/docs/SummaryDocument.java new file mode 100644 index 0000000..0c71ae2 --- /dev/null +++ b/src/org/torproject/onionoo/docs/SummaryDocument.java @@ -0,0 +1,202 @@ +/* Copyright 2013--2014 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.docs; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.regex.Pattern; + +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.codec.digest.DigestUtils; +import org.torproject.onionoo.util.DateTimeHelper; + +public class SummaryDocument extends Document { + + private boolean t; + public void setRelay(boolean isRelay) { + this.t = isRelay; + } + public boolean isRelay() { + return this.t; + } + + private String f; + public void setFingerprint(String fingerprint) { + if (fingerprint != null) { + Pattern fingerprintPattern = Pattern.compile("^[0-9a-fA-F]{40}$"); + if (!fingerprintPattern.matcher(fingerprint).matches()) { + throw new IllegalArgumentException("Fingerprint '" + fingerprint + + "' is not a valid fingerprint."); + } + } + this.f = fingerprint; + } + public String getFingerprint() { + return this.f; + } + + public String getHashedFingerprint() { + String hashedFingerprint = null; + if (this.f != null) { + try { + hashedFingerprint = DigestUtils.shaHex(Hex.decodeHex( + this.f.toCharArray())).toUpperCase(); + } catch (DecoderException e) { + /* Format tested in setFingerprint(). */ + } + } + return hashedFingerprint; + } + + private String n; + public void setNickname(String nickname) { + if (nickname == null || nickname.equals("Unnamed")) { + this.n = null; + } else { + this.n = nickname; + } + } + public String getNickname() { + return this.n == null ? "Unnamed" : this.n; + } + + private String[] ad; + public void setAddresses(List<String> addresses) { + this.ad = this.collectionToStringArray(addresses); + } + public List<String> getAddresses() { + return this.stringArrayToList(this.ad); + } + + private String[] collectionToStringArray( + Collection<String> collection) { + String[] stringArray = null; + if (collection != null && !collection.isEmpty()) { + stringArray = new String[collection.size()]; + int i = 0; + for (String string : collection) { + stringArray[i++] = string; + } + } + return stringArray; + } + private List<String> stringArrayToList(String[] stringArray) { + List<String> list; + if (stringArray == null) { + list = new ArrayList<String>(); + } else { + list = Arrays.asList(stringArray); + } + return list; + } + private SortedSet<String> stringArrayToSortedSet(String[] stringArray) { + SortedSet<String> sortedSet = new TreeSet<String>(); + if (stringArray != null) { + sortedSet.addAll(Arrays.asList(stringArray)); + } + return sortedSet; + } + + private String cc; + public void setCountryCode(String countryCode) { + this.cc = countryCode; + } + public String getCountryCode() { + return this.cc; + } + + private String as; + public void setASNumber(String aSNumber) { + this.as = aSNumber; + } + public String getASNumber() { + return this.as; + } + + private String fs; + public void setFirstSeenMillis(long firstSeenMillis) { + this.fs = DateTimeHelper.format(firstSeenMillis); + } + public long getFirstSeenMillis() { + return DateTimeHelper.parse(this.fs); + } + + private String ls; + public void setLastSeenMillis(long lastSeenMillis) { + this.ls = DateTimeHelper.format(lastSeenMillis); + } + public long getLastSeenMillis() { + return DateTimeHelper.parse(this.ls); + } + + private String[] rf; + public void setRelayFlags(SortedSet<String> relayFlags) { + this.rf = this.collectionToStringArray(relayFlags); + } + public SortedSet<String> getRelayFlags() { + return this.stringArrayToSortedSet(this.rf); + } + + private long cw; + public void setConsensusWeight(long consensusWeight) { + this.cw = consensusWeight; + } + public long getConsensusWeight() { + return this.cw; + } + + private boolean r; + public void setRunning(boolean isRunning) { + this.r = isRunning; + } + public boolean isRunning() { + return this.r; + } + + private String c; + public void setContact(String contact) { + if (contact != null && contact.length() == 0) { + this.c = null; + } else { + this.c = contact; + } + } + public String getContact() { + return this.c; + } + + private String[] ff; + public void setFamilyFingerprints( + SortedSet<String> familyFingerprints) { + this.ff = this.collectionToStringArray(familyFingerprints); + } + public SortedSet<String> getFamilyFingerprints() { + return this.stringArrayToSortedSet(this.ff); + } + + public SummaryDocument(boolean isRelay, String nickname, + String fingerprint, List<String> addresses, long lastSeenMillis, + boolean running, SortedSet<String> relayFlags, long consensusWeight, + String countryCode, long firstSeenMillis, String aSNumber, + String contact, SortedSet<String> familyFingerprints) { + this.setRelay(isRelay); + this.setNickname(nickname); + this.setFingerprint(fingerprint); + this.setAddresses(addresses); + this.setLastSeenMillis(lastSeenMillis); + this.setRunning(running); + this.setRelayFlags(relayFlags); + this.setConsensusWeight(consensusWeight); + this.setCountryCode(countryCode); + this.setFirstSeenMillis(firstSeenMillis); + this.setASNumber(aSNumber); + this.setContact(contact); + this.setFamilyFingerprints(familyFingerprints); + } +} + diff --git a/src/org/torproject/onionoo/docs/UpdateStatus.java b/src/org/torproject/onionoo/docs/UpdateStatus.java new file mode 100644 index 0000000..7bd710b --- /dev/null +++ b/src/org/torproject/onionoo/docs/UpdateStatus.java @@ -0,0 +1,7 @@ +/* Copyright 2013 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.docs; + +public class UpdateStatus extends Document { +} + diff --git a/src/org/torproject/onionoo/docs/UptimeDocument.java b/src/org/torproject/onionoo/docs/UptimeDocument.java new file mode 100644 index 0000000..7f0bacc --- /dev/null +++ b/src/org/torproject/onionoo/docs/UptimeDocument.java @@ -0,0 +1,23 @@ +/* Copyright 2014 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.docs; + +import java.util.Map; + +public class UptimeDocument extends Document { + + @SuppressWarnings("unused") + private String fingerprint; + public void setFingerprint(String fingerprint) { + this.fingerprint = fingerprint; + } + + private Map<String, GraphHistory> uptime; + public void setUptime(Map<String, GraphHistory> uptime) { + this.uptime = uptime; + } + public Map<String, GraphHistory> getUptime() { + return this.uptime; + } +} + diff --git a/src/org/torproject/onionoo/docs/UptimeHistory.java b/src/org/torproject/onionoo/docs/UptimeHistory.java new file mode 100644 index 0000000..f0a966b --- /dev/null +++ b/src/org/torproject/onionoo/docs/UptimeHistory.java @@ -0,0 +1,90 @@ +package org.torproject.onionoo.docs; + +import org.torproject.onionoo.util.DateTimeHelper; + +public class UptimeHistory + implements Comparable<UptimeHistory> { + + private boolean relay; + public boolean isRelay() { + return this.relay; + } + + private long startMillis; + public long getStartMillis() { + return this.startMillis; + } + + private int uptimeHours; + public int getUptimeHours() { + return this.uptimeHours; + } + + UptimeHistory(boolean relay, long startMillis, + int uptimeHours) { + this.relay = relay; + this.startMillis = startMillis; + this.uptimeHours = uptimeHours; + } + + public static UptimeHistory fromString(String uptimeHistoryString) { + String[] parts = uptimeHistoryString.split(" ", 3); + if (parts.length != 3) { + return null; + } + boolean relay = false; + if (parts[0].equals("r")) { + relay = true; + } else if (!parts[0].equals("b")) { + return null; + } + long startMillis = DateTimeHelper.parse(parts[1], + DateTimeHelper.DATEHOUR_NOSPACE_FORMAT); + if (startMillis < 0L) { + return null; + } + int uptimeHours = -1; + try { + uptimeHours = Integer.parseInt(parts[2]); + } catch (NumberFormatException e) { + return null; + } + return new UptimeHistory(relay, startMillis, uptimeHours); + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(this.relay ? "r" : "b"); + sb.append(" " + DateTimeHelper.format(this.startMillis, + DateTimeHelper.DATEHOUR_NOSPACE_FORMAT)); + sb.append(" " + String.format("%d", this.uptimeHours)); + return sb.toString(); + } + + public void addUptime(UptimeHistory other) { + this.uptimeHours += other.uptimeHours; + if (this.startMillis > other.startMillis) { + this.startMillis = other.startMillis; + } + } + + public int compareTo(UptimeHistory other) { + if (this.relay && !other.relay) { + return -1; + } else if (!this.relay && other.relay) { + return 1; + } + return this.startMillis < other.startMillis ? -1 : + this.startMillis > other.startMillis ? 1 : 0; + } + + public boolean equals(Object other) { + return other instanceof UptimeHistory && + this.relay == ((UptimeHistory) other).relay && + this.startMillis == ((UptimeHistory) other).startMillis; + } + + public int hashCode() { + return (int) this.startMillis + (this.relay ? 1 : 0); + } +} \ No newline at end of file diff --git a/src/org/torproject/onionoo/docs/UptimeStatus.java b/src/org/torproject/onionoo/docs/UptimeStatus.java new file mode 100644 index 0000000..1da11f0 --- /dev/null +++ b/src/org/torproject/onionoo/docs/UptimeStatus.java @@ -0,0 +1,142 @@ +/* Copyright 2014 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.docs; + +import java.util.Scanner; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.torproject.onionoo.util.ApplicationFactory; +import org.torproject.onionoo.util.DateTimeHelper; + +public class UptimeStatus extends Document { + + private transient String fingerprint; + + private transient boolean isDirty = false; + + private SortedSet<UptimeHistory> relayHistory = + new TreeSet<UptimeHistory>(); + public void setRelayHistory(SortedSet<UptimeHistory> relayHistory) { + this.relayHistory = relayHistory; + } + public SortedSet<UptimeHistory> getRelayHistory() { + return this.relayHistory; + } + + private SortedSet<UptimeHistory> bridgeHistory = + new TreeSet<UptimeHistory>(); + public void setBridgeHistory(SortedSet<UptimeHistory> bridgeHistory) { + this.bridgeHistory = bridgeHistory; + } + public SortedSet<UptimeHistory> getBridgeHistory() { + return this.bridgeHistory; + } + + public static UptimeStatus loadOrCreate(String fingerprint) { + UptimeStatus uptimeStatus = (fingerprint == null) ? + ApplicationFactory.getDocumentStore().retrieve( + UptimeStatus.class, true) : + ApplicationFactory.getDocumentStore().retrieve( + UptimeStatus.class, true, fingerprint); + if (uptimeStatus == null) { + uptimeStatus = new UptimeStatus(); + } + uptimeStatus.fingerprint = fingerprint; + return uptimeStatus; + } + + public void fromDocumentString(String documentString) { + Scanner s = new Scanner(documentString); + while (s.hasNextLine()) { + String line = s.nextLine(); + UptimeHistory parsedLine = UptimeHistory.fromString(line); + if (parsedLine != null) { + if (parsedLine.isRelay()) { + this.relayHistory.add(parsedLine); + } else { + this.bridgeHistory.add(parsedLine); + } + } else { + System.err.println("Could not parse uptime history line '" + + line + "'. Skipping."); + } + } + s.close(); + } + + public void addToHistory(boolean relay, SortedSet<Long> newIntervals) { + for (long startMillis : newIntervals) { + SortedSet<UptimeHistory> history = relay ? this.relayHistory + : this.bridgeHistory; + UptimeHistory interval = new UptimeHistory(relay, startMillis, 1); + if (!history.headSet(interval).isEmpty()) { + UptimeHistory prev = history.headSet(interval).last(); + if (prev.isRelay() == interval.isRelay() && + prev.getStartMillis() + DateTimeHelper.ONE_HOUR + * prev.getUptimeHours() > interval.getStartMillis()) { + continue; + } + } + if (!history.tailSet(interval).isEmpty()) { + UptimeHistory next = history.tailSet(interval).first(); + if (next.isRelay() == interval.isRelay() && + next.getStartMillis() < interval.getStartMillis() + + DateTimeHelper.ONE_HOUR) { + continue; + } + } + history.add(interval); + this.isDirty = true; + } + } + + public void storeIfChanged() { + if (this.isDirty) { + this.compressHistory(this.relayHistory); + this.compressHistory(this.bridgeHistory); + if (fingerprint == null) { + ApplicationFactory.getDocumentStore().store(this); + } else { + ApplicationFactory.getDocumentStore().store(this, + this.fingerprint); + } + this.isDirty = false; + } + } + + private void compressHistory(SortedSet<UptimeHistory> history) { + SortedSet<UptimeHistory> uncompressedHistory = + new TreeSet<UptimeHistory>(history); + history.clear(); + UptimeHistory lastInterval = null; + for (UptimeHistory interval : uncompressedHistory) { + if (lastInterval != null && + lastInterval.getStartMillis() + DateTimeHelper.ONE_HOUR + * lastInterval.getUptimeHours() == interval.getStartMillis() && + lastInterval.isRelay() == interval.isRelay()) { + lastInterval.addUptime(interval); + } else { + if (lastInterval != null) { + history.add(lastInterval); + } + lastInterval = interval; + } + } + if (lastInterval != null) { + history.add(lastInterval); + } + } + + public String toDocumentString() { + StringBuilder sb = new StringBuilder(); + for (UptimeHistory interval : this.relayHistory) { + sb.append(interval.toString() + "\n"); + } + for (UptimeHistory interval : this.bridgeHistory) { + sb.append(interval.toString() + "\n"); + } + return sb.toString(); + } +} + diff --git a/src/org/torproject/onionoo/docs/WeightsDocument.java b/src/org/torproject/onionoo/docs/WeightsDocument.java new file mode 100644 index 0000000..104b661 --- /dev/null +++ b/src/org/torproject/onionoo/docs/WeightsDocument.java @@ -0,0 +1,64 @@ +/* Copyright 2014 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.docs; + +import java.util.Map; + +public class WeightsDocument extends Document { + + @SuppressWarnings("unused") + private String fingerprint; + public void setFingerprint(String fingerprint) { + this.fingerprint = fingerprint; + } + + @SuppressWarnings("unused") + private Map<String, GraphHistory> advertised_bandwidth_fraction; + public void setAdvertisedBandwidthFraction( + Map<String, GraphHistory> advertisedBandwidthFraction) { + this.advertised_bandwidth_fraction = advertisedBandwidthFraction; + } + + @SuppressWarnings("unused") + private Map<String, GraphHistory> consensus_weight_fraction; + public void setConsensusWeightFraction( + Map<String, GraphHistory> consensusWeightFraction) { + this.consensus_weight_fraction = consensusWeightFraction; + } + + @SuppressWarnings("unused") + private Map<String, GraphHistory> guard_probability; + public void setGuardProbability( + Map<String, GraphHistory> guardProbability) { + this.guard_probability = guardProbability; + } + + @SuppressWarnings("unused") + private Map<String, GraphHistory> middle_probability; + public void setMiddleProbability( + Map<String, GraphHistory> middleProbability) { + this.middle_probability = middleProbability; + } + + @SuppressWarnings("unused") + private Map<String, GraphHistory> exit_probability; + public void setExitProbability( + Map<String, GraphHistory> exitProbability) { + this.exit_probability = exitProbability; + } + + @SuppressWarnings("unused") + private Map<String, GraphHistory> advertised_bandwidth; + public void setAdvertisedBandwidth( + Map<String, GraphHistory> advertisedBandwidth) { + this.advertised_bandwidth = advertisedBandwidth; + } + + @SuppressWarnings("unused") + private Map<String, GraphHistory> consensus_weight; + public void setConsensusWeight( + Map<String, GraphHistory> consensusWeight) { + this.consensus_weight = consensusWeight; + } +} + diff --git a/src/org/torproject/onionoo/docs/WeightsStatus.java b/src/org/torproject/onionoo/docs/WeightsStatus.java new file mode 100644 index 0000000..678789b --- /dev/null +++ b/src/org/torproject/onionoo/docs/WeightsStatus.java @@ -0,0 +1,99 @@ +package org.torproject.onionoo.docs; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; +import java.util.SortedMap; +import java.util.TreeMap; + +import org.torproject.onionoo.util.DateTimeHelper; + +public class WeightsStatus extends Document { + + private SortedMap<long[], double[]> history = + new TreeMap<long[], double[]>(new Comparator<long[]>() { + public int compare(long[] a, long[] b) { + return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0; + } + }); + public void setHistory(SortedMap<long[], double[]> history) { + this.history = history; + } + public SortedMap<long[], double[]> getHistory() { + return this.history; + } + + private Map<String, Integer> advertisedBandwidths = + new HashMap<String, Integer>(); + public Map<String, Integer> getAdvertisedBandwidths() { + return this.advertisedBandwidths; + } + + public void fromDocumentString(String documentString) { + Scanner s = new Scanner(documentString); + while (s.hasNextLine()) { + String line = s.nextLine(); + String[] parts = line.split(" "); + if (parts.length == 2) { + String descriptorDigest = parts[0]; + int advertisedBandwidth = Integer.parseInt(parts[1]); + this.advertisedBandwidths.put(descriptorDigest, + advertisedBandwidth); + continue; + } + if (parts.length != 9 && parts.length != 11) { + System.err.println("Illegal line '" + line + "' in weights " + + "status file. Skipping this line."); + continue; + } + if (parts[4].equals("NaN")) { + /* Remove corrupt lines written on 2013-07-07 and the days + * after. */ + continue; + } + long validAfterMillis = DateTimeHelper.parse(parts[0] + " " + + parts[1]); + long freshUntilMillis = DateTimeHelper.parse(parts[2] + " " + + parts[3]); + if (validAfterMillis < 0L || freshUntilMillis < 0L) { + System.err.println("Could not parse timestamp while reading " + + "weights status file. Skipping."); + break; + } + long[] interval = new long[] { validAfterMillis, freshUntilMillis }; + double[] weights = new double[] { + Double.parseDouble(parts[4]), + Double.parseDouble(parts[5]), + Double.parseDouble(parts[6]), + Double.parseDouble(parts[7]), + Double.parseDouble(parts[8]), -1.0, -1.0 }; + if (parts.length == 11) { + weights[5] = Double.parseDouble(parts[9]); + weights[6] = Double.parseDouble(parts[10]); + } + this.history.put(interval, weights); + } + s.close(); + } + + public String toDocumentString() { + StringBuilder sb = new StringBuilder(); + for (Map.Entry<String, Integer> e : + this.advertisedBandwidths.entrySet()) { + sb.append(e.getKey() + " " + String.valueOf(e.getValue()) + "\n"); + } + for (Map.Entry<long[], double[]> e : history.entrySet()) { + long[] fresh = e.getKey(); + double[] weights = e.getValue(); + sb.append(DateTimeHelper.format(fresh[0]) + " " + + DateTimeHelper.format(fresh[1])); + for (double weight : weights) { + sb.append(String.format(" %.12f", weight)); + } + sb.append("\n"); + } + return sb.toString(); + } +} + diff --git a/src/org/torproject/onionoo/server/NodeIndexer.java b/src/org/torproject/onionoo/server/NodeIndexer.java new file mode 100644 index 0000000..b76f4c1 --- /dev/null +++ b/src/org/torproject/onionoo/server/NodeIndexer.java @@ -0,0 +1,432 @@ +package org.torproject.onionoo.server; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; + +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; + +import org.torproject.onionoo.docs.DocumentStore; +import org.torproject.onionoo.docs.SummaryDocument; +import org.torproject.onionoo.docs.UpdateStatus; +import org.torproject.onionoo.util.ApplicationFactory; +import org.torproject.onionoo.util.DateTimeHelper; +import org.torproject.onionoo.util.Time; + +class NodeIndex { + + private String relaysPublishedString; + public void setRelaysPublishedString(String relaysPublishedString) { + this.relaysPublishedString = relaysPublishedString; + } + public String getRelaysPublishedString() { + return relaysPublishedString; + } + + private String bridgesPublishedString; + public void setBridgesPublishedString(String bridgesPublishedString) { + this.bridgesPublishedString = bridgesPublishedString; + } + public String getBridgesPublishedString() { + return bridgesPublishedString; + } + + private List<String> relaysByConsensusWeight; + public void setRelaysByConsensusWeight( + List<String> relaysByConsensusWeight) { + this.relaysByConsensusWeight = relaysByConsensusWeight; + } + public List<String> getRelaysByConsensusWeight() { + return relaysByConsensusWeight; + } + + + private Map<String, SummaryDocument> relayFingerprintSummaryLines; + public void setRelayFingerprintSummaryLines( + Map<String, SummaryDocument> relayFingerprintSummaryLines) { + this.relayFingerprintSummaryLines = relayFingerprintSummaryLines; + } + public Map<String, SummaryDocument> getRelayFingerprintSummaryLines() { + return this.relayFingerprintSummaryLines; + } + + private Map<String, SummaryDocument> bridgeFingerprintSummaryLines; + public void setBridgeFingerprintSummaryLines( + Map<String, SummaryDocument> bridgeFingerprintSummaryLines) { + this.bridgeFingerprintSummaryLines = bridgeFingerprintSummaryLines; + } + public Map<String, SummaryDocument> getBridgeFingerprintSummaryLines() { + return this.bridgeFingerprintSummaryLines; + } + + private Map<String, Set<String>> relaysByCountryCode = null; + public void setRelaysByCountryCode( + Map<String, Set<String>> relaysByCountryCode) { + this.relaysByCountryCode = relaysByCountryCode; + } + public Map<String, Set<String>> getRelaysByCountryCode() { + return relaysByCountryCode; + } + + private Map<String, Set<String>> relaysByASNumber = null; + public void setRelaysByASNumber( + Map<String, Set<String>> relaysByASNumber) { + this.relaysByASNumber = relaysByASNumber; + } + public Map<String, Set<String>> getRelaysByASNumber() { + return relaysByASNumber; + } + + private Map<String, Set<String>> relaysByFlag = null; + public void setRelaysByFlag(Map<String, Set<String>> relaysByFlag) { + this.relaysByFlag = relaysByFlag; + } + public Map<String, Set<String>> getRelaysByFlag() { + return relaysByFlag; + } + + private Map<String, Set<String>> bridgesByFlag = null; + public void setBridgesByFlag(Map<String, Set<String>> bridgesByFlag) { + this.bridgesByFlag = bridgesByFlag; + } + public Map<String, Set<String>> getBridgesByFlag() { + return bridgesByFlag; + } + + private Map<String, Set<String>> relaysByContact = null; + public void setRelaysByContact( + Map<String, Set<String>> relaysByContact) { + this.relaysByContact = relaysByContact; + } + public Map<String, Set<String>> getRelaysByContact() { + return relaysByContact; + } + + private Map<String, Set<String>> relaysByFamily = null; + public void setRelaysByFamily(Map<String, Set<String>> relaysByFamily) { + this.relaysByFamily = relaysByFamily; + } + public Map<String, Set<String>> getRelaysByFamily() { + return this.relaysByFamily; + } + + private SortedMap<Integer, Set<String>> relaysByFirstSeenDays; + public void setRelaysByFirstSeenDays( + SortedMap<Integer, Set<String>> relaysByFirstSeenDays) { + this.relaysByFirstSeenDays = relaysByFirstSeenDays; + } + public SortedMap<Integer, Set<String>> getRelaysByFirstSeenDays() { + return relaysByFirstSeenDays; + } + + private SortedMap<Integer, Set<String>> bridgesByFirstSeenDays; + public void setBridgesByFirstSeenDays( + SortedMap<Integer, Set<String>> bridgesByFirstSeenDays) { + this.bridgesByFirstSeenDays = bridgesByFirstSeenDays; + } + public SortedMap<Integer, Set<String>> getBridgesByFirstSeenDays() { + return bridgesByFirstSeenDays; + } + + private SortedMap<Integer, Set<String>> relaysByLastSeenDays; + public void setRelaysByLastSeenDays( + SortedMap<Integer, Set<String>> relaysByLastSeenDays) { + this.relaysByLastSeenDays = relaysByLastSeenDays; + } + public SortedMap<Integer, Set<String>> getRelaysByLastSeenDays() { + return relaysByLastSeenDays; + } + + private SortedMap<Integer, Set<String>> bridgesByLastSeenDays; + public void setBridgesByLastSeenDays( + SortedMap<Integer, Set<String>> bridgesByLastSeenDays) { + this.bridgesByLastSeenDays = bridgesByLastSeenDays; + } + public SortedMap<Integer, Set<String>> getBridgesByLastSeenDays() { + return bridgesByLastSeenDays; + } +} + +public class NodeIndexer implements ServletContextListener, Runnable { + + public void contextInitialized(ServletContextEvent contextEvent) { + ServletContext servletContext = contextEvent.getServletContext(); + File outDir = new File(servletContext.getInitParameter("outDir")); + DocumentStore documentStore = ApplicationFactory.getDocumentStore(); + documentStore.setOutDir(outDir); + /* The servlet container created us, and we need to avoid that + * ApplicationFactory creates another instance of us. */ + ApplicationFactory.setNodeIndexer(this); + this.startIndexing(); + } + + public void contextDestroyed(ServletContextEvent contextEvent) { + this.stopIndexing(); + } + + private long lastIndexed = -1L; + + private NodeIndex latestNodeIndex = null; + + private Thread nodeIndexerThread = null; + + public synchronized long getLastIndexed(long timeoutMillis) { + if (this.lastIndexed == -1L && this.nodeIndexerThread != null && + timeoutMillis > 0L) { + try { + this.wait(timeoutMillis); + } catch (InterruptedException e) { + } + } + return this.lastIndexed; + } + + public synchronized NodeIndex getLatestNodeIndex(long timeoutMillis) { + if (this.latestNodeIndex == null && this.nodeIndexerThread != null && + timeoutMillis > 0L) { + try { + this.wait(timeoutMillis); + } catch (InterruptedException e) { + } + } + return this.latestNodeIndex; + } + + public synchronized void startIndexing() { + if (this.nodeIndexerThread == null) { + this.nodeIndexerThread = new Thread(this); + this.nodeIndexerThread.setDaemon(true); + this.nodeIndexerThread.start(); + } + } + + public void run() { + while (this.nodeIndexerThread != null) { + this.indexNodeStatuses(); + try { + Thread.sleep(DateTimeHelper.ONE_MINUTE); + } catch (InterruptedException e) { + } + } + } + + public synchronized void stopIndexing() { + Thread indexerThread = this.nodeIndexerThread; + this.nodeIndexerThread = null; + indexerThread.interrupt(); + } + + private void indexNodeStatuses() { + long updateStatusMillis = -1L; + DocumentStore documentStore = ApplicationFactory.getDocumentStore(); + UpdateStatus updateStatus = documentStore.retrieve(UpdateStatus.class, + false); + if (updateStatus != null && + updateStatus.getDocumentString() != null) { + String updateString = updateStatus.getDocumentString(); + try { + updateStatusMillis = Long.parseLong(updateString.trim()); + } catch (NumberFormatException e) { + /* Handle below. */ + } + } + synchronized (this) { + if (updateStatusMillis <= this.lastIndexed) { + return; + } + } + List<String> newRelaysByConsensusWeight = new ArrayList<String>(); + Map<String, SummaryDocument> + newRelayFingerprintSummaryLines = + new HashMap<String, SummaryDocument>(), + newBridgeFingerprintSummaryLines = + new HashMap<String, SummaryDocument>(); + Map<String, Set<String>> + newRelaysByCountryCode = new HashMap<String, Set<String>>(), + newRelaysByASNumber = new HashMap<String, Set<String>>(), + newRelaysByFlag = new HashMap<String, Set<String>>(), + newBridgesByFlag = new HashMap<String, Set<String>>(), + newRelaysByContact = new HashMap<String, Set<String>>(), + newRelaysByFamily = new HashMap<String, Set<String>>(); + SortedMap<Integer, Set<String>> + newRelaysByFirstSeenDays = new TreeMap<Integer, Set<String>>(), + newBridgesByFirstSeenDays = new TreeMap<Integer, Set<String>>(), + newRelaysByLastSeenDays = new TreeMap<Integer, Set<String>>(), + newBridgesByLastSeenDays = new TreeMap<Integer, Set<String>>(); + Set<SummaryDocument> currentRelays = new HashSet<SummaryDocument>(), + currentBridges = new HashSet<SummaryDocument>(); + SortedSet<String> fingerprints = documentStore.list( + SummaryDocument.class); + long relaysLastValidAfterMillis = 0L, bridgesLastPublishedMillis = 0L; + for (String fingerprint : fingerprints) { + SummaryDocument node = documentStore.retrieve(SummaryDocument.class, + true, fingerprint); + if (node.isRelay()) { + relaysLastValidAfterMillis = Math.max( + relaysLastValidAfterMillis, node.getLastSeenMillis()); + currentRelays.add(node); + } else { + bridgesLastPublishedMillis = Math.max( + bridgesLastPublishedMillis, node.getLastSeenMillis()); + currentBridges.add(node); + } + } + Time time = ApplicationFactory.getTime(); + List<String> orderRelaysByConsensusWeight = new ArrayList<String>(); + for (SummaryDocument entry : currentRelays) { + String fingerprint = entry.getFingerprint().toUpperCase(); + String hashedFingerprint = entry.getHashedFingerprint(). + toUpperCase(); + newRelayFingerprintSummaryLines.put(fingerprint, entry); + newRelayFingerprintSummaryLines.put(hashedFingerprint, entry); + long consensusWeight = entry.getConsensusWeight(); + orderRelaysByConsensusWeight.add(String.format("%020d %s", + consensusWeight, fingerprint)); + orderRelaysByConsensusWeight.add(String.format("%020d %s", + consensusWeight, hashedFingerprint)); + if (entry.getCountryCode() != null) { + String countryCode = entry.getCountryCode(); + if (!newRelaysByCountryCode.containsKey(countryCode)) { + newRelaysByCountryCode.put(countryCode, + new HashSet<String>()); + } + newRelaysByCountryCode.get(countryCode).add(fingerprint); + newRelaysByCountryCode.get(countryCode).add(hashedFingerprint); + } + if (entry.getASNumber() != null) { + String aSNumber = entry.getASNumber(); + if (!newRelaysByASNumber.containsKey(aSNumber)) { + newRelaysByASNumber.put(aSNumber, new HashSet<String>()); + } + newRelaysByASNumber.get(aSNumber).add(fingerprint); + newRelaysByASNumber.get(aSNumber).add(hashedFingerprint); + } + for (String flag : entry.getRelayFlags()) { + String flagLowerCase = flag.toLowerCase(); + if (!newRelaysByFlag.containsKey(flagLowerCase)) { + newRelaysByFlag.put(flagLowerCase, new HashSet<String>()); + } + newRelaysByFlag.get(flagLowerCase).add(fingerprint); + newRelaysByFlag.get(flagLowerCase).add(hashedFingerprint); + } + if (entry.getFamilyFingerprints() != null) { + newRelaysByFamily.put(fingerprint, entry.getFamilyFingerprints()); + } + int daysSinceFirstSeen = (int) ((time.currentTimeMillis() + - entry.getFirstSeenMillis()) / DateTimeHelper.ONE_DAY); + if (!newRelaysByFirstSeenDays.containsKey(daysSinceFirstSeen)) { + newRelaysByFirstSeenDays.put(daysSinceFirstSeen, + new HashSet<String>()); + } + newRelaysByFirstSeenDays.get(daysSinceFirstSeen).add(fingerprint); + newRelaysByFirstSeenDays.get(daysSinceFirstSeen).add( + hashedFingerprint); + int daysSinceLastSeen = (int) ((time.currentTimeMillis() + - entry.getLastSeenMillis()) / DateTimeHelper.ONE_DAY); + if (!newRelaysByLastSeenDays.containsKey(daysSinceLastSeen)) { + newRelaysByLastSeenDays.put(daysSinceLastSeen, + new HashSet<String>()); + } + newRelaysByLastSeenDays.get(daysSinceLastSeen).add(fingerprint); + newRelaysByLastSeenDays.get(daysSinceLastSeen).add( + hashedFingerprint); + String contact = entry.getContact(); + if (!newRelaysByContact.containsKey(contact)) { + newRelaysByContact.put(contact, new HashSet<String>()); + } + newRelaysByContact.get(contact).add(fingerprint); + newRelaysByContact.get(contact).add(hashedFingerprint); + } + Collections.sort(orderRelaysByConsensusWeight); + newRelaysByConsensusWeight = new ArrayList<String>(); + for (String relay : orderRelaysByConsensusWeight) { + newRelaysByConsensusWeight.add(relay.split(" ")[1]); + } + for (Map.Entry<String, Set<String>> e : + newRelaysByFamily.entrySet()) { + String fingerprint = e.getKey(); + Set<String> inMutualFamilyRelation = new HashSet<String>(); + for (String otherFingerprint : e.getValue()) { + if (newRelaysByFamily.containsKey(otherFingerprint) && + newRelaysByFamily.get(otherFingerprint).contains( + fingerprint)) { + inMutualFamilyRelation.add(otherFingerprint); + } + } + e.getValue().retainAll(inMutualFamilyRelation); + } + for (SummaryDocument entry : currentBridges) { + String hashedFingerprint = entry.getFingerprint().toUpperCase(); + String hashedHashedFingerprint = entry.getHashedFingerprint(). + toUpperCase(); + newBridgeFingerprintSummaryLines.put(hashedFingerprint, entry); + newBridgeFingerprintSummaryLines.put(hashedHashedFingerprint, + entry); + for (String flag : entry.getRelayFlags()) { + String flagLowerCase = flag.toLowerCase(); + if (!newBridgesByFlag.containsKey(flagLowerCase)) { + newBridgesByFlag.put(flagLowerCase, new HashSet<String>()); + } + newBridgesByFlag.get(flagLowerCase).add(hashedFingerprint); + newBridgesByFlag.get(flagLowerCase).add( + hashedHashedFingerprint); + } + int daysSinceFirstSeen = (int) ((time.currentTimeMillis() + - entry.getFirstSeenMillis()) / DateTimeHelper.ONE_DAY); + if (!newBridgesByFirstSeenDays.containsKey(daysSinceFirstSeen)) { + newBridgesByFirstSeenDays.put(daysSinceFirstSeen, + new HashSet<String>()); + } + newBridgesByFirstSeenDays.get(daysSinceFirstSeen).add( + hashedFingerprint); + newBridgesByFirstSeenDays.get(daysSinceFirstSeen).add( + hashedHashedFingerprint); + int daysSinceLastSeen = (int) ((time.currentTimeMillis() + - entry.getLastSeenMillis()) / DateTimeHelper.ONE_DAY); + if (!newBridgesByLastSeenDays.containsKey(daysSinceLastSeen)) { + newBridgesByLastSeenDays.put(daysSinceLastSeen, + new HashSet<String>()); + } + newBridgesByLastSeenDays.get(daysSinceLastSeen).add( + hashedFingerprint); + newBridgesByLastSeenDays.get(daysSinceLastSeen).add( + hashedHashedFingerprint); + } + NodeIndex newNodeIndex = new NodeIndex(); + newNodeIndex.setRelaysByConsensusWeight(newRelaysByConsensusWeight); + newNodeIndex.setRelayFingerprintSummaryLines( + newRelayFingerprintSummaryLines); + newNodeIndex.setBridgeFingerprintSummaryLines( + newBridgeFingerprintSummaryLines); + newNodeIndex.setRelaysByCountryCode(newRelaysByCountryCode); + newNodeIndex.setRelaysByASNumber(newRelaysByASNumber); + newNodeIndex.setRelaysByFlag(newRelaysByFlag); + newNodeIndex.setBridgesByFlag(newBridgesByFlag); + newNodeIndex.setRelaysByContact(newRelaysByContact); + newNodeIndex.setRelaysByFamily(newRelaysByFamily); + newNodeIndex.setRelaysByFirstSeenDays(newRelaysByFirstSeenDays); + newNodeIndex.setRelaysByLastSeenDays(newRelaysByLastSeenDays); + newNodeIndex.setBridgesByFirstSeenDays(newBridgesByFirstSeenDays); + newNodeIndex.setBridgesByLastSeenDays(newBridgesByLastSeenDays); + newNodeIndex.setRelaysPublishedString(DateTimeHelper.format( + relaysLastValidAfterMillis)); + newNodeIndex.setBridgesPublishedString(DateTimeHelper.format( + bridgesLastPublishedMillis)); + synchronized (this) { + this.lastIndexed = updateStatusMillis; + this.latestNodeIndex = newNodeIndex; + this.notifyAll(); + } + } +} + diff --git a/src/org/torproject/onionoo/server/RequestHandler.java b/src/org/torproject/onionoo/server/RequestHandler.java new file mode 100644 index 0000000..22e82fb --- /dev/null +++ b/src/org/torproject/onionoo/server/RequestHandler.java @@ -0,0 +1,552 @@ +/* Copyright 2011--2014 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.server; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; + +import org.torproject.onionoo.docs.DocumentStore; +import org.torproject.onionoo.docs.SummaryDocument; +import org.torproject.onionoo.util.ApplicationFactory; + +public class RequestHandler { + + private NodeIndex nodeIndex; + + private DocumentStore documentStore; + + public RequestHandler(NodeIndex nodeIndex) { + this.nodeIndex = nodeIndex; + this.documentStore = ApplicationFactory.getDocumentStore(); + } + + private String resourceType; + public void setResourceType(String resourceType) { + this.resourceType = resourceType; + } + + private String type; + public void setType(String type) { + this.type = type; + } + + private String running; + public void setRunning(String running) { + this.running = running; + } + + private String[] search; + public void setSearch(String[] search) { + this.search = new String[search.length]; + System.arraycopy(search, 0, this.search, 0, search.length); + } + + private String lookup; + public void setLookup(String lookup) { + this.lookup = lookup; + } + + private String fingerprint; + public void setFingerprint(String fingerprint) { + this.fingerprint = fingerprint; + } + + private String country; + public void setCountry(String country) { + this.country = country; + } + + private String as; + public void setAs(String as) { + this.as = as; + } + + private String flag; + public void setFlag(String flag) { + this.flag = flag; + } + + private String[] contact; + public void setContact(String[] contact) { + this.contact = new String[contact.length]; + System.arraycopy(contact, 0, this.contact, 0, contact.length); + } + + private String[] order; + public void setOrder(String[] order) { + this.order = new String[order.length]; + System.arraycopy(order, 0, this.order, 0, order.length); + } + + private String offset; + public void setOffset(String offset) { + this.offset = offset; + } + + private String limit; + public void setLimit(String limit) { + this.limit = limit; + } + + private int[] firstSeenDays; + public void setFirstSeenDays(int[] firstSeenDays) { + this.firstSeenDays = new int[firstSeenDays.length]; + System.arraycopy(firstSeenDays, 0, this.firstSeenDays, 0, + firstSeenDays.length); + } + + private int[] lastSeenDays; + public void setLastSeenDays(int[] lastSeenDays) { + this.lastSeenDays = new int[lastSeenDays.length]; + System.arraycopy(lastSeenDays, 0, this.lastSeenDays, 0, + lastSeenDays.length); + } + + private String family; + public void setFamily(String family) { + this.family = family; + } + + private Map<String, SummaryDocument> filteredRelays = + new HashMap<String, SummaryDocument>(); + + private Map<String, SummaryDocument> filteredBridges = + new HashMap<String, SummaryDocument>(); + + public void handleRequest() { + this.filteredRelays.putAll( + this.nodeIndex.getRelayFingerprintSummaryLines()); + this.filteredBridges.putAll( + this.nodeIndex.getBridgeFingerprintSummaryLines()); + this.filterByResourceType(); + this.filterByType(); + this.filterByRunning(); + this.filterBySearchTerms(); + this.filterByLookup(); + this.filterByFingerprint(); + this.filterByCountryCode(); + this.filterByASNumber(); + this.filterByFlag(); + this.filterNodesByFirstSeenDays(); + this.filterNodesByLastSeenDays(); + this.filterByContact(); + this.filterByFamily(); + this.order(); + this.offset(); + this.limit(); + } + + + private void filterByResourceType() { + if (this.resourceType.equals("clients")) { + this.filteredRelays.clear(); + } + if (this.resourceType.equals("weights")) { + this.filteredBridges.clear(); + } + } + + private void filterByType() { + if (this.type == null) { + return; + } else if (this.type.equals("relay")) { + this.filteredBridges.clear(); + } else { + this.filteredRelays.clear(); + } + } + + private void filterByRunning() { + if (this.running == null) { + return; + } + boolean runningRequested = this.running.equals("true"); + Set<String> removeRelays = new HashSet<String>(); + for (Map.Entry<String, SummaryDocument> e : + filteredRelays.entrySet()) { + if (e.getValue().isRunning() != runningRequested) { + removeRelays.add(e.getKey()); + } + } + for (String fingerprint : removeRelays) { + this.filteredRelays.remove(fingerprint); + } + Set<String> removeBridges = new HashSet<String>(); + for (Map.Entry<String, SummaryDocument> e : + filteredBridges.entrySet()) { + if (e.getValue().isRunning() != runningRequested) { + removeBridges.add(e.getKey()); + } + } + for (String fingerprint : removeBridges) { + this.filteredBridges.remove(fingerprint); + } + } + + private void filterBySearchTerms() { + if (this.search == null) { + return; + } + for (String searchTerm : this.search) { + filterBySearchTerm(searchTerm); + } + } + + private void filterBySearchTerm(String searchTerm) { + Set<String> removeRelays = new HashSet<String>(); + for (Map.Entry<String, SummaryDocument> e : + filteredRelays.entrySet()) { + String fingerprint = e.getKey(); + SummaryDocument entry = e.getValue(); + boolean lineMatches = false; + String nickname = entry.getNickname() != null ? + entry.getNickname().toLowerCase() : "unnamed"; + if (searchTerm.startsWith("$")) { + /* Search is for $-prefixed fingerprint. */ + if (fingerprint.startsWith( + searchTerm.substring(1).toUpperCase())) { + /* $-prefixed fingerprint matches. */ + lineMatches = true; + } + } else if (nickname.contains(searchTerm.toLowerCase())) { + /* Nickname matches. */ + lineMatches = true; + } else if (fingerprint.startsWith(searchTerm.toUpperCase())) { + /* Non-$-prefixed fingerprint matches. */ + lineMatches = true; + } else { + List<String> addresses = entry.getAddresses(); + for (String address : addresses) { + if (address.startsWith(searchTerm.toLowerCase())) { + /* Address matches. */ + lineMatches = true; + break; + } + } + } + if (!lineMatches) { + removeRelays.add(e.getKey()); + } + } + for (String fingerprint : removeRelays) { + this.filteredRelays.remove(fingerprint); + } + Set<String> removeBridges = new HashSet<String>(); + for (Map.Entry<String, SummaryDocument> e : + filteredBridges.entrySet()) { + String hashedFingerprint = e.getKey(); + SummaryDocument entry = e.getValue(); + boolean lineMatches = false; + String nickname = entry.getNickname() != null ? + entry.getNickname().toLowerCase() : "unnamed"; + if (searchTerm.startsWith("$")) { + /* Search is for $-prefixed hashed fingerprint. */ + if (hashedFingerprint.startsWith( + searchTerm.substring(1).toUpperCase())) { + /* $-prefixed hashed fingerprint matches. */ + lineMatches = true; + } + } else if (nickname.contains(searchTerm.toLowerCase())) { + /* Nickname matches. */ + lineMatches = true; + } else if (hashedFingerprint.startsWith(searchTerm.toUpperCase())) { + /* Non-$-prefixed hashed fingerprint matches. */ + lineMatches = true; + } + if (!lineMatches) { + removeBridges.add(e.getKey()); + } + } + for (String fingerprint : removeBridges) { + this.filteredBridges.remove(fingerprint); + } + } + + private void filterByLookup() { + if (this.lookup == null) { + return; + } + String fingerprint = this.lookup; + SummaryDocument relayLine = this.filteredRelays.get(fingerprint); + this.filteredRelays.clear(); + if (relayLine != null) { + this.filteredRelays.put(fingerprint, relayLine); + } + SummaryDocument bridgeLine = this.filteredBridges.get(fingerprint); + this.filteredBridges.clear(); + if (bridgeLine != null) { + this.filteredBridges.put(fingerprint, bridgeLine); + } + } + + private void filterByFingerprint() { + if (this.fingerprint == null) { + return; + } + this.filteredRelays.clear(); + this.filteredBridges.clear(); + String fingerprint = this.fingerprint; + SummaryDocument entry = this.documentStore.retrieve( + SummaryDocument.class, true, fingerprint); + if (entry != null) { + if (entry.isRelay()) { + this.filteredRelays.put(fingerprint, entry); + } else { + this.filteredBridges.put(fingerprint, entry); + } + } + } + + private void filterByCountryCode() { + if (this.country == null) { + return; + } + String countryCode = this.country.toLowerCase(); + if (!this.nodeIndex.getRelaysByCountryCode().containsKey( + countryCode)) { + this.filteredRelays.clear(); + } else { + Set<String> relaysWithCountryCode = + this.nodeIndex.getRelaysByCountryCode().get(countryCode); + Set<String> removeRelays = new HashSet<String>(); + for (String fingerprint : this.filteredRelays.keySet()) { + if (!relaysWithCountryCode.contains(fingerprint)) { + removeRelays.add(fingerprint); + } + } + for (String fingerprint : removeRelays) { + this.filteredRelays.remove(fingerprint); + } + } + this.filteredBridges.clear(); + } + + private void filterByASNumber() { + if (this.as == null) { + return; + } + String aSNumber = this.as.toUpperCase(); + if (!aSNumber.startsWith("AS")) { + aSNumber = "AS" + aSNumber; + } + if (!this.nodeIndex.getRelaysByASNumber().containsKey(aSNumber)) { + this.filteredRelays.clear(); + } else { + Set<String> relaysWithASNumber = + this.nodeIndex.getRelaysByASNumber().get(aSNumber); + Set<String> removeRelays = new HashSet<String>(); + for (String fingerprint : this.filteredRelays.keySet()) { + if (!relaysWithASNumber.contains(fingerprint)) { + removeRelays.add(fingerprint); + } + } + for (String fingerprint : removeRelays) { + this.filteredRelays.remove(fingerprint); + } + } + this.filteredBridges.clear(); + } + + private void filterByFlag() { + if (this.flag == null) { + return; + } + String flag = this.flag.toLowerCase(); + if (!this.nodeIndex.getRelaysByFlag().containsKey(flag)) { + this.filteredRelays.clear(); + } else { + Set<String> relaysWithFlag = this.nodeIndex.getRelaysByFlag().get( + flag); + Set<String> removeRelays = new HashSet<String>(); + for (String fingerprint : this.filteredRelays.keySet()) { + if (!relaysWithFlag.contains(fingerprint)) { + removeRelays.add(fingerprint); + } + } + for (String fingerprint : removeRelays) { + this.filteredRelays.remove(fingerprint); + } + } + if (!this.nodeIndex.getBridgesByFlag().containsKey(flag)) { + this.filteredBridges.clear(); + } else { + Set<String> bridgesWithFlag = this.nodeIndex.getBridgesByFlag().get( + flag); + Set<String> removeBridges = new HashSet<String>(); + for (String fingerprint : this.filteredBridges.keySet()) { + if (!bridgesWithFlag.contains(fingerprint)) { + removeBridges.add(fingerprint); + } + } + for (String fingerprint : removeBridges) { + this.filteredBridges.remove(fingerprint); + } + } + } + + private void filterNodesByFirstSeenDays() { + if (this.firstSeenDays == null) { + return; + } + filterNodesByDays(this.filteredRelays, + this.nodeIndex.getRelaysByFirstSeenDays(), this.firstSeenDays); + filterNodesByDays(this.filteredBridges, + this.nodeIndex.getBridgesByFirstSeenDays(), this.firstSeenDays); + } + + private void filterNodesByLastSeenDays() { + if (this.lastSeenDays == null) { + return; + } + filterNodesByDays(this.filteredRelays, + this.nodeIndex.getRelaysByLastSeenDays(), this.lastSeenDays); + filterNodesByDays(this.filteredBridges, + this.nodeIndex.getBridgesByLastSeenDays(), this.lastSeenDays); + } + + private void filterNodesByDays( + Map<String, SummaryDocument> filteredNodes, + SortedMap<Integer, Set<String>> nodesByDays, int[] days) { + Set<String> removeNodes = new HashSet<String>(); + for (Set<String> nodes : nodesByDays.headMap(days[0]).values()) { + removeNodes.addAll(nodes); + } + if (days[1] < Integer.MAX_VALUE) { + for (Set<String> nodes : + nodesByDays.tailMap(days[1] + 1).values()) { + removeNodes.addAll(nodes); + } + } + for (String fingerprint : removeNodes) { + filteredNodes.remove(fingerprint); + } + } + + private void filterByContact() { + if (this.contact == null) { + return; + } + Set<String> removeRelays = new HashSet<String>(); + for (Map.Entry<String, Set<String>> e : + this.nodeIndex.getRelaysByContact().entrySet()) { + String contact = e.getKey(); + for (String contactPart : this.contact) { + if (contact == null || + !contact.contains(contactPart.toLowerCase())) { + removeRelays.addAll(e.getValue()); + break; + } + } + } + for (String fingerprint : removeRelays) { + this.filteredRelays.remove(fingerprint); + } + this.filteredBridges.clear(); + } + + private void filterByFamily() { + if (this.family == null) { + return; + } + Set<String> removeRelays = new HashSet<String>( + this.filteredRelays.keySet()); + removeRelays.remove(this.family); + if (this.nodeIndex.getRelaysByFamily().containsKey(this.family)) { + removeRelays.removeAll(this.nodeIndex.getRelaysByFamily(). + get(this.family)); + } + for (String fingerprint : removeRelays) { + this.filteredRelays.remove(fingerprint); + } + this.filteredBridges.clear(); + } + + private void order() { + if (this.order != null && this.order.length == 1) { + List<String> orderBy = new ArrayList<String>( + this.nodeIndex.getRelaysByConsensusWeight()); + if (this.order[0].startsWith("-")) { + Collections.reverse(orderBy); + } + for (String relay : orderBy) { + if (this.filteredRelays.containsKey(relay) && + !this.orderedRelays.contains(filteredRelays.get(relay))) { + this.orderedRelays.add(this.filteredRelays.remove(relay)); + } + } + for (String relay : this.filteredRelays.keySet()) { + if (!this.orderedRelays.contains(this.filteredRelays.get(relay))) { + this.orderedRelays.add(this.filteredRelays.remove(relay)); + } + } + Set<SummaryDocument> uniqueBridges = new HashSet<SummaryDocument>( + this.filteredBridges.values()); + this.orderedBridges.addAll(uniqueBridges); + } else { + Set<SummaryDocument> uniqueRelays = new HashSet<SummaryDocument>( + this.filteredRelays.values()); + this.orderedRelays.addAll(uniqueRelays); + Set<SummaryDocument> uniqueBridges = new HashSet<SummaryDocument>( + this.filteredBridges.values()); + this.orderedBridges.addAll(uniqueBridges); + } + } + + private void offset() { + if (this.offset == null) { + return; + } + int offsetValue = Integer.parseInt(this.offset); + while (offsetValue-- > 0 && + (!this.orderedRelays.isEmpty() || + !this.orderedBridges.isEmpty())) { + if (!this.orderedRelays.isEmpty()) { + this.orderedRelays.remove(0); + } else { + this.orderedBridges.remove(0); + } + } + } + + private void limit() { + if (this.limit == null) { + return; + } + int limitValue = Integer.parseInt(this.limit); + while (!this.orderedRelays.isEmpty() && + limitValue < this.orderedRelays.size()) { + this.orderedRelays.remove(this.orderedRelays.size() - 1); + } + limitValue -= this.orderedRelays.size(); + while (!this.orderedBridges.isEmpty() && + limitValue < this.orderedBridges.size()) { + this.orderedBridges.remove(this.orderedBridges.size() - 1); + } + } + + private List<SummaryDocument> orderedRelays = + new ArrayList<SummaryDocument>(); + public List<SummaryDocument> getOrderedRelays() { + return this.orderedRelays; + } + + private List<SummaryDocument> orderedBridges = + new ArrayList<SummaryDocument>(); + public List<SummaryDocument> getOrderedBridges() { + return this.orderedBridges; + } + + public String getRelaysPublishedString() { + return this.nodeIndex.getRelaysPublishedString(); + } + + public String getBridgesPublishedString() { + return this.nodeIndex.getBridgesPublishedString(); + } +} diff --git a/src/org/torproject/onionoo/server/ResourceServlet.java b/src/org/torproject/onionoo/server/ResourceServlet.java new file mode 100644 index 0000000..1e05d12 --- /dev/null +++ b/src/org/torproject/onionoo/server/ResourceServlet.java @@ -0,0 +1,451 @@ +/* Copyright 2011, 2012 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.server; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.torproject.onionoo.util.ApplicationFactory; +import org.torproject.onionoo.util.DateTimeHelper; + +public class ResourceServlet extends HttpServlet { + + private static final long serialVersionUID = 7236658979947465319L; + + private boolean maintenanceMode = false; + + /* Called by servlet container, not by test class. */ + public void init(ServletConfig config) throws ServletException { + super.init(config); + this.maintenanceMode = + config.getInitParameter("maintenance") != null && + config.getInitParameter("maintenance").equals("1"); + } + + public long getLastModified(HttpServletRequest request) { + if (this.maintenanceMode) { + return super.getLastModified(request); + } else { + return ApplicationFactory.getNodeIndexer().getLastIndexed( + DateTimeHelper.TEN_SECONDS); + } + } + + public static class HttpServletRequestWrapper { + private HttpServletRequest request; + protected HttpServletRequestWrapper(HttpServletRequest request) { + this.request = request; + } + protected String getRequestURI() { + return this.request.getRequestURI(); + } + @SuppressWarnings("rawtypes") + protected Map getParameterMap() { + return this.request.getParameterMap(); + } + protected String[] getParameterValues(String parameterKey) { + return this.request.getParameterValues(parameterKey); + } + } + + public static class HttpServletResponseWrapper { + private HttpServletResponse response = null; + protected HttpServletResponseWrapper(HttpServletResponse response) { + this.response = response; + } + protected void sendError(int errorStatusCode) throws IOException { + this.response.sendError(errorStatusCode); + } + protected void setHeader(String headerName, String headerValue) { + this.response.setHeader(headerName, headerValue); + } + protected void setContentType(String contentType) { + this.response.setContentType(contentType); + } + protected void setCharacterEncoding(String characterEncoding) { + this.response.setCharacterEncoding(characterEncoding); + } + protected PrintWriter getWriter() throws IOException { + return this.response.getWriter(); + } + } + + public void doGet(HttpServletRequest request, + HttpServletResponse response) throws IOException, ServletException { + HttpServletRequestWrapper requestWrapper = + new HttpServletRequestWrapper(request); + HttpServletResponseWrapper responseWrapper = + new HttpServletResponseWrapper(response); + this.doGet(requestWrapper, responseWrapper); + } + + public void doGet(HttpServletRequestWrapper request, + HttpServletResponseWrapper response) throws IOException { + + if (this.maintenanceMode) { + response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); + return; + } + + if (ApplicationFactory.getNodeIndexer().getLastIndexed( + DateTimeHelper.TEN_SECONDS) + DateTimeHelper.SIX_HOURS + < ApplicationFactory.getTime().currentTimeMillis()) { + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + return; + } + + NodeIndex nodeIndex = ApplicationFactory.getNodeIndexer(). + getLatestNodeIndex(DateTimeHelper.TEN_SECONDS); + if (nodeIndex == null) { + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + return; + } + + String uri = request.getRequestURI(); + if (uri.startsWith("/onionoo/")) { + uri = uri.substring("/onionoo".length()); + } + String resourceType = null; + if (uri.startsWith("/summary")) { + resourceType = "summary"; + } else if (uri.startsWith("/details")) { + resourceType = "details"; + } else if (uri.startsWith("/bandwidth")) { + resourceType = "bandwidth"; + } else if (uri.startsWith("/weights")) { + resourceType = "weights"; + } else if (uri.startsWith("/clients")) { + resourceType = "clients"; + } else if (uri.startsWith("/uptime")) { + resourceType = "uptime"; + } else { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + + RequestHandler rh = new RequestHandler(nodeIndex); + rh.setResourceType(resourceType); + + /* Extract parameters either from the old-style URI or from request + * parameters. */ + Map<String, String> parameterMap = new HashMap<String, String>(); + for (Object parameterKey : request.getParameterMap().keySet()) { + String[] parameterValues = + request.getParameterValues((String) parameterKey); + parameterMap.put((String) parameterKey, parameterValues[0]); + } + + /* Make sure that the request doesn't contain any unknown + * parameters. */ + Set<String> knownParameters = new HashSet<String>(Arrays.asList(( + "type,running,search,lookup,fingerprint,country,as,flag," + + "first_seen_days,last_seen_days,contact,order,limit,offset," + + "fields,family").split(","))); + for (String parameterKey : parameterMap.keySet()) { + if (!knownParameters.contains(parameterKey)) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + } + + /* Filter relays and bridges matching the request. */ + if (parameterMap.containsKey("type")) { + String typeParameterValue = parameterMap.get("type").toLowerCase(); + boolean relaysRequested = true; + if (typeParameterValue.equals("bridge")) { + relaysRequested = false; + } else if (!typeParameterValue.equals("relay")) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + rh.setType(relaysRequested ? "relay" : "bridge"); + } + if (parameterMap.containsKey("running")) { + String runningParameterValue = + parameterMap.get("running").toLowerCase(); + boolean runningRequested = true; + if (runningParameterValue.equals("false")) { + runningRequested = false; + } else if (!runningParameterValue.equals("true")) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + rh.setRunning(runningRequested ? "true" : "false"); + } + if (parameterMap.containsKey("search")) { + String[] searchTerms = this.parseSearchParameters( + parameterMap.get("search")); + if (searchTerms == null) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + rh.setSearch(searchTerms); + } + if (parameterMap.containsKey("lookup")) { + String lookupParameter = this.parseFingerprintParameter( + parameterMap.get("lookup")); + if (lookupParameter == null) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + String fingerprint = lookupParameter.toUpperCase(); + rh.setLookup(fingerprint); + } + if (parameterMap.containsKey("fingerprint")) { + String fingerprintParameter = this.parseFingerprintParameter( + parameterMap.get("fingerprint")); + if (fingerprintParameter == null) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + String fingerprint = fingerprintParameter.toUpperCase(); + rh.setFingerprint(fingerprint); + } + if (parameterMap.containsKey("country")) { + String countryCodeParameter = this.parseCountryCodeParameter( + parameterMap.get("country")); + if (countryCodeParameter == null) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + rh.setCountry(countryCodeParameter); + } + if (parameterMap.containsKey("as")) { + String aSNumberParameter = this.parseASNumberParameter( + parameterMap.get("as")); + if (aSNumberParameter == null) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + rh.setAs(aSNumberParameter); + } + if (parameterMap.containsKey("flag")) { + String flagParameter = this.parseFlagParameter( + parameterMap.get("flag")); + if (flagParameter == null) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + rh.setFlag(flagParameter); + } + if (parameterMap.containsKey("first_seen_days")) { + int[] days = this.parseDaysParameter( + parameterMap.get("first_seen_days")); + if (days == null) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + rh.setFirstSeenDays(days); + } + if (parameterMap.containsKey("last_seen_days")) { + int[] days = this.parseDaysParameter( + parameterMap.get("last_seen_days")); + if (days == null) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + rh.setLastSeenDays(days); + } + if (parameterMap.containsKey("contact")) { + String[] contactParts = this.parseContactParameter( + parameterMap.get("contact")); + if (contactParts == null) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + rh.setContact(contactParts); + } + if (parameterMap.containsKey("order")) { + String orderParameter = parameterMap.get("order").toLowerCase(); + String orderByField = orderParameter; + if (orderByField.startsWith("-")) { + orderByField = orderByField.substring(1); + } + if (!orderByField.equals("consensus_weight")) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + rh.setOrder(new String[] { orderParameter }); + } + if (parameterMap.containsKey("offset")) { + String offsetParameter = parameterMap.get("offset"); + if (offsetParameter.length() > 6) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + try { + Integer.parseInt(offsetParameter); + } catch (NumberFormatException e) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + rh.setOffset(offsetParameter); + } + if (parameterMap.containsKey("limit")) { + String limitParameter = parameterMap.get("limit"); + if (limitParameter.length() > 6) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + try { + Integer.parseInt(limitParameter); + } catch (NumberFormatException e) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + rh.setLimit(limitParameter); + } + if (parameterMap.containsKey("family")) { + String familyParameter = this.parseFingerprintParameter( + parameterMap.get("family")); + if (familyParameter == null) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + String family = familyParameter.toUpperCase(); + rh.setFamily(family); + } + rh.handleRequest(); + + ResponseBuilder rb = new ResponseBuilder(); + rb.setResourceType(resourceType); + rb.setRelaysPublishedString(rh.getRelaysPublishedString()); + rb.setBridgesPublishedString(rh.getBridgesPublishedString()); + rb.setOrderedRelays(rh.getOrderedRelays()); + rb.setOrderedBridges(rh.getOrderedBridges()); + String[] fields = null; + if (parameterMap.containsKey("fields")) { + fields = this.parseFieldsParameter(parameterMap.get("fields")); + if (fields == null) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + rb.setFields(fields); + } + + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setContentType("application/json"); + response.setCharacterEncoding("utf-8"); + PrintWriter pw = response.getWriter(); + rb.buildResponse(pw); + pw.flush(); + pw.close(); + } + + private static Pattern searchParameterPattern = + Pattern.compile("^\\$?[0-9a-fA-F]{1,40}$|" /* Fingerprint. */ + + "^[0-9a-zA-Z\\.]{1,19}$|" /* Nickname or IPv4 address. */ + + "^\\[[0-9a-fA-F:\\.]{1,39}\\]?$"); /* IPv6 address. */ + private String[] parseSearchParameters(String parameter) { + String[] searchParameters; + if (parameter.contains(" ")) { + searchParameters = parameter.split(" "); + } else { + searchParameters = new String[] { parameter }; + } + for (String searchParameter : searchParameters) { + if (!searchParameterPattern.matcher(searchParameter).matches()) { + return null; + } + } + return searchParameters; + } + + private static Pattern fingerprintParameterPattern = + Pattern.compile("^[0-9a-zA-Z]{1,40}$"); + private String parseFingerprintParameter(String parameter) { + if (!fingerprintParameterPattern.matcher(parameter).matches()) { + return null; + } + if (parameter.length() != 40) { + return null; + } + return parameter; + } + + private static Pattern countryCodeParameterPattern = + Pattern.compile("^[0-9a-zA-Z]{2}$"); + private String parseCountryCodeParameter(String parameter) { + if (!countryCodeParameterPattern.matcher(parameter).matches()) { + return null; + } + return parameter; + } + + private static Pattern aSNumberParameterPattern = + Pattern.compile("^[asAS]{0,2}[0-9]{1,10}$"); + private String parseASNumberParameter(String parameter) { + if (!aSNumberParameterPattern.matcher(parameter).matches()) { + return null; + } + return parameter; + } + + private static Pattern flagPattern = + Pattern.compile("^[a-zA-Z0-9]{1,20}$"); + private String parseFlagParameter(String parameter) { + if (!flagPattern.matcher(parameter).matches()) { + return null; + } + return parameter; + } + + private static Pattern daysPattern = Pattern.compile("^[0-9-]{1,10}$"); + private int[] parseDaysParameter(String parameter) { + if (!daysPattern.matcher(parameter).matches()) { + return null; + } + int x = 0, y = Integer.MAX_VALUE; + try { + if (!parameter.contains("-")) { + x = Integer.parseInt(parameter); + y = x; + } else { + String[] parts = parameter.split("-", 2); + if (parts[0].length() > 0) { + x = Integer.parseInt(parts[0]); + } + if (parts.length > 1 && parts[1].length() > 0) { + y = Integer.parseInt(parts[1]); + } + } + } catch (NumberFormatException e) { + return null; + } + if (x > y) { + return null; + } + return new int[] { x, y }; + } + + private String[] parseContactParameter(String parameter) { + for (char c : parameter.toCharArray()) { + if (c < 32 || c >= 127) { + return null; + } + } + return parameter.split(" "); + } + + private static Pattern fieldsParameterPattern = + Pattern.compile("^[0-9a-zA-Z_,]*$"); + private String[] parseFieldsParameter(String parameter) { + if (!fieldsParameterPattern.matcher(parameter).matches()) { + return null; + } + return parameter.split(","); + } +} + diff --git a/src/org/torproject/onionoo/server/ResponseBuilder.java b/src/org/torproject/onionoo/server/ResponseBuilder.java new file mode 100644 index 0000000..161692c --- /dev/null +++ b/src/org/torproject/onionoo/server/ResponseBuilder.java @@ -0,0 +1,320 @@ +/* Copyright 2011--2013 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.server; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +import org.torproject.onionoo.docs.BandwidthDocument; +import org.torproject.onionoo.docs.ClientsDocument; +import org.torproject.onionoo.docs.DetailsDocument; +import org.torproject.onionoo.docs.DocumentStore; +import org.torproject.onionoo.docs.SummaryDocument; +import org.torproject.onionoo.docs.UptimeDocument; +import org.torproject.onionoo.docs.WeightsDocument; +import org.torproject.onionoo.util.ApplicationFactory; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +public class ResponseBuilder { + + private DocumentStore documentStore; + + public ResponseBuilder() { + this.documentStore = ApplicationFactory.getDocumentStore(); + } + + private String resourceType; + public void setResourceType(String resourceType) { + this.resourceType = resourceType; + } + + private String relaysPublishedString; + public void setRelaysPublishedString(String relaysPublishedString) { + this.relaysPublishedString = relaysPublishedString; + } + + private String bridgesPublishedString; + public void setBridgesPublishedString(String bridgesPublishedString) { + this.bridgesPublishedString = bridgesPublishedString; + } + + private List<SummaryDocument> orderedRelays = + new ArrayList<SummaryDocument>(); + public void setOrderedRelays(List<SummaryDocument> orderedRelays) { + this.orderedRelays = orderedRelays; + } + + private List<SummaryDocument> orderedBridges = + new ArrayList<SummaryDocument>(); + public void setOrderedBridges(List<SummaryDocument> orderedBridges) { + this.orderedBridges = orderedBridges; + } + + private String[] fields; + public void setFields(String[] fields) { + this.fields = new String[fields.length]; + System.arraycopy(fields, 0, this.fields, 0, fields.length); + } + + public void buildResponse(PrintWriter pw) { + writeRelays(orderedRelays, pw); + writeBridges(orderedBridges, pw); + } + + private void writeRelays(List<SummaryDocument> relays, PrintWriter pw) { + pw.write("{\"relays_published\":\"" + relaysPublishedString + + "\",\n\"relays\":["); + int written = 0; + for (SummaryDocument entry : relays) { + String lines = this.formatNodeStatus(entry); + if (lines.length() > 0) { + pw.print((written++ > 0 ? ",\n" : "\n") + lines); + } + } + pw.print("\n],\n"); + } + + private void writeBridges(List<SummaryDocument> bridges, + PrintWriter pw) { + pw.write("\"bridges_published\":\"" + bridgesPublishedString + + "\",\n\"bridges\":["); + int written = 0; + for (SummaryDocument entry : bridges) { + String lines = this.formatNodeStatus(entry); + if (lines.length() > 0) { + pw.print((written++ > 0 ? ",\n" : "\n") + lines); + } + } + pw.print("\n]}\n"); + } + + private String formatNodeStatus(SummaryDocument entry) { + if (this.resourceType == null) { + return ""; + } else if (this.resourceType.equals("summary")) { + return this.writeSummaryLine(entry); + } else if (this.resourceType.equals("details")) { + return this.writeDetailsLines(entry); + } else if (this.resourceType.equals("bandwidth")) { + return this.writeBandwidthLines(entry); + } else if (this.resourceType.equals("weights")) { + return this.writeWeightsLines(entry); + } else if (this.resourceType.equals("clients")) { + return this.writeClientsLines(entry); + } else if (this.resourceType.equals("uptime")) { + return this.writeUptimeLines(entry); + } else { + return ""; + } + } + + private String writeSummaryLine(SummaryDocument entry) { + return entry.isRelay() ? writeRelaySummaryLine(entry) + : writeBridgeSummaryLine(entry); + } + + private String writeRelaySummaryLine(SummaryDocument entry) { + String nickname = !entry.getNickname().equals("Unnamed") ? + entry.getNickname() : null; + String fingerprint = entry.getFingerprint(); + String running = entry.isRunning() ? "true" : "false"; + List<String> addresses = entry.getAddresses(); + StringBuilder addressesBuilder = new StringBuilder(); + int written = 0; + for (String address : addresses) { + addressesBuilder.append((written++ > 0 ? "," : "") + "\"" + + address.toLowerCase() + "\""); + } + return String.format("{%s\"f\":\"%s\",\"a\":[%s],\"r\":%s}", + (nickname == null ? "" : "\"n\":\"" + nickname + "\","), + fingerprint, addressesBuilder.toString(), running); + } + + private String writeBridgeSummaryLine(SummaryDocument entry) { + String nickname = !entry.getNickname().equals("Unnamed") ? + entry.getNickname() : null; + String hashedFingerprint = entry.getFingerprint(); + String running = entry.isRunning() ? "true" : "false"; + return String.format("{%s\"h\":\"%s\",\"r\":%s}", + (nickname == null ? "" : "\"n\":\"" + nickname + "\","), + hashedFingerprint, running); + } + + private String writeDetailsLines(SummaryDocument entry) { + String fingerprint = entry.getFingerprint(); + if (this.fields != null) { + /* TODO Maybe there's a more elegant way (more maintainable, more + * efficient, etc.) to implement this? */ + DetailsDocument detailsDocument = documentStore.retrieve( + DetailsDocument.class, true, fingerprint); + if (detailsDocument != null) { + DetailsDocument dd = new DetailsDocument(); + for (String field : this.fields) { + if (field.equals("nickname")) { + dd.setNickname(detailsDocument.getNickname()); + } else if (field.equals("fingerprint")) { + dd.setFingerprint(detailsDocument.getFingerprint()); + } else if (field.equals("hashed_fingerprint")) { + dd.setHashedFingerprint( + detailsDocument.getHashedFingerprint()); + } else if (field.equals("or_addresses")) { + dd.setOrAddresses(detailsDocument.getOrAddresses()); + } else if (field.equals("exit_addresses")) { + dd.setExitAddresses(detailsDocument.getExitAddresses()); + } else if (field.equals("dir_address")) { + dd.setDirAddress(detailsDocument.getDirAddress()); + } else if (field.equals("last_seen")) { + dd.setLastSeen(detailsDocument.getLastSeen()); + } else if (field.equals("last_changed_address_or_port")) { + dd.setLastChangedAddressOrPort( + detailsDocument.getLastChangedAddressOrPort()); + } else if (field.equals("first_seen")) { + dd.setFirstSeen(detailsDocument.getFirstSeen()); + } else if (field.equals("running")) { + dd.setRunning(detailsDocument.getRunning()); + } else if (field.equals("flags")) { + dd.setFlags(detailsDocument.getFlags()); + } else if (field.equals("country")) { + dd.setCountry(detailsDocument.getCountry()); + } else if (field.equals("country_name")) { + dd.setCountryName(detailsDocument.getCountryName()); + } else if (field.equals("region_name")) { + dd.setRegionName(detailsDocument.getRegionName()); + } else if (field.equals("city_name")) { + dd.setCityName(detailsDocument.getCityName()); + } else if (field.equals("latitude")) { + dd.setLatitude(detailsDocument.getLatitude()); + } else if (field.equals("longitude")) { + dd.setLongitude(detailsDocument.getLongitude()); + } else if (field.equals("as_number")) { + dd.setAsNumber(detailsDocument.getAsNumber()); + } else if (field.equals("as_name")) { + dd.setAsName(detailsDocument.getAsName()); + } else if (field.equals("consensus_weight")) { + dd.setConsensusWeight(detailsDocument.getConsensusWeight()); + } else if (field.equals("host_name")) { + dd.setHostName(detailsDocument.getHostName()); + } else if (field.equals("last_restarted")) { + dd.setLastRestarted(detailsDocument.getLastRestarted()); + } else if (field.equals("bandwidth_rate")) { + dd.setBandwidthRate(detailsDocument.getBandwidthRate()); + } else if (field.equals("bandwidth_burst")) { + dd.setBandwidthBurst(detailsDocument.getBandwidthBurst()); + } else if (field.equals("observed_bandwidth")) { + dd.setObservedBandwidth( + detailsDocument.getObservedBandwidth()); + } else if (field.equals("advertised_bandwidth")) { + dd.setAdvertisedBandwidth( + detailsDocument.getAdvertisedBandwidth()); + } else if (field.equals("exit_policy")) { + dd.setExitPolicy(detailsDocument.getExitPolicy()); + } else if (field.equals("exit_policy_summary")) { + dd.setExitPolicySummary( + detailsDocument.getExitPolicySummary()); + } else if (field.equals("exit_policy_v6_summary")) { + dd.setExitPolicyV6Summary( + detailsDocument.getExitPolicyV6Summary()); + } else if (field.equals("contact")) { + dd.setContact(detailsDocument.getContact()); + } else if (field.equals("platform")) { + dd.setPlatform(detailsDocument.getPlatform()); + } else if (field.equals("family")) { + dd.setFamily(detailsDocument.getFamily()); + } else if (field.equals("advertised_bandwidth_fraction")) { + dd.setAdvertisedBandwidthFraction( + detailsDocument.getAdvertisedBandwidthFraction()); + } else if (field.equals("consensus_weight_fraction")) { + dd.setConsensusWeightFraction( + detailsDocument.getConsensusWeightFraction()); + } else if (field.equals("guard_probability")) { + dd.setGuardProbability(detailsDocument.getGuardProbability()); + } else if (field.equals("middle_probability")) { + dd.setMiddleProbability( + detailsDocument.getMiddleProbability()); + } else if (field.equals("exit_probability")) { + dd.setExitProbability(detailsDocument.getExitProbability()); + } else if (field.equals("recommended_version")) { + dd.setRecommendedVersion( + detailsDocument.getRecommendedVersion()); + } else if (field.equals("hibernating")) { + dd.setHibernating(detailsDocument.getHibernating()); + } else if (field.equals("pool_assignment")) { + dd.setPoolAssignment(detailsDocument.getPoolAssignment()); + } + } + /* Don't escape HTML characters, like < and >, contained in + * strings. */ + Gson gson = new GsonBuilder().disableHtmlEscaping().create(); + /* Whenever we provide Gson with a string containing an escaped + * non-ASCII character like \u00F2, it escapes the \ to \\, which + * we need to undo before including the string in a response. */ + return gson.toJson(dd).replaceAll("\\\\\\\\u", "\\\\u"); + } else { + // TODO We should probably log that we didn't find a details + // document that we expected to exist. + return ""; + } + } else { + DetailsDocument detailsDocument = documentStore.retrieve( + DetailsDocument.class, false, fingerprint); + if (detailsDocument != null) { + return detailsDocument.getDocumentString(); + } else { + // TODO We should probably log that we didn't find a details + // document that we expected to exist. + return ""; + } + } + } + + private String writeBandwidthLines(SummaryDocument entry) { + String fingerprint = entry.getFingerprint(); + BandwidthDocument bandwidthDocument = this.documentStore.retrieve( + BandwidthDocument.class, false, fingerprint); + if (bandwidthDocument != null && + bandwidthDocument.getDocumentString() != null) { + return bandwidthDocument.getDocumentString(); + } else { + return "{\"fingerprint\":\"" + fingerprint.toUpperCase() + "\"}"; + } + } + + private String writeWeightsLines(SummaryDocument entry) { + String fingerprint = entry.getFingerprint(); + WeightsDocument weightsDocument = this.documentStore.retrieve( + WeightsDocument.class, false, fingerprint); + if (weightsDocument != null && + weightsDocument.getDocumentString() != null) { + return weightsDocument.getDocumentString(); + } else { + return "{\"fingerprint\":\"" + fingerprint.toUpperCase() + "\"}"; + } + } + + private String writeClientsLines(SummaryDocument entry) { + String fingerprint = entry.getFingerprint(); + ClientsDocument clientsDocument = this.documentStore.retrieve( + ClientsDocument.class, false, fingerprint); + if (clientsDocument != null && + clientsDocument.getDocumentString() != null) { + return clientsDocument.getDocumentString(); + } else { + return "{\"fingerprint\":\"" + fingerprint.toUpperCase() + "\"}"; + } + } + + private String writeUptimeLines(SummaryDocument entry) { + String fingerprint = entry.getFingerprint(); + UptimeDocument uptimeDocument = this.documentStore.retrieve( + UptimeDocument.class, false, fingerprint); + if (uptimeDocument != null && + uptimeDocument.getDocumentString() != null) { + return uptimeDocument.getDocumentString(); + } else { + return "{\"fingerprint\":\"" + fingerprint.toUpperCase() + "\"}"; + } + } +} diff --git a/src/org/torproject/onionoo/updater/BandwidthStatusUpdater.java b/src/org/torproject/onionoo/updater/BandwidthStatusUpdater.java new file mode 100644 index 0000000..bc7dd74 --- /dev/null +++ b/src/org/torproject/onionoo/updater/BandwidthStatusUpdater.java @@ -0,0 +1,149 @@ +/* Copyright 2011--2014 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.updater; + +import java.util.SortedMap; +import java.util.TreeMap; + +import org.torproject.descriptor.Descriptor; +import org.torproject.descriptor.ExtraInfoDescriptor; +import org.torproject.onionoo.docs.BandwidthStatus; +import org.torproject.onionoo.docs.DocumentStore; +import org.torproject.onionoo.util.ApplicationFactory; +import org.torproject.onionoo.util.DateTimeHelper; + +public class BandwidthStatusUpdater implements DescriptorListener, + StatusUpdater { + + private DescriptorSource descriptorSource; + + private DocumentStore documentStore; + + private long now; + + public BandwidthStatusUpdater() { + this.descriptorSource = ApplicationFactory.getDescriptorSource(); + this.documentStore = ApplicationFactory.getDocumentStore(); + this.now = ApplicationFactory.getTime().currentTimeMillis(); + this.registerDescriptorListeners(); + } + + private void registerDescriptorListeners() { + this.descriptorSource.registerDescriptorListener(this, + DescriptorType.RELAY_EXTRA_INFOS); + this.descriptorSource.registerDescriptorListener(this, + DescriptorType.BRIDGE_EXTRA_INFOS); + } + + public void processDescriptor(Descriptor descriptor, boolean relay) { + if (descriptor instanceof ExtraInfoDescriptor) { + this.parseDescriptor((ExtraInfoDescriptor) descriptor); + } + } + + public void updateStatuses() { + /* Status files are already updated while processing descriptors. */ + } + + private void parseDescriptor(ExtraInfoDescriptor descriptor) { + String fingerprint = descriptor.getFingerprint(); + BandwidthStatus bandwidthStatus = this.documentStore.retrieve( + BandwidthStatus.class, true, fingerprint); + if (bandwidthStatus == null) { + bandwidthStatus = new BandwidthStatus(); + } + if (descriptor.getWriteHistory() != null) { + parseHistoryLine(descriptor.getWriteHistory().getLine(), + bandwidthStatus.getWriteHistory()); + } + if (descriptor.getReadHistory() != null) { + parseHistoryLine(descriptor.getReadHistory().getLine(), + bandwidthStatus.getReadHistory()); + } + this.compressHistory(bandwidthStatus.getWriteHistory()); + this.compressHistory(bandwidthStatus.getReadHistory()); + this.documentStore.store(bandwidthStatus, fingerprint); + } + + private void parseHistoryLine(String line, + SortedMap<Long, long[]> history) { + String[] parts = line.split(" "); + if (parts.length < 6) { + return; + } + long endMillis = DateTimeHelper.parse(parts[1] + " " + parts[2]); + if (endMillis < 0L) { + System.err.println("Could not parse timestamp in line '" + line + + "'. Skipping."); + return; + } + long intervalMillis = Long.parseLong(parts[3].substring(1)) + * DateTimeHelper.ONE_SECOND; + String[] values = parts[5].split(","); + for (int i = values.length - 1; i >= 0; i--) { + long bandwidthValue = Long.parseLong(values[i]); + long startMillis = endMillis - intervalMillis; + /* TODO Should we first check whether an interval is already + * contained in history? */ + history.put(startMillis, new long[] { startMillis, endMillis, + bandwidthValue }); + endMillis -= intervalMillis; + } + } + + private void compressHistory(SortedMap<Long, long[]> history) { + SortedMap<Long, long[]> uncompressedHistory = + new TreeMap<Long, long[]>(history); + history.clear(); + long lastStartMillis = 0L, lastEndMillis = 0L, lastBandwidth = 0L; + String lastMonthString = "1970-01"; + for (long[] v : uncompressedHistory.values()) { + long startMillis = v[0], endMillis = v[1], bandwidth = v[2]; + long intervalLengthMillis; + if (this.now - endMillis <= DateTimeHelper.THREE_DAYS) { + intervalLengthMillis = DateTimeHelper.FIFTEEN_MINUTES; + } else if (this.now - endMillis <= DateTimeHelper.ONE_WEEK) { + intervalLengthMillis = DateTimeHelper.ONE_HOUR; + } else if (this.now - endMillis <= + DateTimeHelper.ROUGHLY_ONE_MONTH) { + intervalLengthMillis = DateTimeHelper.FOUR_HOURS; + } else if (this.now - endMillis <= + DateTimeHelper.ROUGHLY_THREE_MONTHS) { + intervalLengthMillis = DateTimeHelper.TWELVE_HOURS; + } else if (this.now - endMillis <= + DateTimeHelper.ROUGHLY_ONE_YEAR) { + intervalLengthMillis = DateTimeHelper.TWO_DAYS; + } else { + intervalLengthMillis = DateTimeHelper.TEN_DAYS; + } + String monthString = DateTimeHelper.format(startMillis, + DateTimeHelper.ISO_YEARMONTH_FORMAT); + if (lastEndMillis == startMillis && + ((lastEndMillis - 1L) / intervalLengthMillis) == + ((endMillis - 1L) / intervalLengthMillis) && + lastMonthString.equals(monthString)) { + lastEndMillis = endMillis; + lastBandwidth += bandwidth; + } else { + if (lastStartMillis > 0L) { + history.put(lastStartMillis, new long[] { lastStartMillis, + lastEndMillis, lastBandwidth }); + } + lastStartMillis = startMillis; + lastEndMillis = endMillis; + lastBandwidth = bandwidth; + } + lastMonthString = monthString; + } + if (lastStartMillis > 0L) { + history.put(lastStartMillis, new long[] { lastStartMillis, + lastEndMillis, lastBandwidth }); + } + } + + public String getStatsString() { + /* TODO Add statistics string. */ + return null; + } +} + diff --git a/src/org/torproject/onionoo/updater/ClientsStatusUpdater.java b/src/org/torproject/onionoo/updater/ClientsStatusUpdater.java new file mode 100644 index 0000000..79c1060 --- /dev/null +++ b/src/org/torproject/onionoo/updater/ClientsStatusUpdater.java @@ -0,0 +1,230 @@ +/* Copyright 2014 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.updater; + +import java.util.Map; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; + +import org.torproject.descriptor.Descriptor; +import org.torproject.descriptor.ExtraInfoDescriptor; +import org.torproject.onionoo.docs.ClientsHistory; +import org.torproject.onionoo.docs.ClientsStatus; +import org.torproject.onionoo.docs.DocumentStore; +import org.torproject.onionoo.util.ApplicationFactory; +import org.torproject.onionoo.util.DateTimeHelper; +import org.torproject.onionoo.util.Logger; + +/* + * Example extra-info descriptor used as input: + * + * extra-info ndnop2 DE6397A047ABE5F78B4C87AF725047831B221AAB + * dirreq-stats-end 2014-02-16 16:42:11 (86400 s) + * dirreq-v3-resp ok=856,not-enough-sigs=0,unavailable=0,not-found=0, + * not-modified=40,busy=0 + * bridge-stats-end 2014-02-16 16:42:17 (86400 s) + * bridge-ips ??=8,in=8,se=8 + * bridge-ip-versions v4=8,v6=0 + * + * Clients status file produced as intermediate output: + * + * 2014-02-15 16:42:11 2014-02-16 00:00:00 + * 259.042 in=86.347,se=86.347 v4=259.042 + * 2014-02-16 00:00:00 2014-02-16 16:42:11 + * 592.958 in=197.653,se=197.653 v4=592.958 + */ +public class ClientsStatusUpdater implements DescriptorListener, + StatusUpdater { + + private DescriptorSource descriptorSource; + + private DocumentStore documentStore; + + private long now; + + public ClientsStatusUpdater() { + this.descriptorSource = ApplicationFactory.getDescriptorSource(); + this.documentStore = ApplicationFactory.getDocumentStore(); + this.now = ApplicationFactory.getTime().currentTimeMillis(); + this.registerDescriptorListeners(); + } + + private void registerDescriptorListeners() { + this.descriptorSource.registerDescriptorListener(this, + DescriptorType.BRIDGE_EXTRA_INFOS); + } + + public void processDescriptor(Descriptor descriptor, boolean relay) { + if (descriptor instanceof ExtraInfoDescriptor && !relay) { + this.processBridgeExtraInfoDescriptor( + (ExtraInfoDescriptor) descriptor); + } + } + + private SortedMap<String, SortedSet<ClientsHistory>> newResponses = + new TreeMap<String, SortedSet<ClientsHistory>>(); + + private void processBridgeExtraInfoDescriptor( + ExtraInfoDescriptor descriptor) { + long dirreqStatsEndMillis = descriptor.getDirreqStatsEndMillis(); + long dirreqStatsIntervalLengthMillis = + descriptor.getDirreqStatsIntervalLength() + * DateTimeHelper.ONE_SECOND; + SortedMap<String, Integer> responses = descriptor.getDirreqV3Resp(); + if (dirreqStatsEndMillis < 0L || + dirreqStatsIntervalLengthMillis != DateTimeHelper.ONE_DAY || + responses == null || !responses.containsKey("ok")) { + return; + } + double okResponses = (double) (responses.get("ok") - 4); + if (okResponses < 0.0) { + return; + } + String hashedFingerprint = descriptor.getFingerprint().toUpperCase(); + long dirreqStatsStartMillis = dirreqStatsEndMillis + - dirreqStatsIntervalLengthMillis; + long utcBreakMillis = (dirreqStatsEndMillis / DateTimeHelper.ONE_DAY) + * DateTimeHelper.ONE_DAY; + for (int i = 0; i < 2; i++) { + long startMillis = i == 0 ? dirreqStatsStartMillis : utcBreakMillis; + long endMillis = i == 0 ? utcBreakMillis : dirreqStatsEndMillis; + if (startMillis >= endMillis) { + continue; + } + double totalResponses = okResponses + * ((double) (endMillis - startMillis)) + / ((double) DateTimeHelper.ONE_DAY); + SortedMap<String, Double> responsesByCountry = + this.weightResponsesWithUniqueIps(totalResponses, + descriptor.getBridgeIps(), "??"); + SortedMap<String, Double> responsesByTransport = + this.weightResponsesWithUniqueIps(totalResponses, + descriptor.getBridgeIpTransports(), "<??>"); + SortedMap<String, Double> responsesByVersion = + this.weightResponsesWithUniqueIps(totalResponses, + descriptor.getBridgeIpVersions(), ""); + ClientsHistory newResponseHistory = new ClientsHistory( + startMillis, endMillis, totalResponses, responsesByCountry, + responsesByTransport, responsesByVersion); + if (!this.newResponses.containsKey(hashedFingerprint)) { + this.newResponses.put(hashedFingerprint, + new TreeSet<ClientsHistory>()); + } + this.newResponses.get(hashedFingerprint).add( + newResponseHistory); + } + } + + private SortedMap<String, Double> weightResponsesWithUniqueIps( + double totalResponses, SortedMap<String, Integer> uniqueIps, + String omitString) { + SortedMap<String, Double> weightedResponses = + new TreeMap<String, Double>(); + int totalUniqueIps = 0; + if (uniqueIps != null) { + for (Map.Entry<String, Integer> e : uniqueIps.entrySet()) { + if (e.getValue() > 4) { + totalUniqueIps += e.getValue() - 4; + } + } + } + if (totalUniqueIps > 0) { + for (Map.Entry<String, Integer> e : uniqueIps.entrySet()) { + if (!e.getKey().equals(omitString) && e.getValue() > 4) { + weightedResponses.put(e.getKey(), + (((double) (e.getValue() - 4)) * totalResponses) + / ((double) totalUniqueIps)); + } + } + } + return weightedResponses; + } + + public void updateStatuses() { + for (Map.Entry<String, SortedSet<ClientsHistory>> e : + this.newResponses.entrySet()) { + String hashedFingerprint = e.getKey(); + ClientsStatus clientsStatus = this.documentStore.retrieve( + ClientsStatus.class, true, hashedFingerprint); + if (clientsStatus == null) { + clientsStatus = new ClientsStatus(); + } + this.addToHistory(clientsStatus, e.getValue()); + this.compressHistory(clientsStatus); + this.documentStore.store(clientsStatus, hashedFingerprint); + } + } + + private void addToHistory(ClientsStatus clientsStatus, + SortedSet<ClientsHistory> newIntervals) { + SortedSet<ClientsHistory> history = clientsStatus.getHistory(); + for (ClientsHistory interval : newIntervals) { + if ((history.headSet(interval).isEmpty() || + history.headSet(interval).last().getEndMillis() <= + interval.getStartMillis()) && + (history.tailSet(interval).isEmpty() || + history.tailSet(interval).first().getStartMillis() >= + interval.getEndMillis())) { + history.add(interval); + } + } + } + + private void compressHistory(ClientsStatus clientsStatus) { + SortedSet<ClientsHistory> history = clientsStatus.getHistory(); + SortedSet<ClientsHistory> compressedHistory = + new TreeSet<ClientsHistory>(); + ClientsHistory lastResponses = null; + String lastMonthString = "1970-01"; + for (ClientsHistory responses : history) { + long intervalLengthMillis; + if (this.now - responses.getEndMillis() <= + DateTimeHelper.ROUGHLY_THREE_MONTHS) { + intervalLengthMillis = DateTimeHelper.ONE_DAY; + } else if (this.now - responses.getEndMillis() <= + DateTimeHelper.ROUGHLY_ONE_YEAR) { + intervalLengthMillis = DateTimeHelper.TWO_DAYS; + } else { + intervalLengthMillis = DateTimeHelper.TEN_DAYS; + } + String monthString = DateTimeHelper.format( + responses.getStartMillis(), + DateTimeHelper.ISO_YEARMONTH_FORMAT); + if (lastResponses != null && + lastResponses.getEndMillis() == responses.getStartMillis() && + ((lastResponses.getEndMillis() - 1L) / intervalLengthMillis) == + ((responses.getEndMillis() - 1L) / intervalLengthMillis) && + lastMonthString.equals(monthString)) { + lastResponses.addResponses(responses); + } else { + if (lastResponses != null) { + compressedHistory.add(lastResponses); + } + lastResponses = responses; + } + lastMonthString = monthString; + } + if (lastResponses != null) { + compressedHistory.add(lastResponses); + } + clientsStatus.setHistory(compressedHistory); + } + + public String getStatsString() { + int newIntervals = 0; + for (SortedSet<ClientsHistory> hist : this.newResponses.values()) { + newIntervals += hist.size(); + } + StringBuilder sb = new StringBuilder(); + sb.append(" " + + Logger.formatDecimalNumber(newIntervals / 2) + + " client statistics processed from extra-info descriptors\n"); + sb.append(" " + + Logger.formatDecimalNumber(this.newResponses.size()) + + " client status files updated\n"); + return sb.toString(); + } +} + diff --git a/src/org/torproject/onionoo/updater/DescriptorListener.java b/src/org/torproject/onionoo/updater/DescriptorListener.java new file mode 100644 index 0000000..3613879 --- /dev/null +++ b/src/org/torproject/onionoo/updater/DescriptorListener.java @@ -0,0 +1,7 @@ +package org.torproject.onionoo.updater; + +import org.torproject.descriptor.Descriptor; + +public interface DescriptorListener { + abstract void processDescriptor(Descriptor descriptor, boolean relay); +} \ No newline at end of file diff --git a/src/org/torproject/onionoo/updater/DescriptorSource.java b/src/org/torproject/onionoo/updater/DescriptorSource.java new file mode 100644 index 0000000..32fbd2a --- /dev/null +++ b/src/org/torproject/onionoo/updater/DescriptorSource.java @@ -0,0 +1,646 @@ +/* Copyright 2013, 2014 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.updater; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.zip.GZIPInputStream; + +import org.torproject.descriptor.BridgeNetworkStatus; +import org.torproject.descriptor.BridgePoolAssignment; +import org.torproject.descriptor.Descriptor; +import org.torproject.descriptor.DescriptorFile; +import org.torproject.descriptor.DescriptorReader; +import org.torproject.descriptor.DescriptorSourceFactory; +import org.torproject.descriptor.ExitList; +import org.torproject.descriptor.ExitListEntry; +import org.torproject.descriptor.ExtraInfoDescriptor; +import org.torproject.descriptor.RelayNetworkStatusConsensus; +import org.torproject.descriptor.ServerDescriptor; +import org.torproject.onionoo.util.Logger; + +enum DescriptorHistory { + RELAY_CONSENSUS_HISTORY, + RELAY_SERVER_HISTORY, + RELAY_EXTRAINFO_HISTORY, + EXIT_LIST_HISTORY, + BRIDGE_STATUS_HISTORY, + BRIDGE_SERVER_HISTORY, + BRIDGE_EXTRAINFO_HISTORY, + BRIDGE_POOLASSIGN_HISTORY, +} + +class DescriptorDownloader { + + private final String protocolHostNameResourcePrefix = + "https://collector.torproject.org/recent/"; + + private String directory; + + private final File inDir = new File("in/recent"); + + public DescriptorDownloader(DescriptorType descriptorType) { + switch (descriptorType) { + case RELAY_CONSENSUSES: + this.directory = "relay-descriptors/consensuses/"; + break; + case RELAY_SERVER_DESCRIPTORS: + this.directory = "relay-descriptors/server-descriptors/"; + break; + case RELAY_EXTRA_INFOS: + this.directory = "relay-descriptors/extra-infos/"; + break; + case EXIT_LISTS: + this.directory = "exit-lists/"; + break; + case BRIDGE_STATUSES: + this.directory = "bridge-descriptors/statuses/"; + break; + case BRIDGE_SERVER_DESCRIPTORS: + this.directory = "bridge-descriptors/server-descriptors/"; + break; + case BRIDGE_EXTRA_INFOS: + this.directory = "bridge-descriptors/extra-infos/"; + break; + case BRIDGE_POOL_ASSIGNMENTS: + this.directory = "bridge-pool-assignments/"; + break; + default: + System.err.println("Unknown descriptor type."); + return; + } + } + + private SortedSet<String> localFiles = new TreeSet<String>(); + + public int statLocalFiles() { + File localDirectory = new File(this.inDir, this.directory); + if (localDirectory.exists()) { + for (File file : localDirectory.listFiles()) { + this.localFiles.add(file.getName()); + } + } + return this.localFiles.size(); + } + + private SortedSet<String> remoteFiles = new TreeSet<String>(); + + public int fetchRemoteDirectory() { + String directoryUrl = this.protocolHostNameResourcePrefix + + this.directory; + try { + URL u = new URL(directoryUrl); + HttpURLConnection huc = (HttpURLConnection) u.openConnection(); + huc.setRequestMethod("GET"); + huc.connect(); + if (huc.getResponseCode() != 200) { + System.err.println("Could not fetch " + directoryUrl + + ": " + huc.getResponseCode() + " " + + huc.getResponseMessage() + ". Skipping."); + return 0; + } + BufferedReader br = new BufferedReader(new InputStreamReader( + huc.getInputStream())); + String line; + while ((line = br.readLine()) != null) { + if (!line.trim().startsWith("<tr>") || + !line.contains("<a href="")) { + continue; + } + String linePart = line.substring( + line.indexOf("<a href="") + "<a href="".length()); + if (!linePart.contains(""")) { + continue; + } + linePart = linePart.substring(0, linePart.indexOf(""")); + if (linePart.endsWith("/")) { + continue; + } + this.remoteFiles.add(linePart); + } + br.close(); + } catch (IOException e) { + System.err.println("Could not fetch or parse " + directoryUrl + + ". Skipping."); + } + return this.remoteFiles.size(); + } + + public int fetchRemoteFiles() { + int fetchedFiles = 0; + for (String remoteFile : this.remoteFiles) { + if (this.localFiles.contains(remoteFile)) { + continue; + } + String fileUrl = this.protocolHostNameResourcePrefix + + this.directory + remoteFile; + File localTempFile = new File(this.inDir, this.directory + + remoteFile + ".tmp"); + File localFile = new File(this.inDir, this.directory + remoteFile); + try { + localFile.getParentFile().mkdirs(); + URL u = new URL(fileUrl); + HttpURLConnection huc = (HttpURLConnection) u.openConnection(); + huc.setRequestMethod("GET"); + huc.addRequestProperty("Accept-Encoding", "gzip"); + huc.connect(); + if (huc.getResponseCode() != 200) { + System.err.println("Could not fetch " + fileUrl + + ": " + huc.getResponseCode() + " " + + huc.getResponseMessage() + ". Skipping."); + continue; + } + long lastModified = huc.getHeaderFieldDate("Last-Modified", -1L); + InputStream is; + if (huc.getContentEncoding() != null && + huc.getContentEncoding().equalsIgnoreCase("gzip")) { + is = new GZIPInputStream(huc.getInputStream()); + } else { + is = huc.getInputStream(); + } + BufferedInputStream bis = new BufferedInputStream(is); + BufferedOutputStream bos = new BufferedOutputStream( + new FileOutputStream(localTempFile)); + int len; + byte[] data = new byte[1024]; + while ((len = bis.read(data, 0, 1024)) >= 0) { + bos.write(data, 0, len); + } + bis.close(); + bos.close(); + localTempFile.renameTo(localFile); + if (lastModified >= 0) { + localFile.setLastModified(lastModified); + } + fetchedFiles++; + } catch (IOException e) { + System.err.println("Could not fetch or store " + fileUrl + + ". Skipping."); + } + } + return fetchedFiles; + } + + public int deleteOldLocalFiles() { + int deletedFiles = 0; + for (String localFile : this.localFiles) { + if (!this.remoteFiles.contains(localFile)) { + new File(this.inDir, this.directory + localFile).delete(); + deletedFiles++; + } + } + return deletedFiles; + } +} + +class DescriptorQueue { + + private File inDir; + + private File statusDir; + + private DescriptorReader descriptorReader; + + private File historyFile; + + private Iterator<DescriptorFile> descriptorFiles; + + private List<Descriptor> descriptors; + + private int historySizeBefore; + public int getHistorySizeBefore() { + return this.historySizeBefore; + } + + private int historySizeAfter; + public int getHistorySizeAfter() { + return this.historySizeAfter; + } + + private long returnedDescriptors = 0L; + public long getReturnedDescriptors() { + return this.returnedDescriptors; + } + + private long returnedBytes = 0L; + public long getReturnedBytes() { + return this.returnedBytes; + } + + public DescriptorQueue(File inDir, File statusDir) { + this.inDir = inDir; + this.statusDir = statusDir; + this.descriptorReader = + DescriptorSourceFactory.createDescriptorReader(); + } + + public void addDirectory(DescriptorType descriptorType) { + String directoryName = null; + switch (descriptorType) { + case RELAY_CONSENSUSES: + directoryName = "relay-descriptors/consensuses"; + break; + case RELAY_SERVER_DESCRIPTORS: + directoryName = "relay-descriptors/server-descriptors"; + break; + case RELAY_EXTRA_INFOS: + directoryName = "relay-descriptors/extra-infos"; + break; + case BRIDGE_STATUSES: + directoryName = "bridge-descriptors/statuses"; + break; + case BRIDGE_SERVER_DESCRIPTORS: + directoryName = "bridge-descriptors/server-descriptors"; + break; + case BRIDGE_EXTRA_INFOS: + directoryName = "bridge-descriptors/extra-infos"; + break; + case BRIDGE_POOL_ASSIGNMENTS: + directoryName = "bridge-pool-assignments"; + break; + case EXIT_LISTS: + directoryName = "exit-lists"; + break; + default: + System.err.println("Unknown descriptor type. Not adding directory " + + "to descriptor reader."); + return; + } + File directory = new File(this.inDir, directoryName); + if (directory.exists() && directory.isDirectory()) { + this.descriptorReader.addDirectory(directory); + this.descriptorReader.setMaxDescriptorFilesInQueue(1); + } else { + System.err.println("Directory " + directory.getAbsolutePath() + + " either does not exist or is not a directory. Not adding " + + "to descriptor reader."); + } + } + + public void readHistoryFile(DescriptorHistory descriptorHistory) { + String historyFileName = null; + switch (descriptorHistory) { + case RELAY_EXTRAINFO_HISTORY: + historyFileName = "relay-extrainfo-history"; + break; + case BRIDGE_EXTRAINFO_HISTORY: + historyFileName = "bridge-extrainfo-history"; + break; + case EXIT_LIST_HISTORY: + historyFileName = "exit-list-history"; + break; + case BRIDGE_POOLASSIGN_HISTORY: + historyFileName = "bridge-poolassign-history"; + break; + case RELAY_CONSENSUS_HISTORY: + historyFileName = "relay-consensus-history"; + break; + case BRIDGE_STATUS_HISTORY: + historyFileName = "bridge-status-history"; + break; + case RELAY_SERVER_HISTORY: + historyFileName = "relay-server-history"; + break; + case BRIDGE_SERVER_HISTORY: + historyFileName = "bridge-server-history"; + break; + default: + System.err.println("Unknown descriptor history. Not excluding " + + "files."); + return; + } + this.historyFile = new File(this.statusDir, historyFileName); + if (this.historyFile.exists() && this.historyFile.isFile()) { + SortedMap<String, Long> excludedFiles = new TreeMap<String, Long>(); + try { + BufferedReader br = new BufferedReader(new FileReader( + this.historyFile)); + String line; + while ((line = br.readLine()) != null) { + try { + String[] parts = line.split(" ", 2); + excludedFiles.put(parts[1], Long.parseLong(parts[0])); + } catch (NumberFormatException e) { + System.err.println("Illegal line '" + line + "' in parse " + + "history. Skipping line."); + } + } + br.close(); + } catch (IOException e) { + System.err.println("Could not read history file '" + + this.historyFile.getAbsolutePath() + "'. Not excluding " + + "descriptors in this execution."); + e.printStackTrace(); + return; + } + this.historySizeBefore = excludedFiles.size(); + this.descriptorReader.setExcludedFiles(excludedFiles); + } + } + + public void writeHistoryFile() { + if (this.historyFile == null) { + return; + } + SortedMap<String, Long> excludedAndParsedFiles = + new TreeMap<String, Long>(); + excludedAndParsedFiles.putAll( + this.descriptorReader.getExcludedFiles()); + excludedAndParsedFiles.putAll(this.descriptorReader.getParsedFiles()); + this.historySizeAfter = excludedAndParsedFiles.size(); + try { + this.historyFile.getParentFile().mkdirs(); + BufferedWriter bw = new BufferedWriter(new FileWriter( + this.historyFile)); + for (Map.Entry<String, Long> e : excludedAndParsedFiles.entrySet()) { + String absolutePath = e.getKey(); + long lastModifiedMillis = e.getValue(); + bw.write(String.valueOf(lastModifiedMillis) + " " + absolutePath + + "\n"); + } + bw.close(); + } catch (IOException e) { + System.err.println("Could not write history file '" + + this.historyFile.getAbsolutePath() + "'. Not excluding " + + "descriptors in next execution."); + return; + } + } + + public Descriptor nextDescriptor() { + Descriptor nextDescriptor = null; + if (this.descriptorFiles == null) { + this.descriptorFiles = this.descriptorReader.readDescriptors(); + } + while (this.descriptors == null && this.descriptorFiles.hasNext()) { + DescriptorFile descriptorFile = this.descriptorFiles.next(); + if (descriptorFile.getException() != null) { + System.err.println("Could not parse " + + descriptorFile.getFileName()); + descriptorFile.getException().printStackTrace(); + } + if (descriptorFile.getDescriptors() != null && + !descriptorFile.getDescriptors().isEmpty()) { + this.descriptors = descriptorFile.getDescriptors(); + } + } + if (this.descriptors != null) { + nextDescriptor = this.descriptors.remove(0); + this.returnedDescriptors++; + this.returnedBytes += nextDescriptor.getRawDescriptorBytes().length; + if (this.descriptors.isEmpty()) { + this.descriptors = null; + } + } + return nextDescriptor; + } +} + +public class DescriptorSource { + + private final File inDir = new File("in/recent"); + + private final File statusDir = new File("status"); + + private List<DescriptorQueue> descriptorQueues; + + public DescriptorSource() { + this.descriptorQueues = new ArrayList<DescriptorQueue>(); + this.descriptorListeners = + new HashMap<DescriptorType, Set<DescriptorListener>>(); + this.fingerprintListeners = + new HashMap<DescriptorType, Set<FingerprintListener>>(); + } + + private DescriptorQueue getDescriptorQueue( + DescriptorType descriptorType, + DescriptorHistory descriptorHistory) { + DescriptorQueue descriptorQueue = new DescriptorQueue(this.inDir, + this.statusDir); + descriptorQueue.addDirectory(descriptorType); + if (descriptorHistory != null) { + descriptorQueue.readHistoryFile(descriptorHistory); + } + this.descriptorQueues.add(descriptorQueue); + return descriptorQueue; + } + + private Map<DescriptorType, Set<DescriptorListener>> + descriptorListeners; + + private Map<DescriptorType, Set<FingerprintListener>> + fingerprintListeners; + + public void registerDescriptorListener(DescriptorListener listener, + DescriptorType descriptorType) { + if (!this.descriptorListeners.containsKey(descriptorType)) { + this.descriptorListeners.put(descriptorType, + new HashSet<DescriptorListener>()); + } + this.descriptorListeners.get(descriptorType).add(listener); + } + + public void registerFingerprintListener(FingerprintListener listener, + DescriptorType descriptorType) { + if (!this.fingerprintListeners.containsKey(descriptorType)) { + this.fingerprintListeners.put(descriptorType, + new HashSet<FingerprintListener>()); + } + this.fingerprintListeners.get(descriptorType).add(listener); + } + + public void downloadDescriptors() { + for (DescriptorType descriptorType : DescriptorType.values()) { + this.downloadDescriptors(descriptorType); + } + } + + private int localFilesBefore = 0, foundRemoteFiles = 0, + downloadedFiles = 0, deletedLocalFiles = 0; + + private void downloadDescriptors(DescriptorType descriptorType) { + if (!this.descriptorListeners.containsKey(descriptorType) && + !this.fingerprintListeners.containsKey(descriptorType)) { + return; + } + DescriptorDownloader descriptorDownloader = + new DescriptorDownloader(descriptorType); + this.localFilesBefore += descriptorDownloader.statLocalFiles(); + this.foundRemoteFiles += + descriptorDownloader.fetchRemoteDirectory(); + this.downloadedFiles += descriptorDownloader.fetchRemoteFiles(); + this.deletedLocalFiles += descriptorDownloader.deleteOldLocalFiles(); + } + + public void readDescriptors() { + /* Careful when changing the order of parsing descriptor types! The + * various status updaters may base assumptions on this order. */ + this.readDescriptors(DescriptorType.RELAY_SERVER_DESCRIPTORS, + DescriptorHistory.RELAY_SERVER_HISTORY, true); + this.readDescriptors(DescriptorType.RELAY_EXTRA_INFOS, + DescriptorHistory.RELAY_EXTRAINFO_HISTORY, true); + this.readDescriptors(DescriptorType.EXIT_LISTS, + DescriptorHistory.EXIT_LIST_HISTORY, true); + this.readDescriptors(DescriptorType.RELAY_CONSENSUSES, + DescriptorHistory.RELAY_CONSENSUS_HISTORY, true); + this.readDescriptors(DescriptorType.BRIDGE_SERVER_DESCRIPTORS, + DescriptorHistory.BRIDGE_SERVER_HISTORY, false); + this.readDescriptors(DescriptorType.BRIDGE_EXTRA_INFOS, + DescriptorHistory.BRIDGE_EXTRAINFO_HISTORY, false); + this.readDescriptors(DescriptorType.BRIDGE_POOL_ASSIGNMENTS, + DescriptorHistory.BRIDGE_POOLASSIGN_HISTORY, false); + this.readDescriptors(DescriptorType.BRIDGE_STATUSES, + DescriptorHistory.BRIDGE_STATUS_HISTORY, false); + } + + private void readDescriptors(DescriptorType descriptorType, + DescriptorHistory descriptorHistory, boolean relay) { + if (!this.descriptorListeners.containsKey(descriptorType) && + !this.fingerprintListeners.containsKey(descriptorType)) { + return; + } + Set<DescriptorListener> descriptorListeners = + this.descriptorListeners.get(descriptorType); + Set<FingerprintListener> fingerprintListeners = + this.fingerprintListeners.get(descriptorType); + DescriptorQueue descriptorQueue = this.getDescriptorQueue( + descriptorType, descriptorHistory); + Descriptor descriptor; + while ((descriptor = descriptorQueue.nextDescriptor()) != null) { + for (DescriptorListener descriptorListener : descriptorListeners) { + descriptorListener.processDescriptor(descriptor, relay); + } + if (fingerprintListeners == null) { + continue; + } + SortedSet<String> fingerprints = new TreeSet<String>(); + if (descriptorType == DescriptorType.RELAY_CONSENSUSES && + descriptor instanceof RelayNetworkStatusConsensus) { + fingerprints.addAll(((RelayNetworkStatusConsensus) descriptor). + getStatusEntries().keySet()); + } else if (descriptorType + == DescriptorType.RELAY_SERVER_DESCRIPTORS && + descriptor instanceof ServerDescriptor) { + fingerprints.add(((ServerDescriptor) descriptor). + getFingerprint()); + } else if (descriptorType == DescriptorType.RELAY_EXTRA_INFOS && + descriptor instanceof ExtraInfoDescriptor) { + fingerprints.add(((ExtraInfoDescriptor) descriptor). + getFingerprint()); + } else if (descriptorType == DescriptorType.EXIT_LISTS && + descriptor instanceof ExitList) { + for (ExitListEntry entry : + ((ExitList) descriptor).getExitListEntries()) { + fingerprints.add(entry.getFingerprint()); + } + } else if (descriptorType == DescriptorType.BRIDGE_STATUSES && + descriptor instanceof BridgeNetworkStatus) { + fingerprints.addAll(((BridgeNetworkStatus) descriptor). + getStatusEntries().keySet()); + } else if (descriptorType == + DescriptorType.BRIDGE_SERVER_DESCRIPTORS && + descriptor instanceof ServerDescriptor) { + fingerprints.add(((ServerDescriptor) descriptor). + getFingerprint()); + } else if (descriptorType == DescriptorType.BRIDGE_EXTRA_INFOS && + descriptor instanceof ExtraInfoDescriptor) { + fingerprints.add(((ExtraInfoDescriptor) descriptor). + getFingerprint()); + } else if (descriptorType == + DescriptorType.BRIDGE_POOL_ASSIGNMENTS && + descriptor instanceof BridgePoolAssignment) { + fingerprints.addAll(((BridgePoolAssignment) descriptor). + getEntries().keySet()); + } + for (FingerprintListener fingerprintListener : + fingerprintListeners) { + fingerprintListener.processFingerprints(fingerprints, relay); + } + } + switch (descriptorType) { + case RELAY_CONSENSUSES: + Logger.printStatusTime("Read relay network consensuses"); + break; + case RELAY_SERVER_DESCRIPTORS: + Logger.printStatusTime("Read relay server descriptors"); + break; + case RELAY_EXTRA_INFOS: + Logger.printStatusTime("Read relay extra-info descriptors"); + break; + case EXIT_LISTS: + Logger.printStatusTime("Read exit lists"); + break; + case BRIDGE_STATUSES: + Logger.printStatusTime("Read bridge network statuses"); + break; + case BRIDGE_SERVER_DESCRIPTORS: + Logger.printStatusTime("Read bridge server descriptors"); + break; + case BRIDGE_EXTRA_INFOS: + Logger.printStatusTime("Read bridge extra-info descriptors"); + break; + case BRIDGE_POOL_ASSIGNMENTS: + Logger.printStatusTime("Read bridge-pool assignments"); + break; + } + } + + public void writeHistoryFiles() { + for (DescriptorQueue descriptorQueue : this.descriptorQueues) { + descriptorQueue.writeHistoryFile(); + } + } + + public String getStatsString() { + StringBuilder sb = new StringBuilder(); + sb.append(" " + this.localFilesBefore + " descriptor files found " + + "locally\n"); + sb.append(" " + this.foundRemoteFiles + " descriptor files found " + + "remotely\n"); + sb.append(" " + this.downloadedFiles + " descriptor files " + + "downloaded from remote\n"); + sb.append(" " + this.deletedLocalFiles + " descriptor files " + + "deleted locally\n"); + sb.append(" " + this.descriptorQueues.size() + " descriptor " + + "queues created\n"); + int historySizeBefore = 0, historySizeAfter = 0; + long descriptors = 0L, bytes = 0L; + for (DescriptorQueue descriptorQueue : descriptorQueues) { + historySizeBefore += descriptorQueue.getHistorySizeBefore(); + historySizeAfter += descriptorQueue.getHistorySizeAfter(); + descriptors += descriptorQueue.getReturnedDescriptors(); + bytes += descriptorQueue.getReturnedBytes(); + } + sb.append(" " + Logger.formatDecimalNumber(historySizeBefore) + + " descriptors excluded from this execution\n"); + sb.append(" " + Logger.formatDecimalNumber(descriptors) + + " descriptors provided\n"); + sb.append(" " + Logger.formatBytes(bytes) + " provided\n"); + sb.append(" " + Logger.formatDecimalNumber(historySizeAfter) + + " descriptors excluded from next execution\n"); + return sb.toString(); + } +} + diff --git a/src/org/torproject/onionoo/updater/DescriptorType.java b/src/org/torproject/onionoo/updater/DescriptorType.java new file mode 100644 index 0000000..41956da --- /dev/null +++ b/src/org/torproject/onionoo/updater/DescriptorType.java @@ -0,0 +1,15 @@ +/* Copyright 2013, 2014 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.updater; + +public enum DescriptorType { + RELAY_CONSENSUSES, + RELAY_SERVER_DESCRIPTORS, + RELAY_EXTRA_INFOS, + EXIT_LISTS, + BRIDGE_STATUSES, + BRIDGE_SERVER_DESCRIPTORS, + BRIDGE_EXTRA_INFOS, + BRIDGE_POOL_ASSIGNMENTS, +} + diff --git a/src/org/torproject/onionoo/updater/FingerprintListener.java b/src/org/torproject/onionoo/updater/FingerprintListener.java new file mode 100644 index 0000000..5e16eae --- /dev/null +++ b/src/org/torproject/onionoo/updater/FingerprintListener.java @@ -0,0 +1,10 @@ +/* Copyright 2013, 2014 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.updater; + +import java.util.SortedSet; + +public interface FingerprintListener { + abstract void processFingerprints(SortedSet<String> fingerprints, + boolean relay); +} \ No newline at end of file diff --git a/src/org/torproject/onionoo/updater/LookupResult.java b/src/org/torproject/onionoo/updater/LookupResult.java new file mode 100644 index 0000000..dcf3a2a --- /dev/null +++ b/src/org/torproject/onionoo/updater/LookupResult.java @@ -0,0 +1,70 @@ +/* Copyright 2013 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.updater; + +public class LookupResult { + + private String countryCode; + public void setCountryCode(String countryCode) { + this.countryCode = countryCode; + } + public String getCountryCode() { + return this.countryCode; + } + + private String countryName; + public void setCountryName(String countryName) { + this.countryName = countryName; + } + public String getCountryName() { + return this.countryName; + } + + private String regionName; + public void setRegionName(String regionName) { + this.regionName = regionName; + } + public String getRegionName() { + return this.regionName; + } + + private String cityName; + public void setCityName(String cityName) { + this.cityName = cityName; + } + public String getCityName() { + return this.cityName; + } + + private Float latitude; + public void setLatitude(Float latitude) { + this.latitude = latitude; + } + public Float getLatitude() { + return this.latitude; + } + + private Float longitude; + public void setLongitude(Float longitude) { + this.longitude = longitude; + } + public Float getLongitude() { + return this.longitude; + } + + private String asNumber; + public void setAsNumber(String asNumber) { + this.asNumber = asNumber; + } + public String getAsNumber() { + return this.asNumber; + } + + private String asName; + public void setAsName(String asName) { + this.asName = asName; + } + public String getAsName() { + return this.asName; + } +} \ No newline at end of file diff --git a/src/org/torproject/onionoo/updater/LookupService.java b/src/org/torproject/onionoo/updater/LookupService.java new file mode 100644 index 0000000..b816091 --- /dev/null +++ b/src/org/torproject/onionoo/updater/LookupService.java @@ -0,0 +1,343 @@ +/* Copyright 2013 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.updater; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.regex.Pattern; + +import org.torproject.onionoo.util.Logger; + +public class LookupService { + + private File geoipDir; + private File geoLite2CityBlocksCsvFile; + private File geoLite2CityLocationsCsvFile; + private File geoIPASNum2CsvFile; + private boolean hasAllFiles = false; + public LookupService(File geoipDir) { + this.geoipDir = geoipDir; + this.findRequiredCsvFiles(); + } + + /* Make sure we have all required .csv files. */ + private void findRequiredCsvFiles() { + this.geoLite2CityBlocksCsvFile = new File(this.geoipDir, + "GeoLite2-City-Blocks.csv"); + if (!this.geoLite2CityBlocksCsvFile.exists()) { + System.err.println("No GeoLite2-City-Blocks.csv file in geoip/."); + return; + } + this.geoLite2CityLocationsCsvFile = new File(this.geoipDir, + "GeoLite2-City-Locations.csv"); + if (!this.geoLite2CityLocationsCsvFile.exists()) { + System.err.println("No GeoLite2-City-Locations.csv file in " + + "geoip/."); + return; + } + this.geoIPASNum2CsvFile = new File(this.geoipDir, "GeoIPASNum2.csv"); + if (!this.geoIPASNum2CsvFile.exists()) { + System.err.println("No GeoIPASNum2.csv file in geoip/."); + return; + } + this.hasAllFiles = true; + } + + private Pattern ipv4Pattern = Pattern.compile("^[0-9\.]{7,15}$"); + private long parseAddressString(String addressString) { + long addressNumber = -1L; + if (ipv4Pattern.matcher(addressString).matches()) { + String[] parts = addressString.split("\.", 4); + if (parts.length == 4) { + addressNumber = 0L; + for (int i = 0; i < 4; i++) { + addressNumber *= 256L; + int octetValue = -1; + try { + octetValue = Integer.parseInt(parts[i]); + } catch (NumberFormatException e) { + } + if (octetValue < 0 || octetValue > 255) { + addressNumber = -1L; + break; + } + addressNumber += octetValue; + } + } + } + return addressNumber; + } + + public SortedMap<String, LookupResult> lookup( + SortedSet<String> addressStrings) { + + SortedMap<String, LookupResult> lookupResults = + new TreeMap<String, LookupResult>(); + + if (!this.hasAllFiles) { + return lookupResults; + } + + /* Obtain a map from relay IP address strings to numbers. */ + Map<String, Long> addressStringNumbers = new HashMap<String, Long>(); + for (String addressString : addressStrings) { + long addressNumber = this.parseAddressString(addressString); + if (addressNumber >= 0L) { + addressStringNumbers.put(addressString, addressNumber); + } + } + if (addressStringNumbers.isEmpty()) { + return lookupResults; + } + + /* Obtain a map from IP address numbers to blocks and to latitudes and + longitudes. */ + Map<Long, Long> addressNumberBlocks = new HashMap<Long, Long>(); + Map<Long, Float[]> addressNumberLatLong = + new HashMap<Long, Float[]>(); + try { + SortedSet<Long> sortedAddressNumbers = new TreeSet<Long>( + addressStringNumbers.values()); + BufferedReader br = new BufferedReader(new InputStreamReader( + new FileInputStream(geoLite2CityBlocksCsvFile), "ISO-8859-1")); + String line = br.readLine(); + while ((line = br.readLine()) != null) { + if (!line.startsWith("::ffff:")) { + /* TODO Make this less hacky and IPv6-ready at some point. */ + continue; + } + String[] parts = line.replaceAll(""", "").split(",", 10); + if (parts.length != 10) { + System.err.println("Illegal line '" + line + "' in " + + geoLite2CityBlocksCsvFile.getAbsolutePath() + "."); + br.close(); + return lookupResults; + } + try { + String startAddressString = parts[0].substring(7); /* ::ffff: */ + long startIpNum = this.parseAddressString(startAddressString); + if (startIpNum < 0L) { + System.err.println("Illegal IP address in '" + line + + "' in " + geoLite2CityBlocksCsvFile.getAbsolutePath() + + "."); + br.close(); + return lookupResults; + } + int networkMaskLength = Integer.parseInt(parts[1]); + if (networkMaskLength < 96 || networkMaskLength > 128) { + System.err.println("Illegal network mask in '" + line + + "' in " + geoLite2CityBlocksCsvFile.getAbsolutePath() + + "."); + br.close(); + return lookupResults; + } + if (parts[2].length() == 0 && parts[3].length() == 0) { + continue; + } + long endIpNum = startIpNum + (1 << (128 - networkMaskLength)) + - 1; + for (long addressNumber : sortedAddressNumbers. + tailSet(startIpNum).headSet(endIpNum + 1L)) { + String blockString = parts[2].length() > 0 ? parts[2] : + parts[3]; + long blockNumber = Long.parseLong(blockString); + addressNumberBlocks.put(addressNumber, blockNumber); + if (parts[6].length() > 0 && parts[7].length() > 0) { + addressNumberLatLong.put(addressNumber, + new Float[] { Float.parseFloat(parts[6]), + Float.parseFloat(parts[7]) }); + } + } + } catch (NumberFormatException e) { + System.err.println("Number format exception while parsing line " + + "'" + line + "' in " + + geoLite2CityBlocksCsvFile.getAbsolutePath() + "."); + br.close(); + return lookupResults; + } + } + br.close(); + } catch (IOException e) { + System.err.println("I/O exception while reading " + + geoLite2CityBlocksCsvFile.getAbsolutePath() + "."); + return lookupResults; + } + + /* Obtain a map from relevant blocks to location lines. */ + Map<Long, String> blockLocations = new HashMap<Long, String>(); + try { + Set<Long> blockNumbers = new HashSet<Long>( + addressNumberBlocks.values()); + BufferedReader br = new BufferedReader(new InputStreamReader( + new FileInputStream(geoLite2CityLocationsCsvFile), + "ISO-8859-1")); + String line = br.readLine(); + while ((line = br.readLine()) != null) { + String[] parts = line.replaceAll(""", "").split(",", 10); + if (parts.length != 10) { + System.err.println("Illegal line '" + line + "' in " + + geoLite2CityLocationsCsvFile.getAbsolutePath() + "."); + br.close(); + return lookupResults; + } + try { + long locId = Long.parseLong(parts[0]); + if (blockNumbers.contains(locId)) { + blockLocations.put(locId, line); + } + } catch (NumberFormatException e) { + System.err.println("Number format exception while parsing line " + + "'" + line + "' in " + + geoLite2CityLocationsCsvFile.getAbsolutePath() + "."); + br.close(); + return lookupResults; + } + } + br.close(); + } catch (IOException e) { + System.err.println("I/O exception while reading " + + geoLite2CityLocationsCsvFile.getAbsolutePath() + "."); + return lookupResults; + } + + /* Obtain a map from IP address numbers to ASN. */ + Map<Long, String> addressNumberASN = new HashMap<Long, String>(); + try { + SortedSet<Long> sortedAddressNumbers = new TreeSet<Long>( + addressStringNumbers.values()); + long firstAddressNumber = sortedAddressNumbers.first(); + BufferedReader br = new BufferedReader(new InputStreamReader( + new FileInputStream(geoIPASNum2CsvFile), "ISO-8859-1")); + String line; + long previousStartIpNum = -1L; + while ((line = br.readLine()) != null) { + String[] parts = line.replaceAll(""", "").split(",", 3); + if (parts.length != 3) { + System.err.println("Illegal line '" + line + "' in " + + geoIPASNum2CsvFile.getAbsolutePath() + "."); + br.close(); + return lookupResults; + } + try { + long startIpNum = Long.parseLong(parts[0]); + if (startIpNum <= previousStartIpNum) { + System.err.println("Line '" + line + "' not sorted in " + + geoIPASNum2CsvFile.getAbsolutePath() + "."); + br.close(); + return lookupResults; + } + previousStartIpNum = startIpNum; + while (firstAddressNumber < startIpNum && + firstAddressNumber != -1L) { + sortedAddressNumbers.remove(firstAddressNumber); + if (sortedAddressNumbers.isEmpty()) { + firstAddressNumber = -1L; + } else { + firstAddressNumber = sortedAddressNumbers.first(); + } + } + long endIpNum = Long.parseLong(parts[1]); + while (firstAddressNumber <= endIpNum && + firstAddressNumber != -1L) { + if (parts[2].startsWith("AS") && + parts[2].split(" ", 2).length == 2) { + addressNumberASN.put(firstAddressNumber, parts[2]); + } + sortedAddressNumbers.remove(firstAddressNumber); + if (sortedAddressNumbers.isEmpty()) { + firstAddressNumber = -1L; + } else { + firstAddressNumber = sortedAddressNumbers.first(); + } + } + if (firstAddressNumber == -1L) { + break; + } + } + catch (NumberFormatException e) { + System.err.println("Number format exception while parsing line " + + "'" + line + "' in " + + geoIPASNum2CsvFile.getAbsolutePath() + "."); + br.close(); + return lookupResults; + } + } + br.close(); + } catch (IOException e) { + System.err.println("I/O exception while reading " + + geoIPASNum2CsvFile.getAbsolutePath() + "."); + return lookupResults; + } + + /* Finally, put together lookup results. */ + for (String addressString : addressStrings) { + if (!addressStringNumbers.containsKey(addressString)) { + continue; + } + long addressNumber = addressStringNumbers.get(addressString); + if (!addressNumberBlocks.containsKey(addressNumber) && + !addressNumberLatLong.containsKey(addressNumber) && + !addressNumberASN.containsKey(addressNumber)) { + continue; + } + LookupResult lookupResult = new LookupResult(); + if (addressNumberBlocks.containsKey(addressNumber)) { + long blockNumber = addressNumberBlocks.get(addressNumber); + if (blockLocations.containsKey(blockNumber)) { + String[] parts = blockLocations.get(blockNumber). + replaceAll(""", "").split(",", -1); + lookupResult.setCountryCode(parts[3].toLowerCase()); + if (parts[4].length() > 0) { + lookupResult.setCountryName(parts[4]); + } + if (parts[6].length() > 0) { + lookupResult.setRegionName(parts[6]); + } + if (parts[7].length() > 0) { + lookupResult.setCityName(parts[7]); + } + } + } + if (addressNumberLatLong.containsKey(addressNumber)) { + Float[] latLong = addressNumberLatLong.get(addressNumber); + lookupResult.setLatitude(latLong[0]); + lookupResult.setLongitude(latLong[1]); + } + if (addressNumberASN.containsKey(addressNumber)) { + String[] parts = addressNumberASN.get(addressNumber).split(" ", + 2); + lookupResult.setAsNumber(parts[0]); + lookupResult.setAsName(parts[1]); + } + lookupResults.put(addressString, lookupResult); + } + + /* Keep statistics. */ + this.addressesLookedUp += addressStrings.size(); + this.addressesResolved += lookupResults.size(); + + return lookupResults; + } + + private int addressesLookedUp = 0, addressesResolved = 0; + + public String getStatsString() { + StringBuilder sb = new StringBuilder(); + sb.append(" " + Logger.formatDecimalNumber(addressesLookedUp) + + " addresses looked up\n"); + sb.append(" " + Logger.formatDecimalNumber(addressesResolved) + + " addresses resolved\n"); + return sb.toString(); + } +} diff --git a/src/org/torproject/onionoo/updater/NodeDetailsStatusUpdater.java b/src/org/torproject/onionoo/updater/NodeDetailsStatusUpdater.java new file mode 100644 index 0000000..c687704 --- /dev/null +++ b/src/org/torproject/onionoo/updater/NodeDetailsStatusUpdater.java @@ -0,0 +1,626 @@ +/* Copyright 2011--2014 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.updater; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; + +import org.torproject.descriptor.BridgeNetworkStatus; +import org.torproject.descriptor.BridgePoolAssignment; +import org.torproject.descriptor.Descriptor; +import org.torproject.descriptor.ExitList; +import org.torproject.descriptor.ExitListEntry; +import org.torproject.descriptor.NetworkStatusEntry; +import org.torproject.descriptor.RelayNetworkStatusConsensus; +import org.torproject.descriptor.ServerDescriptor; +import org.torproject.onionoo.docs.DetailsStatus; +import org.torproject.onionoo.docs.DocumentStore; +import org.torproject.onionoo.docs.NodeStatus; +import org.torproject.onionoo.util.ApplicationFactory; +import org.torproject.onionoo.util.DateTimeHelper; +import org.torproject.onionoo.util.Logger; + +public class NodeDetailsStatusUpdater implements DescriptorListener, + StatusUpdater { + + private DescriptorSource descriptorSource; + + private ReverseDomainNameResolver reverseDomainNameResolver; + + private LookupService lookupService; + + private DocumentStore documentStore; + + private long now; + + private SortedMap<String, NodeStatus> knownNodes = + new TreeMap<String, NodeStatus>(); + + private SortedMap<String, NodeStatus> relays; + + private SortedMap<String, NodeStatus> bridges; + + private long relaysLastValidAfterMillis = -1L; + + private long bridgesLastPublishedMillis = -1L; + + private SortedMap<String, Integer> lastBandwidthWeights = null; + + private int relayConsensusesProcessed = 0, bridgeStatusesProcessed = 0; + + public NodeDetailsStatusUpdater( + ReverseDomainNameResolver reverseDomainNameResolver, + LookupService lookupService) { + this.descriptorSource = ApplicationFactory.getDescriptorSource(); + this.reverseDomainNameResolver = reverseDomainNameResolver; + this.lookupService = lookupService; + this.documentStore = ApplicationFactory.getDocumentStore(); + this.now = ApplicationFactory.getTime().currentTimeMillis(); + this.registerDescriptorListeners(); + } + + private void registerDescriptorListeners() { + this.descriptorSource.registerDescriptorListener(this, + DescriptorType.RELAY_CONSENSUSES); + this.descriptorSource.registerDescriptorListener(this, + DescriptorType.RELAY_SERVER_DESCRIPTORS); + this.descriptorSource.registerDescriptorListener(this, + DescriptorType.BRIDGE_STATUSES); + this.descriptorSource.registerDescriptorListener(this, + DescriptorType.BRIDGE_SERVER_DESCRIPTORS); + this.descriptorSource.registerDescriptorListener(this, + DescriptorType.BRIDGE_POOL_ASSIGNMENTS); + this.descriptorSource.registerDescriptorListener(this, + DescriptorType.EXIT_LISTS); + } + + public void processDescriptor(Descriptor descriptor, boolean relay) { + if (descriptor instanceof ServerDescriptor && relay) { + this.processRelayServerDescriptor((ServerDescriptor) descriptor); + } else if (descriptor instanceof ExitList) { + this.processExitList((ExitList) descriptor); + } else if (descriptor instanceof RelayNetworkStatusConsensus) { + this.processRelayNetworkStatusConsensus( + (RelayNetworkStatusConsensus) descriptor); + } else if (descriptor instanceof ServerDescriptor && !relay) { + this.processBridgeServerDescriptor((ServerDescriptor) descriptor); + } else if (descriptor instanceof BridgePoolAssignment) { + this.processBridgePoolAssignment((BridgePoolAssignment) descriptor); + } else if (descriptor instanceof BridgeNetworkStatus) { + this.processBridgeNetworkStatus((BridgeNetworkStatus) descriptor); + } + } + + private void processRelayServerDescriptor( + ServerDescriptor descriptor) { + String fingerprint = descriptor.getFingerprint(); + DetailsStatus detailsStatus = this.documentStore.retrieve( + DetailsStatus.class, true, fingerprint); + String publishedDateTime = + DateTimeHelper.format(descriptor.getPublishedMillis()); + if (detailsStatus == null) { + detailsStatus = new DetailsStatus(); + } else if (detailsStatus.getDescPublished() != null && + publishedDateTime.compareTo( + detailsStatus.getDescPublished()) < 0) { + return; + } + String lastRestartedString = DateTimeHelper.format( + descriptor.getPublishedMillis() - descriptor.getUptime() + * DateTimeHelper.ONE_SECOND); + int bandwidthRate = descriptor.getBandwidthRate(); + int bandwidthBurst = descriptor.getBandwidthBurst(); + int observedBandwidth = descriptor.getBandwidthObserved(); + int advertisedBandwidth = Math.min(bandwidthRate, + Math.min(bandwidthBurst, observedBandwidth)); + detailsStatus.setDescPublished(publishedDateTime); + detailsStatus.setLastRestarted(lastRestartedString); + detailsStatus.setBandwidthRate(bandwidthRate); + detailsStatus.setBandwidthBurst(bandwidthBurst); + detailsStatus.setObservedBandwidth(observedBandwidth); + detailsStatus.setAdvertisedBandwidth(advertisedBandwidth); + detailsStatus.setExitPolicy(descriptor.getExitPolicyLines()); + detailsStatus.setContact(descriptor.getContact()); + detailsStatus.setPlatform(descriptor.getPlatform()); + detailsStatus.setFamily(descriptor.getFamilyEntries()); + if (descriptor.getIpv6DefaultPolicy() != null && + (descriptor.getIpv6DefaultPolicy().equals("accept") || + descriptor.getIpv6DefaultPolicy().equals("reject")) && + descriptor.getIpv6PortList() != null) { + Map<String, List<String>> exitPolicyV6Summary = + new HashMap<String, List<String>>(); + List<String> portsOrPortRanges = Arrays.asList( + descriptor.getIpv6PortList().split(",")); + exitPolicyV6Summary.put(descriptor.getIpv6DefaultPolicy(), + portsOrPortRanges); + detailsStatus.setExitPolicyV6Summary(exitPolicyV6Summary); + } + if (descriptor.isHibernating()) { + detailsStatus.setHibernating(true); + } + this.documentStore.store(detailsStatus, fingerprint); + } + + private Map<String, Map<String, Long>> exitListEntries = + new HashMap<String, Map<String, Long>>(); + + private void processExitList(ExitList exitList) { + for (ExitListEntry exitListEntry : exitList.getExitListEntries()) { + String fingerprint = exitListEntry.getFingerprint(); + if (exitListEntry.getScanMillis() < + this.now - DateTimeHelper.ONE_DAY) { + continue; + } + if (!this.exitListEntries.containsKey(fingerprint)) { + this.exitListEntries.put(fingerprint, + new HashMap<String, Long>()); + } + String exitAddress = exitListEntry.getExitAddress(); + long scanMillis = exitListEntry.getScanMillis(); + if (!this.exitListEntries.get(fingerprint).containsKey(exitAddress) + || this.exitListEntries.get(fingerprint).get(exitAddress) + < scanMillis) { + this.exitListEntries.get(fingerprint).put(exitAddress, + scanMillis); + } + } + } + + private void processRelayNetworkStatusConsensus( + RelayNetworkStatusConsensus consensus) { + long validAfterMillis = consensus.getValidAfterMillis(); + if (validAfterMillis > this.relaysLastValidAfterMillis) { + this.relaysLastValidAfterMillis = validAfterMillis; + } + Set<String> recommendedVersions = null; + if (consensus.getRecommendedServerVersions() != null) { + recommendedVersions = new HashSet<String>(); + for (String recommendedVersion : + consensus.getRecommendedServerVersions()) { + recommendedVersions.add("Tor " + recommendedVersion); + } + } + for (NetworkStatusEntry entry : + consensus.getStatusEntries().values()) { + String nickname = entry.getNickname(); + String fingerprint = entry.getFingerprint(); + String address = entry.getAddress(); + SortedSet<String> orAddressesAndPorts = new TreeSet<String>( + entry.getOrAddresses()); + int orPort = entry.getOrPort(); + int dirPort = entry.getDirPort(); + SortedSet<String> relayFlags = entry.getFlags(); + long consensusWeight = entry.getBandwidth(); + String defaultPolicy = entry.getDefaultPolicy(); + String portList = entry.getPortList(); + Boolean recommendedVersion = (recommendedVersions == null || + entry.getVersion() == null) ? null : + recommendedVersions.contains(entry.getVersion()); + NodeStatus newNodeStatus = new NodeStatus(true, nickname, + fingerprint, address, orAddressesAndPorts, null, + validAfterMillis, orPort, dirPort, relayFlags, consensusWeight, + null, null, -1L, defaultPolicy, portList, validAfterMillis, + validAfterMillis, null, null, recommendedVersion, null); + if (this.knownNodes.containsKey(fingerprint)) { + this.knownNodes.get(fingerprint).update(newNodeStatus); + } else { + this.knownNodes.put(fingerprint, newNodeStatus); + } + } + this.relayConsensusesProcessed++; + if (this.relaysLastValidAfterMillis == validAfterMillis) { + this.lastBandwidthWeights = consensus.getBandwidthWeights(); + } + } + + private void processBridgeServerDescriptor( + ServerDescriptor descriptor) { + String fingerprint = descriptor.getFingerprint(); + DetailsStatus detailsStatus = this.documentStore.retrieve( + DetailsStatus.class, true, fingerprint); + String publishedDateTime = + DateTimeHelper.format(descriptor.getPublishedMillis()); + if (detailsStatus == null) { + detailsStatus = new DetailsStatus(); + } else if (detailsStatus.getDescPublished() != null && + publishedDateTime.compareTo( + detailsStatus.getDescPublished()) < 0) { + return; + } + String lastRestartedString = DateTimeHelper.format( + descriptor.getPublishedMillis() - descriptor.getUptime() + * DateTimeHelper.ONE_SECOND); + int advertisedBandwidth = Math.min(descriptor.getBandwidthRate(), + Math.min(descriptor.getBandwidthBurst(), + descriptor.getBandwidthObserved())); + detailsStatus.setDescPublished(publishedDateTime); + detailsStatus.setLastRestarted(lastRestartedString); + detailsStatus.setAdvertisedBandwidth(advertisedBandwidth); + detailsStatus.setPlatform(descriptor.getPlatform()); + this.documentStore.store(detailsStatus, fingerprint); + } + + private void processBridgePoolAssignment( + BridgePoolAssignment bridgePoolAssignment) { + for (Map.Entry<String, String> e : + bridgePoolAssignment.getEntries().entrySet()) { + String fingerprint = e.getKey(); + String details = e.getValue(); + DetailsStatus detailsStatus = this.documentStore.retrieve( + DetailsStatus.class, true, fingerprint); + if (detailsStatus == null) { + detailsStatus = new DetailsStatus(); + } else if (details.equals(detailsStatus.getPoolAssignment())) { + continue; + } + detailsStatus.setPoolAssignment(details); + this.documentStore.store(detailsStatus, fingerprint); + } + } + + private void processBridgeNetworkStatus(BridgeNetworkStatus status) { + long publishedMillis = status.getPublishedMillis(); + if (publishedMillis > this.bridgesLastPublishedMillis) { + this.bridgesLastPublishedMillis = publishedMillis; + } + for (NetworkStatusEntry entry : status.getStatusEntries().values()) { + String nickname = entry.getNickname(); + String fingerprint = entry.getFingerprint(); + String address = entry.getAddress(); + SortedSet<String> orAddressesAndPorts = new TreeSet<String>( + entry.getOrAddresses()); + int orPort = entry.getOrPort(); + int dirPort = entry.getDirPort(); + SortedSet<String> relayFlags = entry.getFlags(); + NodeStatus newNodeStatus = new NodeStatus(false, nickname, + fingerprint, address, orAddressesAndPorts, null, + publishedMillis, orPort, dirPort, relayFlags, -1L, "??", null, + -1L, null, null, publishedMillis, -1L, null, null, null, null); + if (this.knownNodes.containsKey(fingerprint)) { + this.knownNodes.get(fingerprint).update(newNodeStatus); + } else { + this.knownNodes.put(fingerprint, newNodeStatus); + } + } + this.bridgeStatusesProcessed++; + } + + public void updateStatuses() { + this.readStatusSummary(); + Logger.printStatusTime("Read status summary"); + this.setCurrentNodes(); + Logger.printStatusTime("Set current node fingerprints"); + this.startReverseDomainNameLookups(); + Logger.printStatusTime("Started reverse domain name lookups"); + this.lookUpCitiesAndASes(); + Logger.printStatusTime("Looked up cities and ASes"); + this.setDescriptorPartsOfNodeStatus(); + Logger.printStatusTime("Set descriptor parts of node statuses."); + this.calculatePathSelectionProbabilities(); + Logger.printStatusTime("Calculated path selection probabilities"); + this.finishReverseDomainNameLookups(); + Logger.printStatusTime("Finished reverse domain name lookups"); + this.writeStatusSummary(); + Logger.printStatusTime("Wrote status summary"); + this.updateDetailsStatuses(); + Logger.printStatusTime("Updated exit addresses in details statuses"); + } + + private void readStatusSummary() { + SortedSet<String> fingerprints = this.documentStore.list( + NodeStatus.class); + for (String fingerprint : fingerprints) { + NodeStatus node = this.documentStore.retrieve(NodeStatus.class, + true, fingerprint); + if (node.isRelay()) { + this.relaysLastValidAfterMillis = Math.max( + this.relaysLastValidAfterMillis, node.getLastSeenMillis()); + } else { + this.bridgesLastPublishedMillis = Math.max( + this.bridgesLastPublishedMillis, node.getLastSeenMillis()); + } + if (this.knownNodes.containsKey(fingerprint)) { + this.knownNodes.get(fingerprint).update(node); + } else { + this.knownNodes.put(fingerprint, node); + } + } + } + + private void setCurrentNodes() { + long cutoff = Math.max(this.relaysLastValidAfterMillis, + this.bridgesLastPublishedMillis) - 7L * 24L * 60L * 60L * 1000L; + SortedMap<String, NodeStatus> currentNodes = + new TreeMap<String, NodeStatus>(); + for (Map.Entry<String, NodeStatus> e : this.knownNodes.entrySet()) { + if (e.getValue().getLastSeenMillis() >= cutoff) { + currentNodes.put(e.getKey(), e.getValue()); + } + } + this.relays = new TreeMap<String, NodeStatus>(); + this.bridges = new TreeMap<String, NodeStatus>(); + for (Map.Entry<String, NodeStatus> e : currentNodes.entrySet()) { + if (e.getValue().isRelay()) { + this.relays.put(e.getKey(), e.getValue()); + } else { + this.bridges.put(e.getKey(), e.getValue()); + } + } + } + + private void startReverseDomainNameLookups() { + Map<String, Long> addressLastLookupTimes = + new HashMap<String, Long>(); + for (NodeStatus relay : relays.values()) { + addressLastLookupTimes.put(relay.getAddress(), + relay.getLastRdnsLookup()); + } + this.reverseDomainNameResolver.setAddresses(addressLastLookupTimes); + this.reverseDomainNameResolver.startReverseDomainNameLookups(); + } + + private void lookUpCitiesAndASes() { + SortedSet<String> addressStrings = new TreeSet<String>(); + for (NodeStatus node : this.knownNodes.values()) { + if (node.isRelay()) { + addressStrings.add(node.getAddress()); + } + } + if (addressStrings.isEmpty()) { + System.err.println("No relay IP addresses to resolve to cities or " + + "ASN."); + return; + } + SortedMap<String, LookupResult> lookupResults = + this.lookupService.lookup(addressStrings); + for (NodeStatus node : knownNodes.values()) { + if (!node.isRelay()) { + continue; + } + String addressString = node.getAddress(); + if (lookupResults.containsKey(addressString)) { + LookupResult lookupResult = lookupResults.get(addressString); + node.setCountryCode(lookupResult.getCountryCode()); + node.setCountryName(lookupResult.getCountryName()); + node.setRegionName(lookupResult.getRegionName()); + node.setCityName(lookupResult.getCityName()); + node.setLatitude(lookupResult.getLatitude()); + node.setLongitude(lookupResult.getLongitude()); + node.setASNumber(lookupResult.getAsNumber()); + node.setASName(lookupResult.getAsName()); + } + } + } + + private void setDescriptorPartsOfNodeStatus() { + for (Map.Entry<String, NodeStatus> e : this.knownNodes.entrySet()) { + String fingerprint = e.getKey(); + NodeStatus node = e.getValue(); + if (node.isRelay()) { + if (node.getRelayFlags().contains("Running") && + node.getLastSeenMillis() == this.relaysLastValidAfterMillis) { + node.setRunning(true); + } + DetailsStatus detailsStatus = this.documentStore.retrieve( + DetailsStatus.class, true, fingerprint); + if (detailsStatus != null) { + node.setContact(detailsStatus.getContact()); + if (detailsStatus.getExitAddresses() != null) { + for (Map.Entry<String, Long> ea : + detailsStatus.getExitAddresses().entrySet()) { + if (ea.getValue() >= this.now - DateTimeHelper.ONE_DAY) { + node.addExitAddress(ea.getKey()); + } + } + } + if (detailsStatus.getFamily() != null && + !detailsStatus.getFamily().isEmpty()) { + SortedSet<String> familyFingerprints = new TreeSet<String>(); + for (String familyMember : detailsStatus.getFamily()) { + if (familyMember.startsWith("$") && + familyMember.length() == 41) { + familyFingerprints.add(familyMember.substring(1)); + } + } + if (!familyFingerprints.isEmpty()) { + node.setFamilyFingerprints(familyFingerprints); + } + } + } + } + if (!node.isRelay() && node.getRelayFlags().contains("Running") && + node.getLastSeenMillis() == this.bridgesLastPublishedMillis) { + node.setRunning(true); + } + } + } + + private void calculatePathSelectionProbabilities() { + boolean consensusContainsBandwidthWeights = false; + double wgg = 0.0, wgd = 0.0, wmg = 0.0, wmm = 0.0, wme = 0.0, + wmd = 0.0, wee = 0.0, wed = 0.0; + if (this.lastBandwidthWeights != null) { + SortedSet<String> weightKeys = new TreeSet<String>(Arrays.asList( + "Wgg,Wgd,Wmg,Wmm,Wme,Wmd,Wee,Wed".split(","))); + weightKeys.removeAll(this.lastBandwidthWeights.keySet()); + if (weightKeys.isEmpty()) { + consensusContainsBandwidthWeights = true; + wgg = ((double) this.lastBandwidthWeights.get("Wgg")) / 10000.0; + wgd = ((double) this.lastBandwidthWeights.get("Wgd")) / 10000.0; + wmg = ((double) this.lastBandwidthWeights.get("Wmg")) / 10000.0; + wmm = ((double) this.lastBandwidthWeights.get("Wmm")) / 10000.0; + wme = ((double) this.lastBandwidthWeights.get("Wme")) / 10000.0; + wmd = ((double) this.lastBandwidthWeights.get("Wmd")) / 10000.0; + wee = ((double) this.lastBandwidthWeights.get("Wee")) / 10000.0; + wed = ((double) this.lastBandwidthWeights.get("Wed")) / 10000.0; + } + } else { + System.err.println("Could not determine most recent Wxx parameter " + + "values, probably because we didn't parse a consensus in " + + "this execution. All relays' guard/middle/exit weights are " + + "going to be 0.0."); + } + SortedMap<String, Double> + advertisedBandwidths = new TreeMap<String, Double>(), + consensusWeights = new TreeMap<String, Double>(), + guardWeights = new TreeMap<String, Double>(), + middleWeights = new TreeMap<String, Double>(), + exitWeights = new TreeMap<String, Double>(); + double totalAdvertisedBandwidth = 0.0; + double totalConsensusWeight = 0.0; + double totalGuardWeight = 0.0; + double totalMiddleWeight = 0.0; + double totalExitWeight = 0.0; + for (Map.Entry<String, NodeStatus> e : this.relays.entrySet()) { + String fingerprint = e.getKey(); + NodeStatus relay = e.getValue(); + if (!relay.getRunning()) { + continue; + } + boolean isExit = relay.getRelayFlags().contains("Exit") && + !relay.getRelayFlags().contains("BadExit"); + boolean isGuard = relay.getRelayFlags().contains("Guard"); + DetailsStatus detailsStatus = this.documentStore.retrieve( + DetailsStatus.class, true, fingerprint); + if (detailsStatus != null) { + double advertisedBandwidth = + detailsStatus.getAdvertisedBandwidth(); + if (advertisedBandwidth >= 0.0) { + advertisedBandwidths.put(fingerprint, advertisedBandwidth); + totalAdvertisedBandwidth += advertisedBandwidth; + } + } + double consensusWeight = (double) relay.getConsensusWeight(); + consensusWeights.put(fingerprint, consensusWeight); + totalConsensusWeight += consensusWeight; + if (consensusContainsBandwidthWeights) { + double guardWeight = consensusWeight, + middleWeight = consensusWeight, + exitWeight = consensusWeight; + if (isGuard && isExit) { + guardWeight *= wgd; + middleWeight *= wmd; + exitWeight *= wed; + } else if (isGuard) { + guardWeight *= wgg; + middleWeight *= wmg; + exitWeight = 0.0; + } else if (isExit) { + guardWeight = 0.0; + middleWeight *= wme; + exitWeight *= wee; + } else { + guardWeight = 0.0; + middleWeight *= wmm; + exitWeight = 0.0; + } + guardWeights.put(fingerprint, guardWeight); + middleWeights.put(fingerprint, middleWeight); + exitWeights.put(fingerprint, exitWeight); + totalGuardWeight += guardWeight; + totalMiddleWeight += middleWeight; + totalExitWeight += exitWeight; + } + } + for (Map.Entry<String, NodeStatus> e : this.relays.entrySet()) { + String fingerprint = e.getKey(); + NodeStatus relay = e.getValue(); + if (advertisedBandwidths.containsKey(fingerprint)) { + relay.setAdvertisedBandwidthFraction(advertisedBandwidths.get( + fingerprint) / totalAdvertisedBandwidth); + } + if (consensusWeights.containsKey(fingerprint)) { + relay.setConsensusWeightFraction(consensusWeights.get(fingerprint) + / totalConsensusWeight); + } + if (guardWeights.containsKey(fingerprint)) { + relay.setGuardProbability(guardWeights.get(fingerprint) + / totalGuardWeight); + } + if (middleWeights.containsKey(fingerprint)) { + relay.setMiddleProbability(middleWeights.get(fingerprint) + / totalMiddleWeight); + } + if (exitWeights.containsKey(fingerprint)) { + relay.setExitProbability(exitWeights.get(fingerprint) + / totalExitWeight); + } + } + } + + private void finishReverseDomainNameLookups() { + this.reverseDomainNameResolver.finishReverseDomainNameLookups(); + Map<String, String> lookupResults = + this.reverseDomainNameResolver.getLookupResults(); + long startedRdnsLookups = + this.reverseDomainNameResolver.getLookupStartMillis(); + for (NodeStatus relay : relays.values()) { + if (lookupResults.containsKey(relay.getAddress())) { + relay.setHostName(lookupResults.get(relay.getAddress())); + relay.setLastRdnsLookup(startedRdnsLookups); + } + } + } + + private void writeStatusSummary() { + for (Map.Entry<String, NodeStatus> e : this.knownNodes.entrySet()) { + this.documentStore.store(e.getValue(), e.getKey()); + } + } + + private void updateDetailsStatuses() { + SortedSet<String> fingerprints = new TreeSet<String>(); + fingerprints.addAll(this.exitListEntries.keySet()); + for (String fingerprint : fingerprints) { + DetailsStatus detailsStatus = this.documentStore.retrieve( + DetailsStatus.class, true, fingerprint); + if (detailsStatus == null) { + detailsStatus = new DetailsStatus(); + } + Map<String, Long> exitAddresses = new HashMap<String, Long>(); + if (detailsStatus.getExitAddresses() != null) { + for (Map.Entry<String, Long> e : + detailsStatus.getExitAddresses().entrySet()) { + if (e.getValue() >= this.now - DateTimeHelper.ONE_DAY) { + exitAddresses.put(e.getKey(), e.getValue()); + } + } + } + if (this.exitListEntries.containsKey(fingerprint)) { + for (Map.Entry<String, Long> e : + this.exitListEntries.get(fingerprint).entrySet()) { + if (!exitAddresses.containsKey(e.getKey()) || + exitAddresses.get(e.getKey()) < e.getValue()) { + exitAddresses.put(e.getKey(), e.getValue()); + } + } + } + if (this.knownNodes.containsKey(fingerprint)) { + for (String orAddress : + this.knownNodes.get(fingerprint).getOrAddresses()) { + this.exitListEntries.remove(orAddress); + } + } + detailsStatus.setExitAddresses(exitAddresses); + this.documentStore.store(detailsStatus, fingerprint); + } + } + + public String getStatsString() { + StringBuilder sb = new StringBuilder(); + sb.append(" " + Logger.formatDecimalNumber( + relayConsensusesProcessed) + " relay consensuses processed\n"); + sb.append(" " + Logger.formatDecimalNumber(bridgeStatusesProcessed) + + " bridge statuses processed\n"); + return sb.toString(); + } +} + diff --git a/src/org/torproject/onionoo/updater/ReverseDomainNameResolver.java b/src/org/torproject/onionoo/updater/ReverseDomainNameResolver.java new file mode 100644 index 0000000..8ca7eb4 --- /dev/null +++ b/src/org/torproject/onionoo/updater/ReverseDomainNameResolver.java @@ -0,0 +1,179 @@ +/* Copyright 2013 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.updater; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.torproject.onionoo.util.ApplicationFactory; +import org.torproject.onionoo.util.DateTimeHelper; +import org.torproject.onionoo.util.Logger; +import org.torproject.onionoo.util.Time; + +public class ReverseDomainNameResolver { + + private class RdnsLookupWorker extends Thread { + public void run() { + while (time.currentTimeMillis() - RDNS_LOOKUP_MAX_DURATION_MILLIS + <= startedRdnsLookups) { + String rdnsLookupJob = null; + synchronized (rdnsLookupJobs) { + for (String job : rdnsLookupJobs) { + rdnsLookupJob = job; + rdnsLookupJobs.remove(job); + break; + } + } + if (rdnsLookupJob == null) { + break; + } + RdnsLookupRequest request = new RdnsLookupRequest(this, + rdnsLookupJob); + request.setDaemon(true); + request.start(); + try { + Thread.sleep(RDNS_LOOKUP_MAX_REQUEST_MILLIS); + } catch (InterruptedException e) { + /* Getting interrupted should be the default case. */ + } + String hostName = request.getHostName(); + if (hostName != null) { + synchronized (rdnsLookupResults) { + rdnsLookupResults.put(rdnsLookupJob, hostName); + } + } + long lookupMillis = request.getLookupMillis(); + if (lookupMillis >= 0L) { + synchronized (rdnsLookupMillis) { + rdnsLookupMillis.add(lookupMillis); + } + } + } + } + } + + private class RdnsLookupRequest extends Thread { + private RdnsLookupWorker parent; + private String address, hostName; + private long lookupStartedMillis = -1L, lookupCompletedMillis = -1L; + public RdnsLookupRequest(RdnsLookupWorker parent, String address) { + this.parent = parent; + this.address = address; + } + public void run() { + this.lookupStartedMillis = time.currentTimeMillis(); + try { + String result = InetAddress.getByName(this.address).getHostName(); + synchronized (this) { + this.hostName = result; + } + } catch (UnknownHostException e) { + /* We'll try again the next time. */ + } + this.lookupCompletedMillis = time.currentTimeMillis(); + this.parent.interrupt(); + } + public synchronized String getHostName() { + return hostName; + } + public synchronized long getLookupMillis() { + return this.lookupCompletedMillis - this.lookupStartedMillis; + } + } + + private Time time; + + public ReverseDomainNameResolver() { + this.time = ApplicationFactory.getTime(); + } + + private static final long RDNS_LOOKUP_MAX_REQUEST_MILLIS = + DateTimeHelper.TEN_SECONDS; + private static final long RDNS_LOOKUP_MAX_DURATION_MILLIS = + DateTimeHelper.FIVE_MINUTES; + private static final long RDNS_LOOKUP_MAX_AGE_MILLIS = + DateTimeHelper.TWELVE_HOURS; + private static final int RDNS_LOOKUP_WORKERS_NUM = 5; + + private Map<String, Long> addressLastLookupTimes; + + private Set<String> rdnsLookupJobs; + + private Map<String, String> rdnsLookupResults; + + private List<Long> rdnsLookupMillis; + + private long startedRdnsLookups; + + private List<RdnsLookupWorker> rdnsLookupWorkers; + + public void setAddresses(Map<String, Long> addressLastLookupTimes) { + this.addressLastLookupTimes = addressLastLookupTimes; + } + + public void startReverseDomainNameLookups() { + this.startedRdnsLookups = this.time.currentTimeMillis(); + this.rdnsLookupJobs = new HashSet<String>(); + for (Map.Entry<String, Long> e : + this.addressLastLookupTimes.entrySet()) { + if (e.getValue() < this.startedRdnsLookups + - RDNS_LOOKUP_MAX_AGE_MILLIS) { + this.rdnsLookupJobs.add(e.getKey()); + } + } + this.rdnsLookupResults = new HashMap<String, String>(); + this.rdnsLookupMillis = new ArrayList<Long>(); + this.rdnsLookupWorkers = new ArrayList<RdnsLookupWorker>(); + for (int i = 0; i < RDNS_LOOKUP_WORKERS_NUM; i++) { + RdnsLookupWorker rdnsLookupWorker = new RdnsLookupWorker(); + this.rdnsLookupWorkers.add(rdnsLookupWorker); + rdnsLookupWorker.setDaemon(true); + rdnsLookupWorker.start(); + } + } + + public void finishReverseDomainNameLookups() { + for (RdnsLookupWorker rdnsLookupWorker : this.rdnsLookupWorkers) { + try { + rdnsLookupWorker.join(); + } catch (InterruptedException e) { + /* This is not something that we can take care of. Just leave the + * worker thread alone. */ + } + } + } + + public Map<String, String> getLookupResults() { + synchronized (this.rdnsLookupResults) { + return new HashMap<String, String>(this.rdnsLookupResults); + } + } + + public long getLookupStartMillis() { + return this.startedRdnsLookups; + } + + public String getStatsString() { + StringBuilder sb = new StringBuilder(); + sb.append(" " + Logger.formatDecimalNumber(rdnsLookupMillis.size()) + + " lookups performed\n"); + if (rdnsLookupMillis.size() > 0) { + Collections.sort(rdnsLookupMillis); + sb.append(" " + Logger.formatMillis(rdnsLookupMillis.get(0)) + + " minimum lookup time\n"); + sb.append(" " + Logger.formatMillis(rdnsLookupMillis.get( + rdnsLookupMillis.size() / 2)) + " median lookup time\n"); + sb.append(" " + Logger.formatMillis(rdnsLookupMillis.get( + rdnsLookupMillis.size() - 1)) + " maximum lookup time\n"); + } + return sb.toString(); + } +} + diff --git a/src/org/torproject/onionoo/updater/StatusUpdater.java b/src/org/torproject/onionoo/updater/StatusUpdater.java new file mode 100644 index 0000000..9fc34d3 --- /dev/null +++ b/src/org/torproject/onionoo/updater/StatusUpdater.java @@ -0,0 +1,11 @@ +/* Copyright 2014 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.updater; + +public interface StatusUpdater { + + public abstract void updateStatuses(); + + public abstract String getStatsString(); +} + diff --git a/src/org/torproject/onionoo/updater/UptimeStatusUpdater.java b/src/org/torproject/onionoo/updater/UptimeStatusUpdater.java new file mode 100644 index 0000000..dd71e74 --- /dev/null +++ b/src/org/torproject/onionoo/updater/UptimeStatusUpdater.java @@ -0,0 +1,130 @@ +/* Copyright 2014 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.updater; + +import java.util.Map; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; + +import org.torproject.descriptor.BridgeNetworkStatus; +import org.torproject.descriptor.Descriptor; +import org.torproject.descriptor.NetworkStatusEntry; +import org.torproject.descriptor.RelayNetworkStatusConsensus; +import org.torproject.onionoo.docs.UptimeStatus; +import org.torproject.onionoo.util.ApplicationFactory; +import org.torproject.onionoo.util.DateTimeHelper; +import org.torproject.onionoo.util.Logger; + +public class UptimeStatusUpdater implements DescriptorListener, + StatusUpdater { + + private DescriptorSource descriptorSource; + + public UptimeStatusUpdater() { + this.descriptorSource = ApplicationFactory.getDescriptorSource(); + this.registerDescriptorListeners(); + } + + private void registerDescriptorListeners() { + this.descriptorSource.registerDescriptorListener(this, + DescriptorType.RELAY_CONSENSUSES); + this.descriptorSource.registerDescriptorListener(this, + DescriptorType.BRIDGE_STATUSES); + } + + public void processDescriptor(Descriptor descriptor, boolean relay) { + if (descriptor instanceof RelayNetworkStatusConsensus) { + this.processRelayNetworkStatusConsensus( + (RelayNetworkStatusConsensus) descriptor); + } else if (descriptor instanceof BridgeNetworkStatus) { + this.processBridgeNetworkStatus( + (BridgeNetworkStatus) descriptor); + } + } + + private SortedSet<Long> newRelayStatuses = new TreeSet<Long>(), + newBridgeStatuses = new TreeSet<Long>(); + private SortedMap<String, SortedSet<Long>> + newRunningRelays = new TreeMap<String, SortedSet<Long>>(), + newRunningBridges = new TreeMap<String, SortedSet<Long>>(); + + private void processRelayNetworkStatusConsensus( + RelayNetworkStatusConsensus consensus) { + SortedSet<String> fingerprints = new TreeSet<String>(); + for (NetworkStatusEntry entry : + consensus.getStatusEntries().values()) { + if (entry.getFlags().contains("Running")) { + fingerprints.add(entry.getFingerprint()); + } + } + if (!fingerprints.isEmpty()) { + long dateHourMillis = (consensus.getValidAfterMillis() + / DateTimeHelper.ONE_HOUR) * DateTimeHelper.ONE_HOUR; + for (String fingerprint : fingerprints) { + if (!this.newRunningRelays.containsKey(fingerprint)) { + this.newRunningRelays.put(fingerprint, new TreeSet<Long>()); + } + this.newRunningRelays.get(fingerprint).add(dateHourMillis); + } + this.newRelayStatuses.add(dateHourMillis); + } + } + + private void processBridgeNetworkStatus(BridgeNetworkStatus status) { + SortedSet<String> fingerprints = new TreeSet<String>(); + for (NetworkStatusEntry entry : + status.getStatusEntries().values()) { + if (entry.getFlags().contains("Running")) { + fingerprints.add(entry.getFingerprint()); + } + } + if (!fingerprints.isEmpty()) { + long dateHourMillis = (status.getPublishedMillis() + / DateTimeHelper.ONE_HOUR) * DateTimeHelper.ONE_HOUR; + for (String fingerprint : fingerprints) { + if (!this.newRunningBridges.containsKey(fingerprint)) { + this.newRunningBridges.put(fingerprint, new TreeSet<Long>()); + } + this.newRunningBridges.get(fingerprint).add(dateHourMillis); + } + this.newBridgeStatuses.add(dateHourMillis); + } + } + + public void updateStatuses() { + for (Map.Entry<String, SortedSet<Long>> e : + this.newRunningRelays.entrySet()) { + this.updateStatus(true, e.getKey(), e.getValue()); + } + this.updateStatus(true, null, this.newRelayStatuses); + for (Map.Entry<String, SortedSet<Long>> e : + this.newRunningBridges.entrySet()) { + this.updateStatus(false, e.getKey(), e.getValue()); + } + this.updateStatus(false, null, this.newBridgeStatuses); + } + + private void updateStatus(boolean relay, String fingerprint, + SortedSet<Long> newUptimeHours) { + UptimeStatus uptimeStatus = UptimeStatus.loadOrCreate(fingerprint); + uptimeStatus.addToHistory(relay, newUptimeHours); + uptimeStatus.storeIfChanged(); + } + + public String getStatsString() { + StringBuilder sb = new StringBuilder(); + sb.append(" " + Logger.formatDecimalNumber( + this.newRelayStatuses.size()) + " hours of relay uptimes " + + "processed\n"); + sb.append(" " + Logger.formatDecimalNumber( + this.newBridgeStatuses.size()) + " hours of bridge uptimes " + + "processed\n"); + sb.append(" " + Logger.formatDecimalNumber( + this.newRunningRelays.size() + this.newRunningBridges.size()) + + " uptime status files updated\n"); + return sb.toString(); + } +} + diff --git a/src/org/torproject/onionoo/updater/WeightsStatusUpdater.java b/src/org/torproject/onionoo/updater/WeightsStatusUpdater.java new file mode 100644 index 0000000..333afcc --- /dev/null +++ b/src/org/torproject/onionoo/updater/WeightsStatusUpdater.java @@ -0,0 +1,332 @@ +/* Copyright 2012--2014 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.updater; + +import java.util.Arrays; +import java.util.Map; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; + +import org.torproject.descriptor.Descriptor; +import org.torproject.descriptor.NetworkStatusEntry; +import org.torproject.descriptor.RelayNetworkStatusConsensus; +import org.torproject.descriptor.ServerDescriptor; +import org.torproject.onionoo.docs.DocumentStore; +import org.torproject.onionoo.docs.WeightsStatus; +import org.torproject.onionoo.util.ApplicationFactory; +import org.torproject.onionoo.util.DateTimeHelper; + +public class WeightsStatusUpdater implements DescriptorListener, + StatusUpdater { + + private DescriptorSource descriptorSource; + + private DocumentStore documentStore; + + private long now; + + public WeightsStatusUpdater() { + this.descriptorSource = ApplicationFactory.getDescriptorSource(); + this.documentStore = ApplicationFactory.getDocumentStore(); + this.now = ApplicationFactory.getTime().currentTimeMillis(); + this.registerDescriptorListeners(); + } + + private void registerDescriptorListeners() { + this.descriptorSource.registerDescriptorListener(this, + DescriptorType.RELAY_CONSENSUSES); + this.descriptorSource.registerDescriptorListener(this, + DescriptorType.RELAY_SERVER_DESCRIPTORS); + } + + public void processDescriptor(Descriptor descriptor, boolean relay) { + if (descriptor instanceof ServerDescriptor) { + this.processRelayServerDescriptor((ServerDescriptor) descriptor); + } else if (descriptor instanceof RelayNetworkStatusConsensus) { + this.processRelayNetworkConsensus( + (RelayNetworkStatusConsensus) descriptor); + } + } + + public void updateStatuses() { + /* Nothing to do. */ + } + + private void processRelayNetworkConsensus( + RelayNetworkStatusConsensus consensus) { + long validAfterMillis = consensus.getValidAfterMillis(), + freshUntilMillis = consensus.getFreshUntilMillis(); + SortedMap<String, double[]> pathSelectionWeights = + this.calculatePathSelectionProbabilities(consensus); + this.updateWeightsHistory(validAfterMillis, freshUntilMillis, + pathSelectionWeights); + } + + private void processRelayServerDescriptor( + ServerDescriptor serverDescriptor) { + String digest = serverDescriptor.getServerDescriptorDigest(). + toUpperCase(); + int advertisedBandwidth = Math.min(Math.min( + serverDescriptor.getBandwidthBurst(), + serverDescriptor.getBandwidthObserved()), + serverDescriptor.getBandwidthRate()); + String fingerprint = serverDescriptor.getFingerprint(); + WeightsStatus weightsStatus = this.documentStore.retrieve( + WeightsStatus.class, true, fingerprint); + if (weightsStatus == null) { + weightsStatus = new WeightsStatus(); + } + weightsStatus.getAdvertisedBandwidths().put(digest, + advertisedBandwidth); + this.documentStore.store(weightsStatus, fingerprint); +} + + private void updateWeightsHistory(long validAfterMillis, + long freshUntilMillis, + SortedMap<String, double[]> pathSelectionWeights) { + String fingerprint = null; + double[] weights = null; + do { + fingerprint = null; + synchronized (pathSelectionWeights) { + if (!pathSelectionWeights.isEmpty()) { + fingerprint = pathSelectionWeights.firstKey(); + weights = pathSelectionWeights.remove(fingerprint); + } + } + if (fingerprint != null) { + this.addToHistory(fingerprint, validAfterMillis, + freshUntilMillis, weights); + } + } while (fingerprint != null); + } + + private SortedMap<String, double[]> calculatePathSelectionProbabilities( + RelayNetworkStatusConsensus consensus) { + boolean containsBandwidthWeights = false; + double wgg = 1.0, wgd = 1.0, wmg = 1.0, wmm = 1.0, wme = 1.0, + wmd = 1.0, wee = 1.0, wed = 1.0; + SortedMap<String, Integer> bandwidthWeights = + consensus.getBandwidthWeights(); + if (bandwidthWeights != null) { + SortedSet<String> missingWeightKeys = new TreeSet<String>( + Arrays.asList("Wgg,Wgd,Wmg,Wmm,Wme,Wmd,Wee,Wed".split(","))); + missingWeightKeys.removeAll(bandwidthWeights.keySet()); + if (missingWeightKeys.isEmpty()) { + wgg = ((double) bandwidthWeights.get("Wgg")) / 10000.0; + wgd = ((double) bandwidthWeights.get("Wgd")) / 10000.0; + wmg = ((double) bandwidthWeights.get("Wmg")) / 10000.0; + wmm = ((double) bandwidthWeights.get("Wmm")) / 10000.0; + wme = ((double) bandwidthWeights.get("Wme")) / 10000.0; + wmd = ((double) bandwidthWeights.get("Wmd")) / 10000.0; + wee = ((double) bandwidthWeights.get("Wee")) / 10000.0; + wed = ((double) bandwidthWeights.get("Wed")) / 10000.0; + containsBandwidthWeights = true; + } + } + SortedMap<String, Double> + advertisedBandwidths = new TreeMap<String, Double>(), + consensusWeights = new TreeMap<String, Double>(), + guardWeights = new TreeMap<String, Double>(), + middleWeights = new TreeMap<String, Double>(), + exitWeights = new TreeMap<String, Double>(); + double totalAdvertisedBandwidth = 0.0; + double totalConsensusWeight = 0.0; + double totalGuardWeight = 0.0; + double totalMiddleWeight = 0.0; + double totalExitWeight = 0.0; + for (NetworkStatusEntry relay : + consensus.getStatusEntries().values()) { + String fingerprint = relay.getFingerprint(); + if (!relay.getFlags().contains("Running")) { + continue; + } + String digest = relay.getDescriptor().toUpperCase(); + WeightsStatus weightsStatus = this.documentStore.retrieve( + WeightsStatus.class, true, fingerprint); + if (weightsStatus != null && + weightsStatus.getAdvertisedBandwidths() != null && + weightsStatus.getAdvertisedBandwidths().containsKey(digest)) { + /* Read advertised bandwidth from weights status file. Server + * descriptors are parsed before consensuses, so we're sure that + * if there's a server descriptor for this relay, it'll be + * contained in the weights status file by now. */ + double advertisedBandwidth = + (double) weightsStatus.getAdvertisedBandwidths().get(digest); + advertisedBandwidths.put(fingerprint, advertisedBandwidth); + totalAdvertisedBandwidth += advertisedBandwidth; + } + if (relay.getBandwidth() >= 0L) { + double consensusWeight = (double) relay.getBandwidth(); + consensusWeights.put(fingerprint, consensusWeight); + totalConsensusWeight += consensusWeight; + if (containsBandwidthWeights) { + double guardWeight = (double) relay.getBandwidth(); + double middleWeight = (double) relay.getBandwidth(); + double exitWeight = (double) relay.getBandwidth(); + boolean isExit = relay.getFlags().contains("Exit") && + !relay.getFlags().contains("BadExit"); + boolean isGuard = relay.getFlags().contains("Guard"); + if (isGuard && isExit) { + guardWeight *= wgd; + middleWeight *= wmd; + exitWeight *= wed; + } else if (isGuard) { + guardWeight *= wgg; + middleWeight *= wmg; + exitWeight = 0.0; + } else if (isExit) { + guardWeight = 0.0; + middleWeight *= wme; + exitWeight *= wee; + } else { + guardWeight = 0.0; + middleWeight *= wmm; + exitWeight = 0.0; + } + guardWeights.put(fingerprint, guardWeight); + middleWeights.put(fingerprint, middleWeight); + exitWeights.put(fingerprint, exitWeight); + totalGuardWeight += guardWeight; + totalMiddleWeight += middleWeight; + totalExitWeight += exitWeight; + } + } + } + SortedMap<String, double[]> pathSelectionProbabilities = + new TreeMap<String, double[]>(); + SortedSet<String> fingerprints = new TreeSet<String>(); + fingerprints.addAll(consensusWeights.keySet()); + fingerprints.addAll(advertisedBandwidths.keySet()); + for (String fingerprint : fingerprints) { + double[] probabilities = new double[] { -1.0, -1.0, -1.0, -1.0, + -1.0, -1.0, -1.0 }; + if (consensusWeights.containsKey(fingerprint) && + totalConsensusWeight > 0.0) { + probabilities[1] = consensusWeights.get(fingerprint) / + totalConsensusWeight; + probabilities[6] = consensusWeights.get(fingerprint); + } + if (guardWeights.containsKey(fingerprint) && + totalGuardWeight > 0.0) { + probabilities[2] = guardWeights.get(fingerprint) / + totalGuardWeight; + } + if (middleWeights.containsKey(fingerprint) && + totalMiddleWeight > 0.0) { + probabilities[3] = middleWeights.get(fingerprint) / + totalMiddleWeight; + } + if (exitWeights.containsKey(fingerprint) && + totalExitWeight > 0.0) { + probabilities[4] = exitWeights.get(fingerprint) / + totalExitWeight; + } + if (advertisedBandwidths.containsKey(fingerprint) && + totalAdvertisedBandwidth > 0.0) { + probabilities[0] = advertisedBandwidths.get(fingerprint) + / totalAdvertisedBandwidth; + probabilities[5] = advertisedBandwidths.get(fingerprint); + } + pathSelectionProbabilities.put(fingerprint, probabilities); + } + return pathSelectionProbabilities; + } + + private void addToHistory(String fingerprint, long validAfterMillis, + long freshUntilMillis, double[] weights) { + WeightsStatus weightsStatus = this.documentStore.retrieve( + WeightsStatus.class, true, fingerprint); + if (weightsStatus == null) { + weightsStatus = new WeightsStatus(); + } + SortedMap<long[], double[]> history = weightsStatus.getHistory(); + long[] interval = new long[] { validAfterMillis, freshUntilMillis }; + if ((history.headMap(interval).isEmpty() || + history.headMap(interval).lastKey()[1] <= validAfterMillis) && + (history.tailMap(interval).isEmpty() || + history.tailMap(interval).firstKey()[0] >= freshUntilMillis)) { + history.put(interval, weights); + this.compressHistory(weightsStatus); + this.documentStore.store(weightsStatus, fingerprint); + } + } + + private void compressHistory(WeightsStatus weightsStatus) { + SortedMap<long[], double[]> history = weightsStatus.getHistory(); + SortedMap<long[], double[]> compressedHistory = + new TreeMap<long[], double[]>(history.comparator()); + long lastStartMillis = 0L, lastEndMillis = 0L; + double[] lastWeights = null; + String lastMonthString = "1970-01"; + int lastMissingValues = -1; + for (Map.Entry<long[], double[]> e : history.entrySet()) { + long startMillis = e.getKey()[0], endMillis = e.getKey()[1]; + double[] weights = e.getValue(); + long intervalLengthMillis; + if (this.now - endMillis <= DateTimeHelper.ONE_WEEK) { + intervalLengthMillis = DateTimeHelper.ONE_HOUR; + } else if (this.now - endMillis <= + DateTimeHelper.ROUGHLY_ONE_MONTH) { + intervalLengthMillis = DateTimeHelper.FOUR_HOURS; + } else if (this.now - endMillis <= + DateTimeHelper.ROUGHLY_THREE_MONTHS) { + intervalLengthMillis = DateTimeHelper.TWELVE_HOURS; + } else if (this.now - endMillis <= + DateTimeHelper.ROUGHLY_ONE_YEAR) { + intervalLengthMillis = DateTimeHelper.TWO_DAYS; + } else { + intervalLengthMillis = DateTimeHelper.TEN_DAYS; + } + String monthString = DateTimeHelper.format(startMillis, + DateTimeHelper.ISO_YEARMONTH_FORMAT); + int missingValues = 0; + for (int i = 0; i < weights.length; i++) { + if (weights[i] < -0.5) { + missingValues += 1 << i; + } + } + if (lastEndMillis == startMillis && + ((lastEndMillis - 1L) / intervalLengthMillis) == + ((endMillis - 1L) / intervalLengthMillis) && + lastMonthString.equals(monthString) && + lastMissingValues == missingValues) { + double lastIntervalInHours = (double) ((lastEndMillis + - lastStartMillis) / DateTimeHelper.ONE_HOUR); + double currentIntervalInHours = (double) ((endMillis + - startMillis) / DateTimeHelper.ONE_HOUR); + double newIntervalInHours = (double) ((endMillis + - lastStartMillis) / DateTimeHelper.ONE_HOUR); + for (int i = 0; i < lastWeights.length; i++) { + lastWeights[i] *= lastIntervalInHours; + lastWeights[i] += weights[i] * currentIntervalInHours; + lastWeights[i] /= newIntervalInHours; + } + lastEndMillis = endMillis; + } else { + if (lastStartMillis > 0L) { + compressedHistory.put(new long[] { lastStartMillis, + lastEndMillis }, lastWeights); + } + lastStartMillis = startMillis; + lastEndMillis = endMillis; + lastWeights = weights; + } + lastMonthString = monthString; + lastMissingValues = missingValues; + } + if (lastStartMillis > 0L) { + compressedHistory.put(new long[] { lastStartMillis, lastEndMillis }, + lastWeights); + } + weightsStatus.setHistory(compressedHistory); + } + + public String getStatsString() { + /* TODO Add statistics string. */ + return null; + } +} + diff --git a/src/org/torproject/onionoo/util/ApplicationFactory.java b/src/org/torproject/onionoo/util/ApplicationFactory.java new file mode 100644 index 0000000..8eafca9 --- /dev/null +++ b/src/org/torproject/onionoo/util/ApplicationFactory.java @@ -0,0 +1,55 @@ +/* Copyright 2014 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.util; + +import org.torproject.onionoo.docs.DocumentStore; +import org.torproject.onionoo.server.NodeIndexer; +import org.torproject.onionoo.updater.DescriptorSource; + +public class ApplicationFactory { + + private static Time timeInstance; + public static void setTime(Time time) { + timeInstance = time; + } + public static Time getTime() { + if (timeInstance == null) { + timeInstance = new Time(); + } + return timeInstance; + } + + private static DescriptorSource descriptorSourceInstance; + public static void setDescriptorSource( + DescriptorSource descriptorSource) { + descriptorSourceInstance = descriptorSource; + } + public static DescriptorSource getDescriptorSource() { + if (descriptorSourceInstance == null) { + descriptorSourceInstance = new DescriptorSource(); + } + return descriptorSourceInstance; + } + + private static DocumentStore documentStoreInstance; + public static void setDocumentStore(DocumentStore documentStore) { + documentStoreInstance = documentStore; + } + public static DocumentStore getDocumentStore() { + if (documentStoreInstance == null) { + documentStoreInstance = new DocumentStore(); + } + return documentStoreInstance; + } + + private static NodeIndexer nodeIndexerInstance; + public static void setNodeIndexer(NodeIndexer nodeIndexer) { + nodeIndexerInstance = nodeIndexer; + } + public static NodeIndexer getNodeIndexer() { + if (nodeIndexerInstance == null) { + nodeIndexerInstance = new NodeIndexer(); + } + return nodeIndexerInstance; + } +} diff --git a/src/org/torproject/onionoo/util/DateTimeHelper.java b/src/org/torproject/onionoo/util/DateTimeHelper.java new file mode 100644 index 0000000..1fcf6e1 --- /dev/null +++ b/src/org/torproject/onionoo/util/DateTimeHelper.java @@ -0,0 +1,92 @@ +/* Copyright 2014 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.util; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.HashMap; +import java.util.Map; +import java.util.TimeZone; + +public class DateTimeHelper { + + private DateTimeHelper() { + } + + public static final long ONE_SECOND = 1000L, + TEN_SECONDS = 10L * ONE_SECOND, + ONE_MINUTE = 60L * ONE_SECOND, + FIVE_MINUTES = 5L * ONE_MINUTE, + FIFTEEN_MINUTES = 15L * ONE_MINUTE, + ONE_HOUR = 60L * ONE_MINUTE, + FOUR_HOURS = 4L * ONE_HOUR, + SIX_HOURS = 6L * ONE_HOUR, + TWELVE_HOURS = 12L * ONE_HOUR, + ONE_DAY = 24L * ONE_HOUR, + TWO_DAYS = 2L * ONE_DAY, + THREE_DAYS = 3L * ONE_DAY, + ONE_WEEK = 7L * ONE_DAY, + TEN_DAYS = 10L * ONE_DAY, + ROUGHLY_ONE_MONTH = 31L * ONE_DAY, + ROUGHLY_THREE_MONTHS = 92L * ONE_DAY, + ROUGHLY_ONE_YEAR = 366L * ONE_DAY, + ROUGHLY_FIVE_YEARS = 5L * ROUGHLY_ONE_YEAR; + + public static final String ISO_DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; + + public static final String ISO_DATETIME_TAB_FORMAT = + "yyyy-MM-dd\tHH:mm:ss"; + + public static final String ISO_YEARMONTH_FORMAT = "yyyy-MM"; + + public static final String DATEHOUR_NOSPACE_FORMAT = "yyyy-MM-dd-HH"; + + private static ThreadLocal<Map<String, DateFormat>> dateFormats = + new ThreadLocal<Map<String, DateFormat>> () { + public Map<String, DateFormat> get() { + return super.get(); + } + protected Map<String, DateFormat> initialValue() { + return new HashMap<String, DateFormat>(); + } + public void remove() { + super.remove(); + } + public void set(Map<String, DateFormat> value) { + super.set(value); + } + }; + + private static DateFormat getDateFormat(String format) { + Map<String, DateFormat> threadDateFormats = dateFormats.get(); + if (!threadDateFormats.containsKey(format)) { + DateFormat dateFormat = new SimpleDateFormat(format); + dateFormat.setLenient(false); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + threadDateFormats.put(format, dateFormat); + } + return threadDateFormats.get(format); + } + + public static String format(long millis, String format) { + return getDateFormat(format).format(millis); + } + + public static String format(long millis) { + return format(millis, ISO_DATETIME_FORMAT); + } + + public static long parse(String string, String format) { + try { + return getDateFormat(format).parse(string).getTime(); + } catch (ParseException e) { + return -1L; + } + } + + public static long parse(String string) { + return parse(string, ISO_DATETIME_FORMAT); + } +} + diff --git a/src/org/torproject/onionoo/util/LockFile.java b/src/org/torproject/onionoo/util/LockFile.java new file mode 100644 index 0000000..01c4dcb --- /dev/null +++ b/src/org/torproject/onionoo/util/LockFile.java @@ -0,0 +1,43 @@ +/* Copyright 2013 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.util; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; + +public class LockFile { + + private final File lockFile = new File("lock"); + + public boolean acquireLock() { + Time time = ApplicationFactory.getTime(); + try { + if (this.lockFile.exists()) { + return false; + } + if (this.lockFile.getParentFile() != null) { + this.lockFile.getParentFile().mkdirs(); + } + BufferedWriter bw = new BufferedWriter(new FileWriter( + this.lockFile)); + bw.append("" + time.currentTimeMillis() + "\n"); + bw.close(); + return true; + } catch (IOException e) { + System.err.println("Caught exception while trying to acquire " + + "lock!"); + e.printStackTrace(); + return false; + } + } + + public boolean releaseLock() { + if (this.lockFile.exists()) { + this.lockFile.delete(); + } + return !this.lockFile.exists(); + } +} + diff --git a/src/org/torproject/onionoo/util/Logger.java b/src/org/torproject/onionoo/util/Logger.java new file mode 100644 index 0000000..443c1ca --- /dev/null +++ b/src/org/torproject/onionoo/util/Logger.java @@ -0,0 +1,81 @@ +/* Copyright 2013 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.util; + +import java.util.Date; + +public class Logger { + + private Logger() { + } + + private static Time time; + + public static void setTime() { + time = ApplicationFactory.getTime(); + } + + private static long currentTimeMillis() { + if (time == null) { + return System.currentTimeMillis(); + } else { + return time.currentTimeMillis(); + } + } + + public static String formatDecimalNumber(long decimalNumber) { + return String.format("%,d", decimalNumber); + } + + public static String formatMillis(long millis) { + return String.format("%02d:%02d.%03d minutes", + millis / DateTimeHelper.ONE_MINUTE, + (millis % DateTimeHelper.ONE_MINUTE) / DateTimeHelper.ONE_SECOND, + millis % DateTimeHelper.ONE_SECOND); + } + + public static String formatBytes(long bytes) { + if (bytes < 1024) { + return bytes + " B"; + } else { + int exp = (int) (Math.log(bytes) / Math.log(1024)); + return String.format("%.1f %siB", bytes / Math.pow(1024, exp), + "KMGTPE".charAt(exp-1)); + } + } + + private static long printedLastStatusMessage = -1L; + + public static void printStatus(String message) { + System.out.println(new Date() + ": " + message); + printedLastStatusMessage = currentTimeMillis(); + } + + public static void printStatistics(String component, String message) { + System.out.print(" " + component + " statistics:\n" + message); + } + + public static void printStatusTime(String message) { + printStatusOrErrorTime(message, false); + } + + public static void printErrorTime(String message) { + printStatusOrErrorTime(message, true); + } + + private static void printStatusOrErrorTime(String message, + boolean printToSystemErr) { + long now = currentTimeMillis(); + long millis = printedLastStatusMessage < 0 ? 0 : + now - printedLastStatusMessage; + String line = " " + message + " (" + Logger.formatMillis(millis) + + ")."; + if (printToSystemErr) { + System.err.println(line); + } else { + System.out.println(line); + } + printedLastStatusMessage = now; + } +} + diff --git a/src/org/torproject/onionoo/util/Time.java b/src/org/torproject/onionoo/util/Time.java new file mode 100644 index 0000000..126a910 --- /dev/null +++ b/src/org/torproject/onionoo/util/Time.java @@ -0,0 +1,14 @@ +/* Copyright 2013 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.util; + +/* + * Wrapper for System.currentTimeMillis() that can be replaced with a + * custom time source for testing. + */ +public class Time { + public long currentTimeMillis() { + return System.currentTimeMillis(); + } +} + diff --git a/src/org/torproject/onionoo/writer/BandwidthDocumentWriter.java b/src/org/torproject/onionoo/writer/BandwidthDocumentWriter.java new file mode 100644 index 0000000..908ec7c --- /dev/null +++ b/src/org/torproject/onionoo/writer/BandwidthDocumentWriter.java @@ -0,0 +1,201 @@ +/* Copyright 2011--2014 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.writer; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; + +import org.torproject.onionoo.docs.BandwidthDocument; +import org.torproject.onionoo.docs.BandwidthStatus; +import org.torproject.onionoo.docs.DocumentStore; +import org.torproject.onionoo.docs.GraphHistory; +import org.torproject.onionoo.updater.DescriptorSource; +import org.torproject.onionoo.updater.DescriptorType; +import org.torproject.onionoo.updater.FingerprintListener; +import org.torproject.onionoo.util.ApplicationFactory; +import org.torproject.onionoo.util.DateTimeHelper; +import org.torproject.onionoo.util.Logger; + +public class BandwidthDocumentWriter implements FingerprintListener, + DocumentWriter{ + + private DescriptorSource descriptorSource; + + private DocumentStore documentStore; + + private long now; + + public BandwidthDocumentWriter() { + this.descriptorSource = ApplicationFactory.getDescriptorSource(); + this.documentStore = ApplicationFactory.getDocumentStore(); + this.now = ApplicationFactory.getTime().currentTimeMillis(); + this.registerFingerprintListeners(); + } + + private void registerFingerprintListeners() { + this.descriptorSource.registerFingerprintListener(this, + DescriptorType.RELAY_EXTRA_INFOS); + this.descriptorSource.registerFingerprintListener(this, + DescriptorType.BRIDGE_EXTRA_INFOS); + } + + private Set<String> updateBandwidthDocuments = new HashSet<String>(); + + public void processFingerprints(SortedSet<String> fingerprints, + boolean relay) { + this.updateBandwidthDocuments.addAll(fingerprints); + } + + public void writeDocuments() { + for (String fingerprint : this.updateBandwidthDocuments) { + BandwidthStatus bandwidthStatus = this.documentStore.retrieve( + BandwidthStatus.class, true, fingerprint); + if (bandwidthStatus == null) { + continue; + } + BandwidthDocument bandwidthDocument = this.compileBandwidthDocument( + fingerprint, bandwidthStatus); + this.documentStore.store(bandwidthDocument, fingerprint); + } + Logger.printStatusTime("Wrote bandwidth document files"); + } + + + private BandwidthDocument compileBandwidthDocument(String fingerprint, + BandwidthStatus bandwidthStatus) { + BandwidthDocument bandwidthDocument = new BandwidthDocument(); + bandwidthDocument.setFingerprint(fingerprint); + bandwidthDocument.setWriteHistory(this.compileGraphType( + bandwidthStatus.getWriteHistory())); + bandwidthDocument.setReadHistory(this.compileGraphType( + bandwidthStatus.getReadHistory())); + return bandwidthDocument; + } + + private String[] graphNames = new String[] { + "3_days", + "1_week", + "1_month", + "3_months", + "1_year", + "5_years" }; + + private long[] graphIntervals = new long[] { + DateTimeHelper.THREE_DAYS, + DateTimeHelper.ONE_WEEK, + DateTimeHelper.ROUGHLY_ONE_MONTH, + DateTimeHelper.ROUGHLY_THREE_MONTHS, + DateTimeHelper.ROUGHLY_ONE_YEAR, + DateTimeHelper.ROUGHLY_FIVE_YEARS }; + + private long[] dataPointIntervals = new long[] { + DateTimeHelper.FIFTEEN_MINUTES, + DateTimeHelper.ONE_HOUR, + DateTimeHelper.FOUR_HOURS, + DateTimeHelper.TWELVE_HOURS, + DateTimeHelper.TWO_DAYS, + DateTimeHelper.TEN_DAYS }; + + private Map<String, GraphHistory> compileGraphType( + SortedMap<Long, long[]> history) { + Map<String, GraphHistory> graphs = + new LinkedHashMap<String, GraphHistory>(); + for (int i = 0; i < this.graphIntervals.length; i++) { + String graphName = this.graphNames[i]; + long graphInterval = this.graphIntervals[i]; + long dataPointInterval = this.dataPointIntervals[i]; + List<Long> dataPoints = new ArrayList<Long>(); + long intervalStartMillis = ((this.now - graphInterval) + / dataPointInterval) * dataPointInterval; + long totalMillis = 0L, totalBandwidth = 0L; + for (long[] v : history.values()) { + long startMillis = v[0], endMillis = v[1], bandwidth = v[2]; + if (endMillis < intervalStartMillis) { + continue; + } + while ((intervalStartMillis / dataPointInterval) != + (endMillis / dataPointInterval)) { + dataPoints.add(totalMillis * 5L < dataPointInterval + ? -1L : (totalBandwidth * DateTimeHelper.ONE_SECOND) + / totalMillis); + totalBandwidth = 0L; + totalMillis = 0L; + intervalStartMillis += dataPointInterval; + } + totalBandwidth += bandwidth; + totalMillis += (endMillis - startMillis); + } + dataPoints.add(totalMillis * 5L < dataPointInterval + ? -1L : (totalBandwidth * DateTimeHelper.ONE_SECOND) + / totalMillis); + long maxValue = 1L; + int firstNonNullIndex = -1, lastNonNullIndex = -1; + for (int j = 0; j < dataPoints.size(); j++) { + long dataPoint = dataPoints.get(j); + if (dataPoint >= 0L) { + if (firstNonNullIndex < 0) { + firstNonNullIndex = j; + } + lastNonNullIndex = j; + if (dataPoint > maxValue) { + maxValue = dataPoint; + } + } + } + if (firstNonNullIndex < 0) { + continue; + } + long firstDataPointMillis = (((this.now - graphInterval) + / dataPointInterval) + firstNonNullIndex) * dataPointInterval + + dataPointInterval / 2L; + if (i > 0 && + firstDataPointMillis >= this.now - graphIntervals[i - 1]) { + /* Skip bandwidth history object, because it doesn't contain + * anything new that wasn't already contained in the last + * bandwidth history object(s). */ + continue; + } + long lastDataPointMillis = firstDataPointMillis + + (lastNonNullIndex - firstNonNullIndex) * dataPointInterval; + double factor = ((double) maxValue) / 999.0; + int count = lastNonNullIndex - firstNonNullIndex + 1; + GraphHistory graphHistory = new GraphHistory(); + graphHistory.setFirst(DateTimeHelper.format(firstDataPointMillis)); + graphHistory.setLast(DateTimeHelper.format(lastDataPointMillis)); + graphHistory.setInterval((int) (dataPointInterval + / DateTimeHelper.ONE_SECOND)); + graphHistory.setFactor(factor); + graphHistory.setCount(count); + int previousNonNullIndex = -2; + boolean foundTwoAdjacentDataPoints = false; + List<Integer> values = new ArrayList<Integer>(); + for (int j = firstNonNullIndex; j <= lastNonNullIndex; j++) { + long dataPoint = dataPoints.get(j); + if (dataPoint >= 0L) { + if (j - previousNonNullIndex == 1) { + foundTwoAdjacentDataPoints = true; + } + previousNonNullIndex = j; + } + values.add(dataPoint < 0L ? null : + (int) ((dataPoint * 999L) / maxValue)); + } + graphHistory.setValues(values); + if (foundTwoAdjacentDataPoints) { + graphs.put(graphName, graphHistory); + } + } + return graphs; + } + + public String getStatsString() { + /* TODO Add statistics string. */ + return null; + } +} diff --git a/src/org/torproject/onionoo/writer/ClientsDocumentWriter.java b/src/org/torproject/onionoo/writer/ClientsDocumentWriter.java new file mode 100644 index 0000000..976804c --- /dev/null +++ b/src/org/torproject/onionoo/writer/ClientsDocumentWriter.java @@ -0,0 +1,296 @@ +/* Copyright 2014 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.writer; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; + +import org.torproject.onionoo.docs.ClientsDocument; +import org.torproject.onionoo.docs.ClientsGraphHistory; +import org.torproject.onionoo.docs.ClientsHistory; +import org.torproject.onionoo.docs.ClientsStatus; +import org.torproject.onionoo.docs.DocumentStore; +import org.torproject.onionoo.updater.DescriptorSource; +import org.torproject.onionoo.updater.DescriptorType; +import org.torproject.onionoo.updater.FingerprintListener; +import org.torproject.onionoo.util.ApplicationFactory; +import org.torproject.onionoo.util.DateTimeHelper; +import org.torproject.onionoo.util.Logger; + +/* + * Clients status file produced as intermediate output: + * + * 2014-02-15 16:42:11 2014-02-16 00:00:00 + * 259.042 in=86.347,se=86.347 v4=259.042 + * 2014-02-16 00:00:00 2014-02-16 16:42:11 + * 592.958 in=197.653,se=197.653 v4=592.958 + * + * Clients document file produced as output: + * + * "1_month":{ + * "first":"2014-02-03 12:00:00", + * "last":"2014-02-28 12:00:00", + * "interval":86400, + * "factor":0.139049349, + * "count":26, + * "values":[371,354,349,374,432,null,485,458,493,536,null,null,524,576, + * 607,622,null,635,null,566,774,999,945,690,656,681], + * "countries":{"cn":0.0192,"in":0.1768,"ir":0.2487,"ru":0.0104, + * "se":0.1698,"sy":0.0325,"us":0.0406}, + * "transports":{"obfs2":0.4581}, + * "versions":{"v4":1.0000}} + */ +public class ClientsDocumentWriter implements FingerprintListener, + DocumentWriter { + + private DescriptorSource descriptorSource; + + private DocumentStore documentStore; + + private long now; + + public ClientsDocumentWriter() { + this.descriptorSource = ApplicationFactory.getDescriptorSource(); + this.documentStore = ApplicationFactory.getDocumentStore(); + this.now = ApplicationFactory.getTime().currentTimeMillis(); + this.registerFingerprintListeners(); + } + + private void registerFingerprintListeners() { + this.descriptorSource.registerFingerprintListener(this, + DescriptorType.BRIDGE_EXTRA_INFOS); + } + + private SortedSet<String> updateDocuments = new TreeSet<String>(); + + public void processFingerprints(SortedSet<String> fingerprints, + boolean relay) { + if (!relay) { + this.updateDocuments.addAll(fingerprints); + } + } + + private int writtenDocuments = 0; + + public void writeDocuments() { + for (String hashedFingerprint : this.updateDocuments) { + ClientsStatus clientsStatus = this.documentStore.retrieve( + ClientsStatus.class, true, hashedFingerprint); + if (clientsStatus == null) { + continue; + } + SortedSet<ClientsHistory> history = clientsStatus.getHistory(); + ClientsDocument clientsDocument = this.compileClientsDocument( + hashedFingerprint, history); + this.documentStore.store(clientsDocument, hashedFingerprint); + this.writtenDocuments++; + } + Logger.printStatusTime("Wrote clients document files"); + } + + private String[] graphNames = new String[] { + "1_week", + "1_month", + "3_months", + "1_year", + "5_years" }; + + private long[] graphIntervals = new long[] { + DateTimeHelper.ONE_WEEK, + DateTimeHelper.ROUGHLY_ONE_MONTH, + DateTimeHelper.ROUGHLY_THREE_MONTHS, + DateTimeHelper.ROUGHLY_ONE_YEAR, + DateTimeHelper.ROUGHLY_FIVE_YEARS }; + + private long[] dataPointIntervals = new long[] { + DateTimeHelper.ONE_DAY, + DateTimeHelper.ONE_DAY, + DateTimeHelper.ONE_DAY, + DateTimeHelper.TWO_DAYS, + DateTimeHelper.TEN_DAYS }; + + private ClientsDocument compileClientsDocument(String hashedFingerprint, + SortedSet<ClientsHistory> history) { + ClientsDocument clientsDocument = new ClientsDocument(); + clientsDocument.setFingerprint(hashedFingerprint); + Map<String, ClientsGraphHistory> averageClients = + new LinkedHashMap<String, ClientsGraphHistory>(); + for (int graphIntervalIndex = 0; graphIntervalIndex < + this.graphIntervals.length; graphIntervalIndex++) { + String graphName = this.graphNames[graphIntervalIndex]; + ClientsGraphHistory graphHistory = this.compileClientsHistory( + graphIntervalIndex, history); + if (graphHistory != null) { + averageClients.put(graphName, graphHistory); + } + } + clientsDocument.setAverageClients(averageClients); + return clientsDocument; + } + + private ClientsGraphHistory compileClientsHistory( + int graphIntervalIndex, SortedSet<ClientsHistory> history) { + long graphInterval = this.graphIntervals[graphIntervalIndex]; + long dataPointInterval = + this.dataPointIntervals[graphIntervalIndex]; + List<Double> dataPoints = new ArrayList<Double>(); + long intervalStartMillis = ((this.now - graphInterval) + / dataPointInterval) * dataPointInterval; + long millis = 0L; + double responses = 0.0, totalResponses = 0.0; + SortedMap<String, Double> + totalResponsesByCountry = new TreeMap<String, Double>(), + totalResponsesByTransport = new TreeMap<String, Double>(), + totalResponsesByVersion = new TreeMap<String, Double>(); + for (ClientsHistory hist : history) { + if (hist.getEndMillis() < intervalStartMillis) { + continue; + } + while ((intervalStartMillis / dataPointInterval) != + (hist.getEndMillis() / dataPointInterval)) { + dataPoints.add(millis * 2L < dataPointInterval + ? -1.0 : responses * ((double) DateTimeHelper.ONE_DAY) + / (((double) millis) * 10.0)); + responses = 0.0; + millis = 0L; + intervalStartMillis += dataPointInterval; + } + responses += hist.getTotalResponses(); + totalResponses += hist.getTotalResponses(); + for (Map.Entry<String, Double> e : + hist.getResponsesByCountry().entrySet()) { + if (!totalResponsesByCountry.containsKey(e.getKey())) { + totalResponsesByCountry.put(e.getKey(), 0.0); + } + totalResponsesByCountry.put(e.getKey(), e.getValue() + + totalResponsesByCountry.get(e.getKey())); + } + for (Map.Entry<String, Double> e : + hist.getResponsesByTransport().entrySet()) { + if (!totalResponsesByTransport.containsKey(e.getKey())) { + totalResponsesByTransport.put(e.getKey(), 0.0); + } + totalResponsesByTransport.put(e.getKey(), e.getValue() + + totalResponsesByTransport.get(e.getKey())); + } + for (Map.Entry<String, Double> e : + hist.getResponsesByVersion().entrySet()) { + if (!totalResponsesByVersion.containsKey(e.getKey())) { + totalResponsesByVersion.put(e.getKey(), 0.0); + } + totalResponsesByVersion.put(e.getKey(), e.getValue() + + totalResponsesByVersion.get(e.getKey())); + } + millis += (hist.getEndMillis() - hist.getStartMillis()); + } + dataPoints.add(millis * 2L < dataPointInterval + ? -1.0 : responses * ((double) DateTimeHelper.ONE_DAY) + / (((double) millis) * 10.0)); + double maxValue = 0.0; + int firstNonNullIndex = -1, lastNonNullIndex = -1; + for (int dataPointIndex = 0; dataPointIndex < dataPoints.size(); + dataPointIndex++) { + double dataPoint = dataPoints.get(dataPointIndex); + if (dataPoint >= 0.0) { + if (firstNonNullIndex < 0) { + firstNonNullIndex = dataPointIndex; + } + lastNonNullIndex = dataPointIndex; + if (dataPoint > maxValue) { + maxValue = dataPoint; + } + } + } + if (firstNonNullIndex < 0) { + return null; + } + long firstDataPointMillis = (((this.now - graphInterval) + / dataPointInterval) + firstNonNullIndex) * dataPointInterval + + dataPointInterval / 2L; + if (graphIntervalIndex > 0 && firstDataPointMillis >= + this.now - graphIntervals[graphIntervalIndex - 1]) { + /* Skip clients history object, because it doesn't contain + * anything new that wasn't already contained in the last + * clients history object(s). */ + return null; + } + long lastDataPointMillis = firstDataPointMillis + + (lastNonNullIndex - firstNonNullIndex) * dataPointInterval; + double factor = ((double) maxValue) / 999.0; + int count = lastNonNullIndex - firstNonNullIndex + 1; + ClientsGraphHistory graphHistory = new ClientsGraphHistory(); + graphHistory.setFirst(DateTimeHelper.format(firstDataPointMillis)); + graphHistory.setLast(DateTimeHelper.format(lastDataPointMillis)); + graphHistory.setInterval((int) (dataPointInterval + / DateTimeHelper.ONE_SECOND)); + graphHistory.setFactor(factor); + graphHistory.setCount(count); + int previousNonNullIndex = -2; + boolean foundTwoAdjacentDataPoints = false; + List<Integer> values = new ArrayList<Integer>(); + for (int dataPointIndex = firstNonNullIndex; dataPointIndex <= + lastNonNullIndex; dataPointIndex++) { + double dataPoint = dataPoints.get(dataPointIndex); + if (dataPoint >= 0.0) { + if (dataPointIndex - previousNonNullIndex == 1) { + foundTwoAdjacentDataPoints = true; + } + previousNonNullIndex = dataPointIndex; + } + values.add(dataPoint < 0.0 ? null : + (int) ((dataPoint * 999.0) / maxValue)); + } + graphHistory.setValues(values); + if (!totalResponsesByCountry.isEmpty()) { + SortedMap<String, Float> countries = new TreeMap<String, Float>(); + for (Map.Entry<String, Double> e : + totalResponsesByCountry.entrySet()) { + if (e.getValue() > totalResponses / 100.0) { + countries.put(e.getKey(), + (float) (e.getValue() / totalResponses)); + } + } + graphHistory.setCountries(countries); + } + if (!totalResponsesByTransport.isEmpty()) { + SortedMap<String, Float> transports = new TreeMap<String, Float>(); + for (Map.Entry<String, Double> e : + totalResponsesByTransport.entrySet()) { + if (e.getValue() > totalResponses / 100.0) { + transports.put(e.getKey(), + (float) (e.getValue() / totalResponses)); + } + } + graphHistory.setTransports(transports); + } + if (!totalResponsesByVersion.isEmpty()) { + SortedMap<String, Float> versions = new TreeMap<String, Float>(); + for (Map.Entry<String, Double> e : + totalResponsesByVersion.entrySet()) { + if (e.getValue() > totalResponses / 100.0) { + versions.put(e.getKey(), + (float) (e.getValue() / totalResponses)); + } + } + graphHistory.setVersions(versions); + } + if (foundTwoAdjacentDataPoints) { + return graphHistory; + } else { + return null; + } + } + + public String getStatsString() { + StringBuilder sb = new StringBuilder(); + sb.append(" " + Logger.formatDecimalNumber(this.writtenDocuments) + + " clients document files updated\n"); + return sb.toString(); + } +} diff --git a/src/org/torproject/onionoo/writer/DetailsDocumentWriter.java b/src/org/torproject/onionoo/writer/DetailsDocumentWriter.java new file mode 100644 index 0000000..03f7024 --- /dev/null +++ b/src/org/torproject/onionoo/writer/DetailsDocumentWriter.java @@ -0,0 +1,233 @@ +package org.torproject.onionoo.writer; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.torproject.onionoo.docs.DetailsDocument; +import org.torproject.onionoo.docs.DetailsStatus; +import org.torproject.onionoo.docs.DocumentStore; +import org.torproject.onionoo.docs.NodeStatus; +import org.torproject.onionoo.updater.DescriptorSource; +import org.torproject.onionoo.updater.DescriptorType; +import org.torproject.onionoo.updater.FingerprintListener; +import org.torproject.onionoo.util.ApplicationFactory; +import org.torproject.onionoo.util.DateTimeHelper; +import org.torproject.onionoo.util.Logger; + +public class DetailsDocumentWriter implements FingerprintListener, + DocumentWriter { + + private DescriptorSource descriptorSource; + + private DocumentStore documentStore; + + private long now; + + public DetailsDocumentWriter() { + this.descriptorSource = ApplicationFactory.getDescriptorSource(); + this.documentStore = ApplicationFactory.getDocumentStore(); + this.now = ApplicationFactory.getTime().currentTimeMillis(); + this.registerFingerprintListeners(); + } + + private void registerFingerprintListeners() { + this.descriptorSource.registerFingerprintListener(this, + DescriptorType.RELAY_CONSENSUSES); + this.descriptorSource.registerFingerprintListener(this, + DescriptorType.RELAY_SERVER_DESCRIPTORS); + this.descriptorSource.registerFingerprintListener(this, + DescriptorType.BRIDGE_STATUSES); + this.descriptorSource.registerFingerprintListener(this, + DescriptorType.BRIDGE_SERVER_DESCRIPTORS); + this.descriptorSource.registerFingerprintListener(this, + DescriptorType.BRIDGE_POOL_ASSIGNMENTS); + this.descriptorSource.registerFingerprintListener(this, + DescriptorType.EXIT_LISTS); + } + + private SortedSet<String> newRelays = new TreeSet<String>(), + newBridges = new TreeSet<String>(); + + public void processFingerprints(SortedSet<String> fingerprints, + boolean relay) { + if (relay) { + this.newRelays.addAll(fingerprints); + } else { + this.newBridges.addAll(fingerprints); + } + } + + public void writeDocuments() { + this.updateRelayDetailsFiles(); + this.updateBridgeDetailsFiles(); + Logger.printStatusTime("Wrote details document files"); + } + + private void updateRelayDetailsFiles() { + for (String fingerprint : this.newRelays) { + + /* Generate network-status-specific part. */ + NodeStatus entry = this.documentStore.retrieve(NodeStatus.class, + true, fingerprint); + if (entry == null) { + continue; + } + DetailsDocument detailsDocument = new DetailsDocument(); + detailsDocument.setNickname(entry.getNickname()); + detailsDocument.setFingerprint(fingerprint); + List<String> orAddresses = new ArrayList<String>(); + orAddresses.add(entry.getAddress() + ":" + entry.getOrPort()); + for (String orAddress : entry.getOrAddressesAndPorts()) { + orAddresses.add(orAddress.toLowerCase()); + } + detailsDocument.setOrAddresses(orAddresses); + if (entry.getDirPort() != 0) { + detailsDocument.setDirAddress(entry.getAddress() + ":" + + entry.getDirPort()); + } + detailsDocument.setLastSeen(DateTimeHelper.format( + entry.getLastSeenMillis())); + detailsDocument.setFirstSeen(DateTimeHelper.format( + entry.getFirstSeenMillis())); + detailsDocument.setLastChangedAddressOrPort( + DateTimeHelper.format(entry.getLastChangedOrAddress())); + detailsDocument.setRunning(entry.getRunning()); + if (!entry.getRelayFlags().isEmpty()) { + detailsDocument.setFlags(new ArrayList<String>( + entry.getRelayFlags())); + } + detailsDocument.setCountry(entry.getCountryCode()); + detailsDocument.setLatitude(entry.getLatitude()); + detailsDocument.setLongitude(entry.getLongitude()); + detailsDocument.setCountryName(entry.getCountryName()); + detailsDocument.setRegionName(entry.getRegionName()); + detailsDocument.setCityName(entry.getCityName()); + detailsDocument.setAsNumber(entry.getASNumber()); + detailsDocument.setAsName(entry.getASName()); + detailsDocument.setConsensusWeight(entry.getConsensusWeight()); + detailsDocument.setHostName(entry.getHostName()); + detailsDocument.setAdvertisedBandwidthFraction( + (float) entry.getAdvertisedBandwidthFraction()); + detailsDocument.setConsensusWeightFraction( + (float) entry.getConsensusWeightFraction()); + detailsDocument.setGuardProbability( + (float) entry.getGuardProbability()); + detailsDocument.setMiddleProbability( + (float) entry.getMiddleProbability()); + detailsDocument.setExitProbability( + (float) entry.getExitProbability()); + String defaultPolicy = entry.getDefaultPolicy(); + String portList = entry.getPortList(); + if (defaultPolicy != null && (defaultPolicy.equals("accept") || + defaultPolicy.equals("reject")) && portList != null) { + Map<String, List<String>> exitPolicySummary = + new HashMap<String, List<String>>(); + List<String> portsOrPortRanges = Arrays.asList( + portList.split(",")); + exitPolicySummary.put(defaultPolicy, portsOrPortRanges); + detailsDocument.setExitPolicySummary(exitPolicySummary); + } + detailsDocument.setRecommendedVersion( + entry.getRecommendedVersion()); + + /* Append descriptor-specific part and exit addresses from details + * status file. */ + DetailsStatus detailsStatus = this.documentStore.retrieve( + DetailsStatus.class, true, fingerprint); + if (detailsStatus != null) { + detailsDocument.setLastRestarted( + detailsStatus.getLastRestarted()); + detailsDocument.setBandwidthRate( + detailsStatus.getBandwidthRate()); + detailsDocument.setBandwidthBurst( + detailsStatus.getBandwidthBurst()); + detailsDocument.setObservedBandwidth( + detailsStatus.getObservedBandwidth()); + detailsDocument.setAdvertisedBandwidth( + detailsStatus.getAdvertisedBandwidth()); + detailsDocument.setExitPolicy(detailsStatus.getExitPolicy()); + detailsDocument.setContact(detailsStatus.getContact()); + detailsDocument.setPlatform(detailsStatus.getPlatform()); + detailsDocument.setFamily(detailsStatus.getFamily()); + detailsDocument.setExitPolicyV6Summary( + detailsStatus.getExitPolicyV6Summary()); + detailsDocument.setHibernating(detailsStatus.getHibernating()); + if (detailsStatus.getExitAddresses() != null) { + SortedSet<String> exitAddresses = new TreeSet<String>(); + for (Map.Entry<String, Long> e : + detailsStatus.getExitAddresses().entrySet()) { + String exitAddress = e.getKey().toLowerCase(); + long scanMillis = e.getValue(); + if (!entry.getAddress().equals(exitAddress) && + !entry.getOrAddresses().contains(exitAddress) && + scanMillis >= this.now - DateTimeHelper.ONE_DAY) { + exitAddresses.add(exitAddress); + } + } + if (!exitAddresses.isEmpty()) { + detailsDocument.setExitAddresses(new ArrayList<String>( + exitAddresses)); + } + } + } + + /* Write details file to disk. */ + this.documentStore.store(detailsDocument, fingerprint); + } + } + + private void updateBridgeDetailsFiles() { + for (String fingerprint : this.newBridges) { + + /* Generate network-status-specific part. */ + NodeStatus entry = this.documentStore.retrieve(NodeStatus.class, + true, fingerprint); + if (entry == null) { + continue; + } + DetailsDocument detailsDocument = new DetailsDocument(); + detailsDocument.setNickname(entry.getNickname()); + detailsDocument.setHashedFingerprint(fingerprint); + String address = entry.getAddress(); + List<String> orAddresses = new ArrayList<String>(); + orAddresses.add(address + ":" + entry.getOrPort()); + for (String orAddress : entry.getOrAddressesAndPorts()) { + orAddresses.add(orAddress.toLowerCase()); + } + detailsDocument.setOrAddresses(orAddresses); + detailsDocument.setLastSeen(DateTimeHelper.format( + entry.getLastSeenMillis())); + detailsDocument.setFirstSeen(DateTimeHelper.format( + entry.getFirstSeenMillis())); + detailsDocument.setRunning(entry.getRunning()); + detailsDocument.setFlags(new ArrayList<String>( + entry.getRelayFlags())); + + /* Append descriptor-specific part from details status file. */ + DetailsStatus detailsStatus = this.documentStore.retrieve( + DetailsStatus.class, true, fingerprint); + if (detailsStatus != null) { + detailsDocument.setLastRestarted( + detailsStatus.getLastRestarted()); + detailsDocument.setAdvertisedBandwidth( + detailsStatus.getAdvertisedBandwidth()); + detailsDocument.setPlatform(detailsStatus.getPlatform()); + detailsDocument.setPoolAssignment( + detailsStatus.getPoolAssignment()); + } + + /* Write details file to disk. */ + this.documentStore.store(detailsDocument, fingerprint); + } + } + + public String getStatsString() { + /* TODO Add statistics string. */ + return null; + } +} diff --git a/src/org/torproject/onionoo/writer/DocumentWriter.java b/src/org/torproject/onionoo/writer/DocumentWriter.java new file mode 100644 index 0000000..c238170 --- /dev/null +++ b/src/org/torproject/onionoo/writer/DocumentWriter.java @@ -0,0 +1,11 @@ +/* Copyright 2014 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.writer; + +public interface DocumentWriter { + + public abstract void writeDocuments(); + + public abstract String getStatsString(); +} + diff --git a/src/org/torproject/onionoo/writer/SummaryDocumentWriter.java b/src/org/torproject/onionoo/writer/SummaryDocumentWriter.java new file mode 100644 index 0000000..1b4630e --- /dev/null +++ b/src/org/torproject/onionoo/writer/SummaryDocumentWriter.java @@ -0,0 +1,94 @@ +/* Copyright 2014 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.writer; + +import java.util.ArrayList; +import java.util.List; +import java.util.SortedSet; + +import org.torproject.onionoo.docs.DocumentStore; +import org.torproject.onionoo.docs.NodeStatus; +import org.torproject.onionoo.docs.SummaryDocument; +import org.torproject.onionoo.util.ApplicationFactory; +import org.torproject.onionoo.util.DateTimeHelper; +import org.torproject.onionoo.util.Logger; + +public class SummaryDocumentWriter implements DocumentWriter { + + private DocumentStore documentStore; + + public SummaryDocumentWriter() { + this.documentStore = ApplicationFactory.getDocumentStore(); + } + + private int writtenDocuments = 0, deletedDocuments = 0; + + public void writeDocuments() { + long maxLastSeenMillis = 0L; + for (String fingerprint : this.documentStore.list(NodeStatus.class)) { + NodeStatus nodeStatus = this.documentStore.retrieve( + NodeStatus.class, true, fingerprint); + if (nodeStatus != null && + nodeStatus.getLastSeenMillis() > maxLastSeenMillis) { + maxLastSeenMillis = nodeStatus.getLastSeenMillis(); + } + } + long cutoff = maxLastSeenMillis - DateTimeHelper.ONE_WEEK; + for (String fingerprint : this.documentStore.list(NodeStatus.class)) { + NodeStatus nodeStatus = this.documentStore.retrieve( + NodeStatus.class, + true, fingerprint); + if (nodeStatus == null) { + continue; + } + if (nodeStatus.getLastSeenMillis() < cutoff) { + if (this.documentStore.remove(SummaryDocument.class, + fingerprint)) { + this.deletedDocuments++; + } + continue; + } + boolean isRelay = nodeStatus.isRelay(); + String nickname = nodeStatus.getNickname(); + List<String> addresses = new ArrayList<String>(); + addresses.add(nodeStatus.getAddress()); + for (String orAddress : nodeStatus.getOrAddresses()) { + if (!addresses.contains(orAddress)) { + addresses.add(orAddress); + } + } + for (String exitAddress : nodeStatus.getExitAddresses()) { + if (!addresses.contains(exitAddress)) { + addresses.add(exitAddress); + } + } + long lastSeenMillis = nodeStatus.getLastSeenMillis(); + boolean running = nodeStatus.getRunning(); + SortedSet<String> relayFlags = nodeStatus.getRelayFlags(); + long consensusWeight = nodeStatus.getConsensusWeight(); + String countryCode = nodeStatus.getCountryCode(); + long firstSeenMillis = nodeStatus.getFirstSeenMillis(); + String aSNumber = nodeStatus.getASNumber(); + String contact = nodeStatus.getContact(); + SortedSet<String> familyFingerprints = + nodeStatus.getFamilyFingerprints(); + SummaryDocument summaryDocument = new SummaryDocument(isRelay, + nickname, fingerprint, addresses, lastSeenMillis, running, + relayFlags, consensusWeight, countryCode, firstSeenMillis, + aSNumber, contact, familyFingerprints); + if (this.documentStore.store(summaryDocument, fingerprint)) { + this.writtenDocuments++; + }; + } + Logger.printStatusTime("Wrote summary document files"); + } + + public String getStatsString() { + StringBuilder sb = new StringBuilder(); + sb.append(" " + Logger.formatDecimalNumber(this.writtenDocuments) + + " summary document files written\n"); + sb.append(" " + Logger.formatDecimalNumber(this.deletedDocuments) + + " summary document files deleted\n"); + return sb.toString(); + } +} diff --git a/src/org/torproject/onionoo/writer/UptimeDocumentWriter.java b/src/org/torproject/onionoo/writer/UptimeDocumentWriter.java new file mode 100644 index 0000000..3e04abb --- /dev/null +++ b/src/org/torproject/onionoo/writer/UptimeDocumentWriter.java @@ -0,0 +1,303 @@ +/* Copyright 2014 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.writer; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.torproject.onionoo.docs.DocumentStore; +import org.torproject.onionoo.docs.GraphHistory; +import org.torproject.onionoo.docs.UptimeDocument; +import org.torproject.onionoo.docs.UptimeHistory; +import org.torproject.onionoo.docs.UptimeStatus; +import org.torproject.onionoo.updater.DescriptorSource; +import org.torproject.onionoo.updater.DescriptorType; +import org.torproject.onionoo.updater.FingerprintListener; +import org.torproject.onionoo.util.ApplicationFactory; +import org.torproject.onionoo.util.DateTimeHelper; +import org.torproject.onionoo.util.Logger; + +public class UptimeDocumentWriter implements FingerprintListener, + DocumentWriter { + + private DescriptorSource descriptorSource; + + private DocumentStore documentStore; + + private long now; + + public UptimeDocumentWriter() { + this.descriptorSource = ApplicationFactory.getDescriptorSource(); + this.documentStore = ApplicationFactory.getDocumentStore(); + this.now = ApplicationFactory.getTime().currentTimeMillis(); + this.registerFingerprintListeners(); + } + + private void registerFingerprintListeners() { + this.descriptorSource.registerFingerprintListener(this, + DescriptorType.RELAY_CONSENSUSES); + this.descriptorSource.registerFingerprintListener(this, + DescriptorType.BRIDGE_STATUSES); + } + + private SortedSet<String> newRelayFingerprints = new TreeSet<String>(), + newBridgeFingerprints = new TreeSet<String>(); + + public void processFingerprints(SortedSet<String> fingerprints, + boolean relay) { + if (relay) { + this.newRelayFingerprints.addAll(fingerprints); + } else { + this.newBridgeFingerprints.addAll(fingerprints); + } + } + + public void writeDocuments() { + UptimeStatus uptimeStatus = this.documentStore.retrieve( + UptimeStatus.class, true); + if (uptimeStatus == null) { + return; + } + for (String fingerprint : this.newRelayFingerprints) { + this.updateDocument(true, fingerprint, + uptimeStatus.getRelayHistory()); + } + for (String fingerprint : this.newBridgeFingerprints) { + this.updateDocument(false, fingerprint, + uptimeStatus.getBridgeHistory()); + } + Logger.printStatusTime("Wrote uptime document files"); + } + + private int writtenDocuments = 0; + + private void updateDocument(boolean relay, String fingerprint, + SortedSet<UptimeHistory> knownStatuses) { + UptimeStatus uptimeStatus = this.documentStore.retrieve( + UptimeStatus.class, true, fingerprint); + if (uptimeStatus != null) { + SortedSet<UptimeHistory> history = relay + ? uptimeStatus.getRelayHistory() + : uptimeStatus.getBridgeHistory(); + UptimeDocument uptimeDocument = this.compileUptimeDocument(relay, + fingerprint, history, knownStatuses); + this.documentStore.store(uptimeDocument, fingerprint); + this.writtenDocuments++; + } + } + + private String[] graphNames = new String[] { + "1_week", + "1_month", + "3_months", + "1_year", + "5_years" }; + + private long[] graphIntervals = new long[] { + DateTimeHelper.ONE_WEEK, + DateTimeHelper.ROUGHLY_ONE_MONTH, + DateTimeHelper.ROUGHLY_THREE_MONTHS, + DateTimeHelper.ROUGHLY_ONE_YEAR, + DateTimeHelper.ROUGHLY_FIVE_YEARS }; + + private long[] dataPointIntervals = new long[] { + DateTimeHelper.ONE_HOUR, + DateTimeHelper.FOUR_HOURS, + DateTimeHelper.TWELVE_HOURS, + DateTimeHelper.TWO_DAYS, + DateTimeHelper.TEN_DAYS }; + + private UptimeDocument compileUptimeDocument(boolean relay, + String fingerprint, SortedSet<UptimeHistory> history, + SortedSet<UptimeHistory> knownStatuses) { + UptimeDocument uptimeDocument = new UptimeDocument(); + uptimeDocument.setFingerprint(fingerprint); + Map<String, GraphHistory> uptime = + new LinkedHashMap<String, GraphHistory>(); + for (int graphIntervalIndex = 0; graphIntervalIndex < + this.graphIntervals.length; graphIntervalIndex++) { + String graphName = this.graphNames[graphIntervalIndex]; + GraphHistory graphHistory = this.compileUptimeHistory( + graphIntervalIndex, relay, history, knownStatuses); + if (graphHistory != null) { + uptime.put(graphName, graphHistory); + } + } + uptimeDocument.setUptime(uptime); + return uptimeDocument; + } + + private GraphHistory compileUptimeHistory(int graphIntervalIndex, + boolean relay, SortedSet<UptimeHistory> history, + SortedSet<UptimeHistory> knownStatuses) { + long graphInterval = this.graphIntervals[graphIntervalIndex]; + long dataPointInterval = + this.dataPointIntervals[graphIntervalIndex]; + int dataPointIntervalHours = (int) (dataPointInterval + / DateTimeHelper.ONE_HOUR); + List<Integer> uptimeDataPoints = new ArrayList<Integer>(); + long intervalStartMillis = ((this.now - graphInterval) + / dataPointInterval) * dataPointInterval; + int uptimeHours = 0; + long firstStatusStartMillis = -1L; + for (UptimeHistory hist : history) { + if (hist.isRelay() != relay) { + continue; + } + if (firstStatusStartMillis < 0L) { + firstStatusStartMillis = hist.getStartMillis(); + } + long histEndMillis = hist.getStartMillis() + DateTimeHelper.ONE_HOUR + * hist.getUptimeHours(); + if (histEndMillis < intervalStartMillis) { + continue; + } + while (hist.getStartMillis() >= intervalStartMillis + + dataPointInterval) { + if (firstStatusStartMillis < intervalStartMillis + + dataPointInterval) { + uptimeDataPoints.add(uptimeHours); + } else { + uptimeDataPoints.add(-1); + } + uptimeHours = 0; + intervalStartMillis += dataPointInterval; + } + while (histEndMillis >= intervalStartMillis + dataPointInterval) { + uptimeHours += (int) ((intervalStartMillis + dataPointInterval + - Math.max(hist.getStartMillis(), intervalStartMillis)) + / DateTimeHelper.ONE_HOUR); + uptimeDataPoints.add(uptimeHours); + uptimeHours = 0; + intervalStartMillis += dataPointInterval; + } + uptimeHours += (int) ((histEndMillis - Math.max( + hist.getStartMillis(), intervalStartMillis)) + / DateTimeHelper.ONE_HOUR); + } + uptimeDataPoints.add(uptimeHours); + List<Integer> statusDataPoints = new ArrayList<Integer>(); + intervalStartMillis = ((this.now - graphInterval) + / dataPointInterval) * dataPointInterval; + int statusHours = -1; + for (UptimeHistory hist : knownStatuses) { + if (hist.isRelay() != relay) { + continue; + } + long histEndMillis = hist.getStartMillis() + DateTimeHelper.ONE_HOUR + * hist.getUptimeHours(); + if (histEndMillis < intervalStartMillis) { + continue; + } + while (hist.getStartMillis() >= intervalStartMillis + + dataPointInterval) { + statusDataPoints.add(statusHours * 5 > dataPointIntervalHours + ? statusHours : -1); + statusHours = -1; + intervalStartMillis += dataPointInterval; + } + while (histEndMillis >= intervalStartMillis + dataPointInterval) { + if (statusHours < 0) { + statusHours = 0; + } + statusHours += (int) ((intervalStartMillis + dataPointInterval + - Math.max(Math.max(hist.getStartMillis(), + firstStatusStartMillis), intervalStartMillis)) + / DateTimeHelper.ONE_HOUR); + statusDataPoints.add(statusHours * 5 > dataPointIntervalHours + ? statusHours : -1); + statusHours = -1; + intervalStartMillis += dataPointInterval; + } + if (statusHours < 0) { + statusHours = 0; + } + statusHours += (int) ((histEndMillis - Math.max(Math.max( + hist.getStartMillis(), firstStatusStartMillis), + intervalStartMillis)) / DateTimeHelper.ONE_HOUR); + } + if (statusHours > 0) { + statusDataPoints.add(statusHours * 5 > dataPointIntervalHours + ? statusHours : -1); + } + List<Double> dataPoints = new ArrayList<Double>(); + for (int dataPointIndex = 0; dataPointIndex < statusDataPoints.size(); + dataPointIndex++) { + if (dataPointIndex >= uptimeDataPoints.size()) { + dataPoints.add(0.0); + } else if (uptimeDataPoints.get(dataPointIndex) >= 0 && + statusDataPoints.get(dataPointIndex) > 0) { + dataPoints.add(((double) uptimeDataPoints.get(dataPointIndex)) + / ((double) statusDataPoints.get(dataPointIndex))); + } else { + dataPoints.add(-1.0); + } + } + int firstNonNullIndex = -1, lastNonNullIndex = -1; + for (int dataPointIndex = 0; dataPointIndex < dataPoints.size(); + dataPointIndex++) { + double dataPoint = dataPoints.get(dataPointIndex); + if (dataPoint >= 0.0) { + if (firstNonNullIndex < 0) { + firstNonNullIndex = dataPointIndex; + } + lastNonNullIndex = dataPointIndex; + } + } + if (firstNonNullIndex < 0) { + return null; + } + long firstDataPointMillis = (((this.now - graphInterval) + / dataPointInterval) + firstNonNullIndex) + * dataPointInterval + dataPointInterval / 2L; + if (graphIntervalIndex > 0 && firstDataPointMillis >= + this.now - graphIntervals[graphIntervalIndex - 1]) { + /* Skip uptime history object, because it doesn't contain + * anything new that wasn't already contained in the last + * uptime history object(s). */ + return null; + } + long lastDataPointMillis = firstDataPointMillis + + (lastNonNullIndex - firstNonNullIndex) * dataPointInterval; + int count = lastNonNullIndex - firstNonNullIndex + 1; + GraphHistory graphHistory = new GraphHistory(); + graphHistory.setFirst(DateTimeHelper.format(firstDataPointMillis)); + graphHistory.setLast(DateTimeHelper.format(lastDataPointMillis)); + graphHistory.setInterval((int) (dataPointInterval + / DateTimeHelper.ONE_SECOND)); + graphHistory.setFactor(1.0 / 999.0); + graphHistory.setCount(count); + int previousNonNullIndex = -2; + boolean foundTwoAdjacentDataPoints = false; + List<Integer> values = new ArrayList<Integer>(); + for (int dataPointIndex = firstNonNullIndex; dataPointIndex <= + lastNonNullIndex; dataPointIndex++) { + double dataPoint = dataPoints.get(dataPointIndex); + if (dataPoint >= 0.0) { + if (dataPointIndex - previousNonNullIndex == 1) { + foundTwoAdjacentDataPoints = true; + } + previousNonNullIndex = dataPointIndex; + } + values.add(dataPoint < -0.5 ? null : ((int) (dataPoint * 999.0))); + } + graphHistory.setValues(values); + if (foundTwoAdjacentDataPoints) { + return graphHistory; + } else { + return null; + } + } + + public String getStatsString() { + StringBuilder sb = new StringBuilder(); + sb.append(" " + Logger.formatDecimalNumber(this.writtenDocuments) + + " uptime document files written\n"); + return sb.toString(); + } +} + diff --git a/src/org/torproject/onionoo/writer/WeightsDocumentWriter.java b/src/org/torproject/onionoo/writer/WeightsDocumentWriter.java new file mode 100644 index 0000000..ddd2774 --- /dev/null +++ b/src/org/torproject/onionoo/writer/WeightsDocumentWriter.java @@ -0,0 +1,233 @@ +/* Copyright 2012--2014 The Tor Project + * See LICENSE for licensing information */ +package org.torproject.onionoo.writer; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; + +import org.torproject.onionoo.docs.DocumentStore; +import org.torproject.onionoo.docs.GraphHistory; +import org.torproject.onionoo.docs.WeightsDocument; +import org.torproject.onionoo.docs.WeightsStatus; +import org.torproject.onionoo.updater.DescriptorSource; +import org.torproject.onionoo.updater.DescriptorType; +import org.torproject.onionoo.updater.FingerprintListener; +import org.torproject.onionoo.util.ApplicationFactory; +import org.torproject.onionoo.util.DateTimeHelper; +import org.torproject.onionoo.util.Logger; + +public class WeightsDocumentWriter implements FingerprintListener, + DocumentWriter { + + private DescriptorSource descriptorSource; + + private DocumentStore documentStore; + + private long now; + + public WeightsDocumentWriter() { + this.descriptorSource = ApplicationFactory.getDescriptorSource(); + this.documentStore = ApplicationFactory.getDocumentStore(); + this.now = ApplicationFactory.getTime().currentTimeMillis(); + this.registerFingerprintListeners(); + } + + private void registerFingerprintListeners() { + this.descriptorSource.registerFingerprintListener(this, + DescriptorType.RELAY_CONSENSUSES); + this.descriptorSource.registerFingerprintListener(this, + DescriptorType.RELAY_SERVER_DESCRIPTORS); + } + + private Set<String> updateWeightsDocuments = new HashSet<String>(); + + public void processFingerprints(SortedSet<String> fingerprints, + boolean relay) { + if (relay) { + this.updateWeightsDocuments.addAll(fingerprints); + } + } + + public void writeDocuments() { + this.writeWeightsDataFiles(); + Logger.printStatusTime("Wrote weights document files"); + } + + private void writeWeightsDataFiles() { + for (String fingerprint : this.updateWeightsDocuments) { + WeightsStatus weightsStatus = this.documentStore.retrieve( + WeightsStatus.class, true, fingerprint); + if (weightsStatus == null) { + continue; + } + SortedMap<long[], double[]> history = weightsStatus.getHistory(); + WeightsDocument weightsDocument = this.compileWeightsDocument( + fingerprint, history); + this.documentStore.store(weightsDocument, fingerprint); + } + Logger.printStatusTime("Wrote weights document files"); + } + + private String[] graphNames = new String[] { + "1_week", + "1_month", + "3_months", + "1_year", + "5_years" }; + + private long[] graphIntervals = new long[] { + DateTimeHelper.ONE_WEEK, + DateTimeHelper.ROUGHLY_ONE_MONTH, + DateTimeHelper.ROUGHLY_THREE_MONTHS, + DateTimeHelper.ROUGHLY_ONE_YEAR, + DateTimeHelper.ROUGHLY_FIVE_YEARS }; + + private long[] dataPointIntervals = new long[] { + DateTimeHelper.ONE_HOUR, + DateTimeHelper.FOUR_HOURS, + DateTimeHelper.TWELVE_HOURS, + DateTimeHelper.TWO_DAYS, + DateTimeHelper.TEN_DAYS }; + + private WeightsDocument compileWeightsDocument(String fingerprint, + SortedMap<long[], double[]> history) { + WeightsDocument weightsDocument = new WeightsDocument(); + weightsDocument.setFingerprint(fingerprint); + weightsDocument.setAdvertisedBandwidthFraction( + this.compileGraphType(history, 0)); + weightsDocument.setConsensusWeightFraction( + this.compileGraphType(history, 1)); + weightsDocument.setGuardProbability( + this.compileGraphType(history, 2)); + weightsDocument.setMiddleProbability( + this.compileGraphType(history, 3)); + weightsDocument.setExitProbability( + this.compileGraphType(history, 4)); + weightsDocument.setAdvertisedBandwidth( + this.compileGraphType(history, 5)); + weightsDocument.setConsensusWeight( + this.compileGraphType(history, 6)); + return weightsDocument; + } + + private Map<String, GraphHistory> compileGraphType( + SortedMap<long[], double[]> history, int graphTypeIndex) { + Map<String, GraphHistory> graphs = + new LinkedHashMap<String, GraphHistory>(); + for (int graphIntervalIndex = 0; graphIntervalIndex < + this.graphIntervals.length; graphIntervalIndex++) { + String graphName = this.graphNames[graphIntervalIndex]; + GraphHistory graphHistory = this.compileWeightsHistory( + graphTypeIndex, graphIntervalIndex, history); + if (graphHistory != null) { + graphs.put(graphName, graphHistory); + } + } + return graphs; + } + + private GraphHistory compileWeightsHistory(int graphTypeIndex, + int graphIntervalIndex, SortedMap<long[], double[]> history) { + long graphInterval = this.graphIntervals[graphIntervalIndex]; + long dataPointInterval = + this.dataPointIntervals[graphIntervalIndex]; + List<Double> dataPoints = new ArrayList<Double>(); + long intervalStartMillis = ((this.now - graphInterval) + / dataPointInterval) * dataPointInterval; + long totalMillis = 0L; + double totalWeightTimesMillis = 0.0; + for (Map.Entry<long[], double[]> e : history.entrySet()) { + long startMillis = e.getKey()[0], endMillis = e.getKey()[1]; + double weight = e.getValue()[graphTypeIndex]; + if (endMillis < intervalStartMillis) { + continue; + } + while ((intervalStartMillis / dataPointInterval) != + (endMillis / dataPointInterval)) { + dataPoints.add(totalMillis * 5L < dataPointInterval + ? -1.0 : totalWeightTimesMillis / (double) totalMillis); + totalWeightTimesMillis = 0.0; + totalMillis = 0L; + intervalStartMillis += dataPointInterval; + } + if (weight >= 0.0) { + totalWeightTimesMillis += weight + * ((double) (endMillis - startMillis)); + totalMillis += (endMillis - startMillis); + } + } + dataPoints.add(totalMillis * 5L < dataPointInterval + ? -1.0 : totalWeightTimesMillis / (double) totalMillis); + double maxValue = 0.0; + int firstNonNullIndex = -1, lastNonNullIndex = -1; + for (int dataPointIndex = 0; dataPointIndex < dataPoints.size(); + dataPointIndex++) { + double dataPoint = dataPoints.get(dataPointIndex); + if (dataPoint >= 0.0) { + if (firstNonNullIndex < 0) { + firstNonNullIndex = dataPointIndex; + } + lastNonNullIndex = dataPointIndex; + if (dataPoint > maxValue) { + maxValue = dataPoint; + } + } + } + if (firstNonNullIndex < 0) { + return null; + } + long firstDataPointMillis = (((this.now - graphInterval) + / dataPointInterval) + firstNonNullIndex) * dataPointInterval + + dataPointInterval / 2L; + if (graphIntervalIndex > 0 && firstDataPointMillis >= + this.now - graphIntervals[graphIntervalIndex - 1]) { + /* Skip weights history object, because it doesn't contain + * anything new that wasn't already contained in the last + * weights history object(s). */ + return null; + } + long lastDataPointMillis = firstDataPointMillis + + (lastNonNullIndex - firstNonNullIndex) * dataPointInterval; + double factor = ((double) maxValue) / 999.0; + int count = lastNonNullIndex - firstNonNullIndex + 1; + GraphHistory graphHistory = new GraphHistory(); + graphHistory.setFirst(DateTimeHelper.format(firstDataPointMillis)); + graphHistory.setLast(DateTimeHelper.format(lastDataPointMillis)); + graphHistory.setInterval((int) (dataPointInterval + / DateTimeHelper.ONE_SECOND)); + graphHistory.setFactor(factor); + graphHistory.setCount(count); + int previousNonNullIndex = -2; + boolean foundTwoAdjacentDataPoints = false; + List<Integer> values = new ArrayList<Integer>(); + for (int dataPointIndex = firstNonNullIndex; dataPointIndex <= + lastNonNullIndex; dataPointIndex++) { + double dataPoint = dataPoints.get(dataPointIndex); + if (dataPoint >= 0.0) { + if (dataPointIndex - previousNonNullIndex == 1) { + foundTwoAdjacentDataPoints = true; + } + previousNonNullIndex = dataPointIndex; + } + values.add(dataPoint < 0.0 ? null : + (int) ((dataPoint * 999.0) / maxValue)); + } + graphHistory.setValues(values); + if (foundTwoAdjacentDataPoints) { + return graphHistory; + } else { + return null; + } + } + + public String getStatsString() { + /* TODO Add statistics string. */ + return null; + } +} diff --git a/test/org/torproject/onionoo/DummyDescriptorSource.java b/test/org/torproject/onionoo/DummyDescriptorSource.java index 06ec499..e93b063 100644 --- a/test/org/torproject/onionoo/DummyDescriptorSource.java +++ b/test/org/torproject/onionoo/DummyDescriptorSource.java @@ -9,6 +9,10 @@ import java.util.SortedSet; import java.util.TreeSet;
import org.torproject.descriptor.Descriptor; +import org.torproject.onionoo.updater.DescriptorListener; +import org.torproject.onionoo.updater.DescriptorSource; +import org.torproject.onionoo.updater.DescriptorType; +import org.torproject.onionoo.updater.FingerprintListener;
public class DummyDescriptorSource extends DescriptorSource {
diff --git a/test/org/torproject/onionoo/DummyDocumentStore.java b/test/org/torproject/onionoo/DummyDocumentStore.java index 5a5905b..54311aa 100644 --- a/test/org/torproject/onionoo/DummyDocumentStore.java +++ b/test/org/torproject/onionoo/DummyDocumentStore.java @@ -7,6 +7,9 @@ import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet;
+import org.torproject.onionoo.docs.Document; +import org.torproject.onionoo.docs.DocumentStore; + public class DummyDocumentStore extends DocumentStore {
private Map<Class<? extends Document>, SortedMap<String, Document>> diff --git a/test/org/torproject/onionoo/DummyTime.java b/test/org/torproject/onionoo/DummyTime.java index 7178ed1..ffbd6e3 100644 --- a/test/org/torproject/onionoo/DummyTime.java +++ b/test/org/torproject/onionoo/DummyTime.java @@ -2,6 +2,8 @@ * See LICENSE for licensing information */ package org.torproject.onionoo;
+import org.torproject.onionoo.util.Time; + public class DummyTime extends Time { private long currentTimeMillis; public DummyTime(long currentTimeMillis) { diff --git a/test/org/torproject/onionoo/LookupServiceTest.java b/test/org/torproject/onionoo/LookupServiceTest.java index 23b5a91..052b4c0 100644 --- a/test/org/torproject/onionoo/LookupServiceTest.java +++ b/test/org/torproject/onionoo/LookupServiceTest.java @@ -22,6 +22,8 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; +import org.torproject.onionoo.updater.LookupResult; +import org.torproject.onionoo.updater.LookupService;
public class LookupServiceTest {
diff --git a/test/org/torproject/onionoo/ResourceServletTest.java b/test/org/torproject/onionoo/ResourceServletTest.java index f2ace5d..8cd3752 100644 --- a/test/org/torproject/onionoo/ResourceServletTest.java +++ b/test/org/torproject/onionoo/ResourceServletTest.java @@ -21,8 +21,14 @@ import java.util.TreeSet;
import org.junit.Before; import org.junit.Test; -import org.torproject.onionoo.ResourceServlet.HttpServletRequestWrapper; -import org.torproject.onionoo.ResourceServlet.HttpServletResponseWrapper; +import org.torproject.onionoo.docs.UpdateStatus; +import org.torproject.onionoo.server.NodeIndexer; +import org.torproject.onionoo.server.ResourceServlet; +import org.torproject.onionoo.server.ResourceServlet.HttpServletRequestWrapper; +import org.torproject.onionoo.server.ResourceServlet.HttpServletResponseWrapper; +import org.torproject.onionoo.util.ApplicationFactory; +import org.torproject.onionoo.util.DateTimeHelper; +import org.torproject.onionoo.util.Time;
import com.google.gson.Gson;
@@ -31,7 +37,7 @@ import com.google.gson.Gson; * which tests servlet specifics. */ public class ResourceServletTest {
- private SortedMap<String, org.torproject.onionoo.SummaryDocument> + private SortedMap<String, org.torproject.onionoo.docs.SummaryDocument> relays, bridges;
private long currentTimeMillis = DateTimeHelper.parse( @@ -102,8 +108,8 @@ public class ResourceServletTest {
@Before public void createSampleRelaysAndBridges() { - org.torproject.onionoo.SummaryDocument relayTorkaZ = - new org.torproject.onionoo.SummaryDocument(true, "TorkaZ", + org.torproject.onionoo.docs.SummaryDocument relayTorkaZ = + new org.torproject.onionoo.docs.SummaryDocument(true, "TorkaZ", "000C5F55BD4814B917CC474BD537F1A3B33CCE2A", Arrays.asList( new String[] { "62.216.201.221", "62.216.201.222", "62.216.201.223" }), DateTimeHelper.parse("2013-04-19 05:00:00"), @@ -114,8 +120,8 @@ public class ResourceServletTest { + "fb-token:np5_g_83jmf=", new TreeSet<String>(Arrays.asList( new String[] { "001C13B3A55A71B977CA65EC85539D79C653A3FC", "0025C136C1F3A9EEFE2AE3F918F03BFA21B5070B" }))); - org.torproject.onionoo.SummaryDocument relayFerrari458 = - new org.torproject.onionoo.SummaryDocument(true, "Ferrari458", + org.torproject.onionoo.docs.SummaryDocument relayFerrari458 = + new org.torproject.onionoo.docs.SummaryDocument(true, "Ferrari458", "001C13B3A55A71B977CA65EC85539D79C653A3FC", Arrays.asList( new String[] { "68.38.171.200", "[2001:4f8:3:2e::51]" }), DateTimeHelper.parse("2013-04-24 12:00:00"), true, @@ -124,8 +130,8 @@ public class ResourceServletTest { DateTimeHelper.parse("2013-02-12 16:00:00"), "AS7922", null, new TreeSet<String>(Arrays.asList(new String[] { "000C5F55BD4814B917CC474BD537F1A3B33CCE2A" }))); - org.torproject.onionoo.SummaryDocument relayTimMayTribute = - new org.torproject.onionoo.SummaryDocument(true, "TimMayTribute", + org.torproject.onionoo.docs.SummaryDocument relayTimMayTribute = + new org.torproject.onionoo.docs.SummaryDocument(true, "TimMayTribute", "0025C136C1F3A9EEFE2AE3F918F03BFA21B5070B", Arrays.asList( new String[] { "89.69.68.246" }), DateTimeHelper.parse("2013-04-22 20:00:00"), false, @@ -135,24 +141,24 @@ public class ResourceServletTest { "1024D/51E2A1C7 steven j. murdoch " + "tor+steven.murdoch@cl.cam.ac.uk fb-token:5sr_k_zs2wm=", new TreeSet<String>()); - org.torproject.onionoo.SummaryDocument bridgeec2bridgercc7f31fe = - new org.torproject.onionoo.SummaryDocument(false, + org.torproject.onionoo.docs.SummaryDocument bridgeec2bridgercc7f31fe = + new org.torproject.onionoo.docs.SummaryDocument(false, "ec2bridgercc7f31fe", "0000831B236DFF73D409AD17B40E2A728A53994F", Arrays.asList(new String[] { "10.199.7.176" }), DateTimeHelper.parse("2013-04-21 18:07:03"), false, new TreeSet<String>(Arrays.asList(new String[] { "Valid" })), -1L, null, DateTimeHelper.parse("2013-04-20 15:37:04"), null, null, null); - org.torproject.onionoo.SummaryDocument bridgeUnnamed = - new org.torproject.onionoo.SummaryDocument(false, "Unnamed", + org.torproject.onionoo.docs.SummaryDocument bridgeUnnamed = + new org.torproject.onionoo.docs.SummaryDocument(false, "Unnamed", "0002D9BDBBC230BD9C78FF502A16E0033EF87E0C", Arrays.asList( new String[] { "10.0.52.84" }), DateTimeHelper.parse("2013-04-20 17:37:04"), false, new TreeSet<String>(Arrays.asList(new String[] { "Valid" })), -1L, null, DateTimeHelper.parse("2013-04-14 07:07:05"), null, null, null); - org.torproject.onionoo.SummaryDocument bridgegummy = - new org.torproject.onionoo.SummaryDocument(false, "gummy", + org.torproject.onionoo.docs.SummaryDocument bridgegummy = + new org.torproject.onionoo.docs.SummaryDocument(false, "gummy", "1FEDE50ED8DBA1DD9F9165F78C8131E4A44AB756", Arrays.asList( new String[] { "10.63.169.98" }), DateTimeHelper.parse("2013-04-24 01:07:04"), true, @@ -160,7 +166,7 @@ public class ResourceServletTest { "Valid" })), -1L, null, DateTimeHelper.parse("2013-01-16 21:07:04"), null, null, null); this.relays = - new TreeMap<String, org.torproject.onionoo.SummaryDocument>(); + new TreeMap<String, org.torproject.onionoo.docs.SummaryDocument>(); this.relays.put("000C5F55BD4814B917CC474BD537F1A3B33CCE2A", relayTorkaZ); this.relays.put("001C13B3A55A71B977CA65EC85539D79C653A3FC", @@ -168,7 +174,7 @@ public class ResourceServletTest { this.relays.put("0025C136C1F3A9EEFE2AE3F918F03BFA21B5070B", relayTimMayTribute); this.bridges = - new TreeMap<String, org.torproject.onionoo.SummaryDocument>(); + new TreeMap<String, org.torproject.onionoo.docs.SummaryDocument>(); this.bridges.put("0000831B236DFF73D409AD17B40E2A728A53994F", bridgeec2bridgercc7f31fe); this.bridges.put("0002D9BDBBC230BD9C78FF502A16E0033EF87E0C", @@ -201,11 +207,11 @@ public class ResourceServletTest { updateStatus.setDocumentString(String.valueOf( this.currentTimeMillis)); documentStore.addDocument(updateStatus, null); - for (Map.Entry<String, org.torproject.onionoo.SummaryDocument> e : + for (Map.Entry<String, org.torproject.onionoo.docs.SummaryDocument> e : this.relays.entrySet()) { documentStore.addDocument(e.getValue(), e.getKey()); } - for (Map.Entry<String, org.torproject.onionoo.SummaryDocument> e : + for (Map.Entry<String, org.torproject.onionoo.docs.SummaryDocument> e : this.bridges.entrySet()) { documentStore.addDocument(e.getValue(), e.getKey()); } diff --git a/test/org/torproject/onionoo/UptimeDocumentWriterTest.java b/test/org/torproject/onionoo/UptimeDocumentWriterTest.java index 5065e4d..5a77514 100644 --- a/test/org/torproject/onionoo/UptimeDocumentWriterTest.java +++ b/test/org/torproject/onionoo/UptimeDocumentWriterTest.java @@ -10,6 +10,13 @@ import java.util.List;
import org.junit.Before; import org.junit.Test; +import org.torproject.onionoo.docs.GraphHistory; +import org.torproject.onionoo.docs.UptimeDocument; +import org.torproject.onionoo.docs.UptimeStatus; +import org.torproject.onionoo.updater.DescriptorType; +import org.torproject.onionoo.util.ApplicationFactory; +import org.torproject.onionoo.util.DateTimeHelper; +import org.torproject.onionoo.writer.UptimeDocumentWriter;
public class UptimeDocumentWriterTest {
diff --git a/test/org/torproject/onionoo/UptimeStatusTest.java b/test/org/torproject/onionoo/UptimeStatusTest.java index 884ccc5..671ffa3 100644 --- a/test/org/torproject/onionoo/UptimeStatusTest.java +++ b/test/org/torproject/onionoo/UptimeStatusTest.java @@ -9,6 +9,10 @@ import java.util.TreeSet;
import org.junit.Before; import org.junit.Test; +import org.torproject.onionoo.docs.UptimeHistory; +import org.torproject.onionoo.docs.UptimeStatus; +import org.torproject.onionoo.util.ApplicationFactory; +import org.torproject.onionoo.util.DateTimeHelper;
public class UptimeStatusTest {
diff --git a/test/org/torproject/onionoo/UptimeStatusUpdaterTest.java b/test/org/torproject/onionoo/UptimeStatusUpdaterTest.java index a34292b..8070ae4 100644 --- a/test/org/torproject/onionoo/UptimeStatusUpdaterTest.java +++ b/test/org/torproject/onionoo/UptimeStatusUpdaterTest.java @@ -6,6 +6,12 @@ import static org.junit.Assert.assertEquals;
import org.junit.Before; import org.junit.Test; +import org.torproject.onionoo.docs.UptimeHistory; +import org.torproject.onionoo.docs.UptimeStatus; +import org.torproject.onionoo.updater.DescriptorType; +import org.torproject.onionoo.updater.UptimeStatusUpdater; +import org.torproject.onionoo.util.ApplicationFactory; +import org.torproject.onionoo.util.DateTimeHelper;
public class UptimeStatusUpdaterTest {