commit b2b3363232fd1a425a6e83bde89b2687b56abc0a Author: Karsten Loesing karsten.loesing@gmx.net Date: Fri Aug 30 11:09:23 2019 +0200
Archive bridge pool assignments again.
Implements #31558. --- CHANGELOG.md | 1 + .../org/torproject/metrics/collector/Main.java | 3 + .../BridgePoolAssignmentsProcessor.java | 363 +++++++++++++++++++++ .../metrics/collector/conf/Annotation.java | 1 + .../metrics/collector/conf/Configuration.java | 1 + .../org/torproject/metrics/collector/conf/Key.java | 6 + .../persist/BridgePoolAssignmentPersistence.java | 34 ++ .../collector/persist/DescriptorPersistence.java | 2 + .../metrics/collector/sync/SyncPersistence.java | 6 + src/main/resources/collector.properties | 18 + .../metrics/collector/conf/ConfigurationTest.java | 2 +- .../metrics/collector/cron/CollecTorMainTest.java | 1 + 12 files changed, 437 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md index b38b124..4ecfb35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Remove Cobertura from the build process. - Archive snowflake statistics. - Update to metrics-lib 2.7.0. + - Archive bridge pool assignments again.
# Changes in version 1.9.1 - 2019-05-29 diff --git a/src/main/java/org/torproject/metrics/collector/Main.java b/src/main/java/org/torproject/metrics/collector/Main.java index 6907e93..3150ffc 100644 --- a/src/main/java/org/torproject/metrics/collector/Main.java +++ b/src/main/java/org/torproject/metrics/collector/Main.java @@ -4,6 +4,7 @@ package org.torproject.metrics.collector;
import org.torproject.metrics.collector.bridgedescs.SanitizedBridgesWriter; +import org.torproject.metrics.collector.bridgepools.BridgePoolAssignmentsProcessor; import org.torproject.metrics.collector.conf.Configuration; import org.torproject.metrics.collector.conf.ConfigurationException; import org.torproject.metrics.collector.conf.Key; @@ -49,6 +50,8 @@ public class Main {
static { // add a new main class here collecTorMains.put(Key.BridgedescsActivated, SanitizedBridgesWriter.class); + collecTorMains.put(Key.BridgePoolAssignmentsActivated, + BridgePoolAssignmentsProcessor.class); collecTorMains.put(Key.ExitlistsActivated, ExitListDownloader.class); collecTorMains.put(Key.UpdateindexActivated, CreateIndexJson.class); collecTorMains.put(Key.RelaydescsActivated, ArchiveWriter.class); diff --git a/src/main/java/org/torproject/metrics/collector/bridgepools/BridgePoolAssignmentsProcessor.java b/src/main/java/org/torproject/metrics/collector/bridgepools/BridgePoolAssignmentsProcessor.java new file mode 100644 index 0000000..cad91ef --- /dev/null +++ b/src/main/java/org/torproject/metrics/collector/bridgepools/BridgePoolAssignmentsProcessor.java @@ -0,0 +1,363 @@ +/* Copyright 2011--2019 The Tor Project + * See LICENSE for licensing information */ + +package org.torproject.metrics.collector.bridgepools; + +import org.torproject.metrics.collector.conf.Configuration; +import org.torproject.metrics.collector.conf.ConfigurationException; +import org.torproject.metrics.collector.conf.Key; +import org.torproject.metrics.collector.cron.CollecTorMain; + +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.DateTimeException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.Stack; +import java.util.TreeMap; + +public class BridgePoolAssignmentsProcessor extends CollecTorMain { + + /** + * Class logger. + */ + private static final Logger logger = LoggerFactory.getLogger( + BridgePoolAssignmentsProcessor.class); + + /** + * Directory containing original, not-yet-sanitized bridge pool assignment + * files. + */ + private File assignmentsDirectory; + + /** + * Directory containing sanitized bridge pool assignments for tarballs. + */ + private String outputPathName; + + /** + * Directory containing recently stored sanitized bridge pool assignments. + */ + private String recentPathName; + + /** + * Timestamp format in bridge-pool-assignments line. + */ + private DateTimeFormatter assignmentFormat = DateTimeFormatter.ofPattern( + "uuuu-MM-dd HH:mm:ss"); + + /** + * File name format. + */ + private DateTimeFormatter filenameFormat = DateTimeFormatter.ofPattern( + "uuuu/MM/dd/uuuu-MM-dd-HH-mm-ss"); + + /** + * Initialize this class with the given configuration. + */ + public BridgePoolAssignmentsProcessor(Configuration config) { + super(config); + } + + /** + * Return the module identifier. + * + * @return Module identifier. + */ + @Override + public String module() { + return "BridgePoolAssignments"; + } + + /** + * Return the synchronization marker. + * + * @return Synchronization marker. + */ + @Override + protected String syncMarker() { + return "BridgePoolAssignments"; + } + + /** + * Start processing files, which includes reading original, not-yet-sanitized + * bridge pool assignment files from disk, splitting them into bridge pool + * assignment descriptors, sanitizing contained fingerprints, and writing + * sanitized bridge pool assignments to disk. + * + * @throws ConfigurationException Thrown if configuration values cannot be + * obtained. + */ + @Override + protected void startProcessing() throws ConfigurationException { + logger.info("Starting bridge-pool-assignments module of CollecTor."); + this.initializeConfiguration(); + List<File> assignmentFiles = this.listAssignmentFiles(); + for (File assignmentFile : assignmentFiles) { + logger.info("Processing bridge pool assignment file '{}'...", + assignmentFile.getAbsolutePath()); + for (Map.Entry<LocalDateTime, SortedMap<String, String>> e + : this.readBridgePoolAssignments(assignmentFile).entrySet()) { + LocalDateTime published = e.getKey(); + SortedMap<String, String> originalAssignments = e.getValue(); + SortedMap<String, String> sanitizedAssignments + = this.sanitizeAssignments(originalAssignments); + if (null == sanitizedAssignments) { + logger.warn("Unable to sanitize assignments published at {}. " + + "Skipping.", published); + continue; + } + String formattedSanitizedAssignments = this.formatSanitizedAssignments( + published, sanitizedAssignments); + File tarballFile = Paths.get(this.outputPathName, + published.format(this.filenameFormat)).toFile(); + File rsyncFile = new File(this.recentPathName, + tarballFile.getName()); + File[] outputFiles = new File[] { tarballFile, rsyncFile }; + for (File outputFile : outputFiles) { + if (!outputFile.exists()) { + this.writeSanitizedAssignmentsToFile(outputFile, + formattedSanitizedAssignments); + } + } + } + } + this.cleanUpRsyncDirectory(); + logger.info("Finished processing bridge pool assignment file(s)."); + } + + /** + * Initialize configuration by obtaining current configuration values and + * storing them in instance attributes. + */ + private void initializeConfiguration() throws ConfigurationException { + this.outputPathName = Paths.get(config.getPath(Key.OutputPath).toString(), + "bridge-pool-assignments").toString(); + this.recentPathName = Paths.get(config.getPath(Key.RecentPath).toString(), + "bridge-pool-assignments").toString(); + this.assignmentsDirectory = + config.getPath(Key.BridgePoolAssignmentsLocalOrigins).toFile(); + } + + /** + * Compile a list of all assignment files in the input directory. + * + * @return List of assignment files. + */ + private List<File> listAssignmentFiles() { + List<File> assignmentFiles = new ArrayList<>(); + Stack<File> files = new Stack<>(); + files.add(this.assignmentsDirectory); + while (!files.isEmpty()) { + File file = files.pop(); + if (file.isDirectory()) { + File[] filesInDirectory = file.listFiles(); + if (null != filesInDirectory) { + files.addAll(Arrays.asList(filesInDirectory)); + } + } else if (file.getName().startsWith("assignments.log")) { + assignmentFiles.add(file); + } + } + return assignmentFiles; + } + + /** + * Read one or more bridge pool assignments from the given file and store them + * in a map with keys being published timestamps and values being maps of + * (original, not-yet-sanitized) fingerprints and assignment details. + * + * @param assignmentFile File containing one or more bridge pool assignments. + * @return Map containing all read bridge pool assignments. + */ + private SortedMap<LocalDateTime, SortedMap<String, String>> + readBridgePoolAssignments(File assignmentFile) { + SortedMap<LocalDateTime, SortedMap<String, String>> + readBridgePoolAssignments = new TreeMap<>(); + try { + BufferedReader br; + if (assignmentFile.getName().endsWith(".gz")) { + br = new BufferedReader(new InputStreamReader( + new GzipCompressorInputStream(new FileInputStream( + assignmentFile)))); + } else { + br = new BufferedReader(new FileReader(assignmentFile)); + } + String line; + SortedMap<String, String> currentAssignments = null; + while ((line = br.readLine()) != null) { + if (line.startsWith("bridge-pool-assignment ")) { + try { + LocalDateTime bridgePoolAssignmentTime = LocalDateTime.parse( + line.substring("bridge-pool-assignment ".length()), + this.assignmentFormat); + if (readBridgePoolAssignments.containsKey( + bridgePoolAssignmentTime)) { + logger.warn("Input file {} contains duplicate line: {}. " + + "Discarding previously read line and subsequent assignment " + + "lines.", assignmentFile, line); + } + currentAssignments = new TreeMap<>(); + readBridgePoolAssignments.put(bridgePoolAssignmentTime, + currentAssignments); + } catch (DateTimeException e) { + logger.warn("Could not parse timestamp from line {}. Skipping " + + "bridge pool assignment file '{}'.", line, + assignmentFile.getAbsolutePath(), e); + break; + } + } else if (null == currentAssignments) { + logger.warn("Input file {} does not start with a " + + "bridge-pool-assignments line. Skipping.", + assignmentFile); + break; + } else { + String[] parts = line.split(" ", 2); + if (parts.length < 2 || parts[0].length() < 40) { + logger.warn("Unrecognized line '{}'. Aborting.", line); + break; + } + if (currentAssignments.containsKey(parts[0])) { + logger.warn("Input file {} contains duplicate line: {}. " + + "Discarding previously read line.", assignmentFile, line); + } + currentAssignments.put(parts[0], parts[1]); + } + } + br.close(); + } catch (IOException e) { + logger.warn("Could not read bridge pool assignment file '{}'. " + + "Skipping.", assignmentFile.getAbsolutePath(), e); + } + if (!readBridgePoolAssignments.isEmpty() + && readBridgePoolAssignments.lastKey().minusMinutes(330L) + .isBefore(LocalDateTime.now())) { + logger.warn("The last known bridge pool assignment list was " + + "published at {}, which is more than 5:30 hours in the past.", + readBridgePoolAssignments.lastKey()); + } + return readBridgePoolAssignments; + } + + /** + * Sanitize the given bridge pool assignments by returning a new map with keys + * being SHA-1 digests of keys found in the given map. + * + * @param originalAssignments Map of (original, not-yet-sanitized) + * fingerprints to assignment details. + * @return Map of sanitized fingerprints to assignment details. + */ + private SortedMap<String, String> sanitizeAssignments( + SortedMap<String, String> originalAssignments) { + SortedMap<String, String> sanitizedAssignments = new TreeMap<>(); + for (Map.Entry<String, String> e : originalAssignments.entrySet()) { + String originalFingerprint = e.getKey(); + String assignmentDetails = e.getValue(); + try { + String hashedFingerprint = Hex.encodeHexString(DigestUtils.sha1( + Hex.decodeHex(originalFingerprint.toCharArray()))).toLowerCase(); + sanitizedAssignments.put(hashedFingerprint, assignmentDetails); + } catch (DecoderException ex) { + logger.warn("Unable to decode hex fingerprint. Aborting.", ex); + return null; + } + } + return sanitizedAssignments; + } + + /** + * Format sanitized bridge pool assignments consisting of a published + * timestamp and a map of sanitized fingerprints to assignment details as a + * single string. + * + * @param published Published timestamp as found in the bridge-pool-assignment + * line. + * @param sanitizedAssignments Map of sanitized fingerprints to assignment + * details. + * @return Formatted sanitized bridge pool assignments. + */ + private String formatSanitizedAssignments(LocalDateTime published, + SortedMap<String, String> sanitizedAssignments) { + StringBuilder sb = new StringBuilder(); + sb.append("@type bridge-pool-assignment 1.0\n"); + sb.append(String.format("bridge-pool-assignment %s\n", + published.format(this.assignmentFormat))); + for (Map.Entry<String, String> e : sanitizedAssignments.entrySet()) { + sb.append(String.format("%s %s%n", e.getKey(), e.getValue())); + } + return sb.toString(); + } + + /** + * Write the given formatted sanitized bridge pool assignments to the given + * file, or if that fails for any reason, log a warning and exit. + * + * @param outputFile File to write to. + * @param formattedSanitizedAssignments Formatted sanitized bridge pool + * assignments to write. + */ + private void writeSanitizedAssignmentsToFile(File outputFile, + String formattedSanitizedAssignments) { + if (!outputFile.getParentFile().exists() + && !outputFile.getParentFile().mkdirs()) { + logger.warn("Could not create parent directories of {}.", outputFile); + return; + } + try (BufferedWriter bw = new BufferedWriter(new FileWriter(outputFile))) { + bw.write(formattedSanitizedAssignments); + } catch (IOException e) { + logger.warn("Unable to write sanitized bridge pool assignments to {}.", + outputFile, e); + } + } + + /** + * Delete all files from the rsync directory that have not been modified in + * the last three days. + */ + public void cleanUpRsyncDirectory() { + Instant cutOff = Instant.now().minus(3L, ChronoUnit.DAYS); + Stack<File> allFiles = new Stack<>(); + allFiles.add(new File(this.recentPathName)); + while (!allFiles.isEmpty()) { + File file = allFiles.pop(); + if (file.isDirectory()) { + File[] filesInDirectory = file.listFiles(); + if (null != filesInDirectory) { + allFiles.addAll(Arrays.asList(filesInDirectory)); + } + } else if (Instant.ofEpochMilli(file.lastModified()).isBefore(cutOff)) { + try { + Files.deleteIfExists(file.toPath()); + } catch (IOException e) { + logger.warn("Unable to delete file {} that is apparently older than " + + "three days.", file, e); + } + } + } + } +} + + diff --git a/src/main/java/org/torproject/metrics/collector/conf/Annotation.java b/src/main/java/org/torproject/metrics/collector/conf/Annotation.java index 8cd3324..7d2bbe9 100644 --- a/src/main/java/org/torproject/metrics/collector/conf/Annotation.java +++ b/src/main/java/org/torproject/metrics/collector/conf/Annotation.java @@ -8,6 +8,7 @@ public enum Annotation {
BandwidthFile("@type bandwidth-file 1.0\n"), BridgeExtraInfo("@type bridge-extra-info 1.3\n"), + BridgePoolAssignment("@type bridge-pool-assignment 1.0\n"), BridgeServer("@type bridge-server-descriptor 1.2\n"), Cert("@type dir-key-certificate-3 1.0\n"), Consensus("@type network-status-consensus-3 1.0\n"), diff --git a/src/main/java/org/torproject/metrics/collector/conf/Configuration.java b/src/main/java/org/torproject/metrics/collector/conf/Configuration.java index 27f5125..59229e3 100644 --- a/src/main/java/org/torproject/metrics/collector/conf/Configuration.java +++ b/src/main/java/org/torproject/metrics/collector/conf/Configuration.java @@ -88,6 +88,7 @@ public class Configuration extends Observable implements Cloneable { private void anythingActivated() throws ConfigurationException { if (!(this.getBool(Key.RelaydescsActivated) || this.getBool(Key.BridgedescsActivated) + || this.getBool(Key.BridgePoolAssignmentsActivated) || this.getBool(Key.ExitlistsActivated) || this.getBool(Key.UpdateindexActivated) || this.getBool(Key.OnionPerfActivated) diff --git a/src/main/java/org/torproject/metrics/collector/conf/Key.java b/src/main/java/org/torproject/metrics/collector/conf/Key.java index e683fe2..dfef673 100644 --- a/src/main/java/org/torproject/metrics/collector/conf/Key.java +++ b/src/main/java/org/torproject/metrics/collector/conf/Key.java @@ -27,6 +27,7 @@ public enum Key { SyncPath(Path.class), RelaySources(SourceType[].class), BridgeSources(SourceType[].class), + BridgePoolAssignmentsSources(SourceType[].class), ExitlistSources(SourceType[].class), OnionPerfSources(SourceType[].class), WebstatsSources(SourceType[].class), @@ -35,6 +36,8 @@ public enum Key { RelaySyncOrigins(URL[].class), BridgeSyncOrigins(URL[].class), BridgeLocalOrigins(Path.class), + BridgePoolAssignmentsLocalOrigins(Path.class), + BridgePoolAssignmentsSyncOrigins(URL[].class), ExitlistSyncOrigins(URL[].class), OnionPerfSyncOrigins(URL[].class), WebstatsSyncOrigins(URL[].class), @@ -42,6 +45,9 @@ public enum Key { BridgedescsActivated(Boolean.class), BridgedescsOffsetMinutes(Integer.class), BridgedescsPeriodMinutes(Integer.class), + BridgePoolAssignmentsActivated(Boolean.class), + BridgePoolAssignmentsOffsetMinutes(Integer.class), + BridgePoolAssignmentsPeriodMinutes(Integer.class), ExitlistsActivated(Boolean.class), ExitlistsOffsetMinutes(Integer.class), ExitlistsPeriodMinutes(Integer.class), diff --git a/src/main/java/org/torproject/metrics/collector/persist/BridgePoolAssignmentPersistence.java b/src/main/java/org/torproject/metrics/collector/persist/BridgePoolAssignmentPersistence.java new file mode 100644 index 0000000..5613060 --- /dev/null +++ b/src/main/java/org/torproject/metrics/collector/persist/BridgePoolAssignmentPersistence.java @@ -0,0 +1,34 @@ +/* Copyright 2016--2018 The Tor Project + * See LICENSE for licensing information */ + +package org.torproject.metrics.collector.persist; + +import org.torproject.descriptor.BridgePoolAssignment; +import org.torproject.metrics.collector.conf.Annotation; + +import java.nio.file.Paths; + +public class BridgePoolAssignmentPersistence + extends DescriptorPersistence<BridgePoolAssignment> { + + public BridgePoolAssignmentPersistence(BridgePoolAssignment desc) { + super(desc, Annotation.BridgePoolAssignment.bytes()); + calculatePaths(); + } + + private void calculatePaths() { + String file = PersistenceUtils.dateTime(desc.getPublishedMillis()); + String[] parts = file.split(DASH); + this.recentPath = Paths.get( + BRIDGEPOOLASSIGNMENTS, + file).toString(); + this.storagePath = Paths.get( + BRIDGEPOOLASSIGNMENTS, + parts[0], // year + parts[1], // month + parts[2], // day + file).toString(); + } + +} + diff --git a/src/main/java/org/torproject/metrics/collector/persist/DescriptorPersistence.java b/src/main/java/org/torproject/metrics/collector/persist/DescriptorPersistence.java index fed4839..3e7a06b 100644 --- a/src/main/java/org/torproject/metrics/collector/persist/DescriptorPersistence.java +++ b/src/main/java/org/torproject/metrics/collector/persist/DescriptorPersistence.java @@ -18,6 +18,8 @@ public abstract class DescriptorPersistence<T extends Descriptor> { DescriptorPersistence.class);
protected static final String BRIDGEDESCS = "bridge-descriptors"; + protected static final String BRIDGEPOOLASSIGNMENTS + = "bridge-pool-assignments"; protected static final String DASH = "-"; protected static final String DOT = "."; protected static final String MICRODESC = "microdesc"; diff --git a/src/main/java/org/torproject/metrics/collector/sync/SyncPersistence.java b/src/main/java/org/torproject/metrics/collector/sync/SyncPersistence.java index 4b3b7bc..cfc3dbe 100644 --- a/src/main/java/org/torproject/metrics/collector/sync/SyncPersistence.java +++ b/src/main/java/org/torproject/metrics/collector/sync/SyncPersistence.java @@ -6,6 +6,7 @@ package org.torproject.metrics.collector.sync; import org.torproject.descriptor.BandwidthFile; import org.torproject.descriptor.BridgeExtraInfoDescriptor; import org.torproject.descriptor.BridgeNetworkStatus; +import org.torproject.descriptor.BridgePoolAssignment; import org.torproject.descriptor.BridgeServerDescriptor; import org.torproject.descriptor.Descriptor; import org.torproject.descriptor.ExitList; @@ -21,6 +22,7 @@ import org.torproject.metrics.collector.conf.ConfigurationException; import org.torproject.metrics.collector.conf.Key; import org.torproject.metrics.collector.persist.BandwidthFilePersistence; import org.torproject.metrics.collector.persist.BridgeExtraInfoPersistence; +import org.torproject.metrics.collector.persist.BridgePoolAssignmentPersistence; import org.torproject.metrics.collector.persist.BridgeServerDescriptorPersistence; import org.torproject.metrics.collector.persist.ConsensusPersistence; import org.torproject.metrics.collector.persist.DescriptorPersistence; @@ -132,6 +134,10 @@ public class SyncPersistence { descPersist = new BridgeServerDescriptorPersistence( (BridgeServerDescriptor) desc, received); break; + case "BridgePoolAssignment": + descPersist = new BridgePoolAssignmentPersistence( + (BridgePoolAssignment) desc); + break; case "ExitList": // downloaded is part of desc, which to use? descPersist = new ExitlistPersistence((ExitList) desc, received); break; diff --git a/src/main/resources/collector.properties b/src/main/resources/collector.properties index a4eed7a..b180a3e 100644 --- a/src/main/resources/collector.properties +++ b/src/main/resources/collector.properties @@ -18,6 +18,12 @@ BridgedescsPeriodMinutes = 60 # offset in minutes since the epoch and BridgedescsOffsetMinutes = 9 ## the following defines, if this module is activated +BridgePoolAssignmentsActivated = false +# period in minutes +BridgePoolAssignmentsPeriodMinutes = 60 +# offset in minutes since the epoch and +BridgePoolAssignmentsOffsetMinutes = 9 +## the following defines, if this module is activated ExitlistsActivated = false # period in minutes ExitlistsPeriodMinutes = 60 @@ -146,6 +152,18 @@ ReplaceIpAddressesWithHashes = false BridgeDescriptorMappingsLimit = inf # # +######## Bridge pool assignments ######## +# +## Define descriptor sources +# possible values: Sync, Local +BridgePoolAssignmentsSources = Local +## Retrieve files from the following instances. +## List of URLs separated by comma. +BridgePoolAssignmentsSyncOrigins = https://collector.torproject.org +## Relative path to directory to read bridge pool assignment files from +BridgePoolAssignmentsLocalOrigins = in/bridge-pool-assignments/ +# +# ######## Exit lists ######## # ## Define descriptor sources diff --git a/src/test/java/org/torproject/metrics/collector/conf/ConfigurationTest.java b/src/test/java/org/torproject/metrics/collector/conf/ConfigurationTest.java index 3a69c0c..201d541 100644 --- a/src/test/java/org/torproject/metrics/collector/conf/ConfigurationTest.java +++ b/src/test/java/org/torproject/metrics/collector/conf/ConfigurationTest.java @@ -38,7 +38,7 @@ public class ConfigurationTest { public void testKeyCount() { assertEquals("The number of properties keys in enum Key changed." + "\n This test class should be adapted.", - 59, Key.values().length); + 65, Key.values().length); }
@Test() diff --git a/src/test/java/org/torproject/metrics/collector/cron/CollecTorMainTest.java b/src/test/java/org/torproject/metrics/collector/cron/CollecTorMainTest.java index d0fe173..99f1f48 100644 --- a/src/test/java/org/torproject/metrics/collector/cron/CollecTorMainTest.java +++ b/src/test/java/org/torproject/metrics/collector/cron/CollecTorMainTest.java @@ -69,6 +69,7 @@ public class CollecTorMainTest { switch (marker) { case "Relay": case "Bridge": + case "BridgePoolAssignments": case "Exitlist": case "OnionPerf": case "Webstats":
tor-commits@lists.torproject.org