package com.subgraph.orchid.directory; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collection; 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.logging.Logger; import com.subgraph.orchid.ConsensusDocument; import com.subgraph.orchid.ConsensusDocument.ConsensusFlavor; import com.subgraph.orchid.ConsensusDocument.RequiredCertificate; import com.subgraph.orchid.Descriptor; import com.subgraph.orchid.Directory; import com.subgraph.orchid.DirectoryServer; import com.subgraph.orchid.DirectoryStore; import com.subgraph.orchid.DirectoryStore.CacheFile; import com.subgraph.orchid.GuardEntry; import com.subgraph.orchid.KeyCertificate; import com.subgraph.orchid.Router; import com.subgraph.orchid.RouterDescriptor; import com.subgraph.orchid.RouterMicrodescriptor; import com.subgraph.orchid.RouterStatus; import com.subgraph.orchid.TorConfig; import com.subgraph.orchid.TorConfig.AutoBoolValue; import com.subgraph.orchid.TorException; import com.subgraph.orchid.crypto.TorRandom; import com.subgraph.orchid.data.HexDigest; import com.subgraph.orchid.data.RandomSet; import com.subgraph.orchid.directory.parsing.DocumentParser; import com.subgraph.orchid.directory.parsing.DocumentParserFactory; import com.subgraph.orchid.directory.parsing.DocumentParsingResult; import com.subgraph.orchid.events.Event; import com.subgraph.orchid.events.EventHandler; import com.subgraph.orchid.events.EventManager; public class DirectoryImpl implements Directory { private final static Logger logger = Logger.getLogger(DirectoryImpl.class.getName()); private final Object loadLock = new Object(); private boolean isLoaded = false; private final DirectoryStore store; private final TorConfig config; private final StateFile stateFile; private final DescriptorCache<RouterMicrodescriptor> microdescriptorCache; private final DescriptorCache<RouterDescriptor> basicDescriptorCache; private final Map<HexDigest, RouterImpl> routersByIdentity; private final Map<String, RouterImpl> routersByNickname; private final RandomSet<RouterImpl> directoryCaches; private final Set<ConsensusDocument.RequiredCertificate> requiredCertificates; private boolean haveMinimumRouterInfo; private boolean needRecalculateMinimumRouterInfo; private final EventManager consensusChangedManager; private final TorRandom random; private final static DocumentParserFactory parserFactory = new DocumentParserFactoryImpl(); private ConsensusDocument currentConsensus; private ConsensusDocument consensusWaitingForCertificates; public DirectoryImpl(TorConfig config, DirectoryStore customDirectoryStore) { store = (customDirectoryStore == null) ? (new DirectoryStoreImpl(config)) : (customDirectoryStore); this.config = config; stateFile = new StateFile(store, this); microdescriptorCache = createMicrodescriptorCache(store); basicDescriptorCache = createBasicDescriptorCache(store); routersByIdentity = new HashMap<HexDigest, RouterImpl>(); routersByNickname = new HashMap<String, RouterImpl>(); directoryCaches = new RandomSet<RouterImpl>(); requiredCertificates = new HashSet<ConsensusDocument.RequiredCertificate>(); consensusChangedManager = new EventManager(); random = new TorRandom(); } private static DescriptorCache<RouterMicrodescriptor> createMicrodescriptorCache(DirectoryStore store) { return new DescriptorCache<RouterMicrodescriptor>(store, CacheFile.MICRODESCRIPTOR_CACHE, CacheFile.MICRODESCRIPTOR_JOURNAL) { @Override protected DocumentParser<RouterMicrodescriptor> createDocumentParser(ByteBuffer buffer) { return parserFactory.createRouterMicrodescriptorParser(buffer); } }; } private static DescriptorCache<RouterDescriptor> createBasicDescriptorCache(DirectoryStore store) { return new DescriptorCache<RouterDescriptor>(store, CacheFile.DESCRIPTOR_CACHE, CacheFile.DESCRIPTOR_JOURNAL) { @Override protected DocumentParser<RouterDescriptor> createDocumentParser(ByteBuffer buffer) { return parserFactory.createRouterDescriptorParser(buffer, false); } }; } public synchronized boolean haveMinimumRouterInfo() { if(needRecalculateMinimumRouterInfo) { checkMinimumRouterInfo(); } return haveMinimumRouterInfo; } private synchronized void checkMinimumRouterInfo() { if(currentConsensus == null || !currentConsensus.isLive()) { needRecalculateMinimumRouterInfo = true; haveMinimumRouterInfo = false; return; } int routerCount = 0; int descriptorCount = 0; for(Router r: routersByIdentity.values()) { routerCount++; if(!r.isDescriptorDownloadable()) descriptorCount++; } needRecalculateMinimumRouterInfo = false; haveMinimumRouterInfo = (descriptorCount * 4 > routerCount); } public void loadFromStore() { logger.info("Loading cached network information from disk"); synchronized(loadLock) { if(isLoaded) { return; } boolean useMicrodescriptors = config.getUseMicrodescriptors() != AutoBoolValue.FALSE; last = System.currentTimeMillis(); logger.info("Loading certificates"); loadCertificates(store.loadCacheFile(CacheFile.CERTIFICATES)); logElapsed(); logger.info("Loading consensus"); loadConsensus(store.loadCacheFile(useMicrodescriptors ? CacheFile.CONSENSUS_MICRODESC : CacheFile.CONSENSUS)); logElapsed(); if(!useMicrodescriptors) { logger.info("Loading descriptors"); basicDescriptorCache.initialLoad(); } else { logger.info("Loading microdescriptor cache"); microdescriptorCache.initialLoad(); } needRecalculateMinimumRouterInfo = true; logElapsed(); logger.info("loading state file"); stateFile.parseBuffer(store.loadCacheFile(CacheFile.STATE)); logElapsed(); isLoaded = true; loadLock.notifyAll(); } } public void close() { basicDescriptorCache.shutdown(); microdescriptorCache.shutdown(); } private long last = 0; private void logElapsed() { final long now = System.currentTimeMillis(); final long elapsed = now - last; last = now; logger.fine("Loaded in "+ elapsed + " ms."); } private void loadCertificates(ByteBuffer buffer) { final DocumentParser<KeyCertificate> parser = parserFactory.createKeyCertificateParser(buffer); final DocumentParsingResult<KeyCertificate> result = parser.parse(); if(testResult(result, "certificates")) { for(KeyCertificate cert: result.getParsedDocuments()) { addCertificate(cert); } } } private void loadConsensus(ByteBuffer buffer) { final DocumentParser<ConsensusDocument> parser = parserFactory.createConsensusDocumentParser(buffer); final DocumentParsingResult<ConsensusDocument> result = parser.parse(); if(testResult(result, "consensus")) { addConsensusDocument(result.getDocument(), true); } } private boolean testResult(DocumentParsingResult<?> result, String type) { if(result.isOkay()) { return true; } else if(result.isError()) { logger.warning("Parsing error loading "+ type + " : "+ result.getMessage()); } else if(result.isInvalid()) { logger.warning("Problem loading "+ type + " : "+ result.getMessage()); } else { logger.warning("Unknown problem loading "+ type); } return false; } public void waitUntilLoaded() { synchronized (loadLock) { while(!isLoaded) { try { loadLock.wait(); } catch (InterruptedException e) { logger.warning("Thread interrupted while waiting for directory to load from disk"); } } } } public Collection<DirectoryServer> getDirectoryAuthorities() { return TrustedAuthorities.getInstance().getAuthorityServers(); } public DirectoryServer getRandomDirectoryAuthority() { final List<DirectoryServer> servers = TrustedAuthorities.getInstance().getAuthorityServers(); final int idx = random.nextInt(servers.size()); return servers.get(idx); } public Set<ConsensusDocument.RequiredCertificate> getRequiredCertificates() { return new HashSet<ConsensusDocument.RequiredCertificate>(requiredCertificates); } public void addCertificate(KeyCertificate certificate) { synchronized(TrustedAuthorities.getInstance()) { final boolean wasRequired = removeRequiredCertificate(certificate); final DirectoryServer as = TrustedAuthorities.getInstance().getAuthorityServerByIdentity(certificate.getAuthorityFingerprint()); if(as == null) { logger.warning("Certificate read for unknown directory authority with identity: "+ certificate.getAuthorityFingerprint()); return; } as.addCertificate(certificate); if(consensusWaitingForCertificates != null && wasRequired) { switch(consensusWaitingForCertificates.verifySignatures()) { case STATUS_FAILED: consensusWaitingForCertificates = null; return; case STATUS_VERIFIED: addConsensusDocument(consensusWaitingForCertificates, false); consensusWaitingForCertificates = null; return; case STATUS_NEED_CERTS: requiredCertificates.addAll(consensusWaitingForCertificates.getRequiredCertificates()); return; } } } } private boolean removeRequiredCertificate(KeyCertificate certificate) { final Iterator<RequiredCertificate> it = requiredCertificates.iterator(); while(it.hasNext()) { RequiredCertificate r = it.next(); if(r.getSigningKey().equals(certificate.getAuthoritySigningKey().getFingerprint())) { it.remove(); return true; } } return false; } public void storeCertificates() { synchronized(TrustedAuthorities.getInstance()) { final List<KeyCertificate> certs = new ArrayList<KeyCertificate>(); for(DirectoryServer ds: TrustedAuthorities.getInstance().getAuthorityServers()) { certs.addAll(ds.getCertificates()); } store.writeDocumentList(CacheFile.CERTIFICATES, certs); } } public void addRouterDescriptors(List<RouterDescriptor> descriptors) { basicDescriptorCache.addDescriptors(descriptors); needRecalculateMinimumRouterInfo = true; } public synchronized void addConsensusDocument(ConsensusDocument consensus, boolean fromCache) { if(consensus.equals(currentConsensus)) return; if(currentConsensus != null && consensus.getValidAfterTime().isBefore(currentConsensus.getValidAfterTime())) { logger.warning("New consensus document is older than current consensus document"); return; } synchronized(TrustedAuthorities.getInstance()) { switch(consensus.verifySignatures()) { case STATUS_FAILED: logger.warning("Unable to verify signatures on consensus document, discarding..."); return; case STATUS_NEED_CERTS: consensusWaitingForCertificates = consensus; requiredCertificates.addAll(consensus.getRequiredCertificates()); return; case STATUS_VERIFIED: break; } requiredCertificates.addAll(consensus.getRequiredCertificates()); } final Map<HexDigest, RouterImpl> oldRouterByIdentity = new HashMap<HexDigest, RouterImpl>(routersByIdentity); clearAll(); for(RouterStatus status: consensus.getRouterStatusEntries()) { if(status.hasFlag("Running") && status.hasFlag("Valid")) { final RouterImpl router = updateOrCreateRouter(status, oldRouterByIdentity); addRouter(router); classifyRouter(router); } final Descriptor d = getDescriptorForRouterStatus(status, consensus.getFlavor() == ConsensusFlavor.MICRODESC); if(d != null) { d.setLastListed(consensus.getValidAfterTime().getTime()); } } logger.fine("Loaded "+ routersByIdentity.size() +" routers from consensus document"); currentConsensus = consensus; if(!fromCache) { storeCurrentConsensus(); } consensusChangedManager.fireEvent(new Event() {}); } private void storeCurrentConsensus() { if(currentConsensus != null) { if(currentConsensus.getFlavor() == ConsensusFlavor.MICRODESC) { store.writeDocument(CacheFile.CONSENSUS_MICRODESC, currentConsensus); } else { store.writeDocument(CacheFile.CONSENSUS, currentConsensus); } } } private Descriptor getDescriptorForRouterStatus(RouterStatus rs, boolean isMicrodescriptor) { if(isMicrodescriptor) { return microdescriptorCache.getDescriptor(rs.getMicrodescriptorDigest()); } else { return basicDescriptorCache.getDescriptor(rs.getDescriptorDigest()); } } private RouterImpl updateOrCreateRouter(RouterStatus status, Map<HexDigest, RouterImpl> knownRouters) { final RouterImpl router = knownRouters.get(status.getIdentity()); if(router == null) return RouterImpl.createFromRouterStatus(this, status); router.updateStatus(status); return router; } private void clearAll() { routersByIdentity.clear(); routersByNickname.clear(); directoryCaches.clear(); } private void classifyRouter(RouterImpl router) { if(isValidDirectoryCache(router)) { directoryCaches.add(router); } else { directoryCaches.remove(router); } } private boolean isValidDirectoryCache(RouterImpl router) { if(router.getDirectoryPort() == 0) return false; if(router.hasFlag("BadDirectory")) return false; return router.hasFlag("V2Dir"); } private void addRouter(RouterImpl router) { routersByIdentity.put(router.getIdentityHash(), router); addRouterByNickname(router); } private void addRouterByNickname(RouterImpl router) { final String name = router.getNickname(); if(name == null || name.equals("Unnamed")) return; if(routersByNickname.containsKey(router.getNickname())) { //logger.warn("Duplicate router nickname: "+ router.getNickname()); return; } routersByNickname.put(name, router); } public synchronized void addRouterMicrodescriptors(List<RouterMicrodescriptor> microdescriptors) { microdescriptorCache.addDescriptors(microdescriptors); needRecalculateMinimumRouterInfo = true; } synchronized public List<Router> getRoutersWithDownloadableDescriptors() { waitUntilLoaded(); final List<Router> routers = new ArrayList<Router>(); for(RouterImpl router: routersByIdentity.values()) { if(router.isDescriptorDownloadable()) routers.add(router); } for(int i = 0; i < routers.size(); i++) { final Router a = routers.get(i); final int swapIdx = random.nextInt(routers.size()); final Router b = routers.get(swapIdx); routers.set(i, b); routers.set(swapIdx, a); } return routers; } public ConsensusDocument getCurrentConsensusDocument() { return currentConsensus; } public boolean hasPendingConsensus() { synchronized (TrustedAuthorities.getInstance()) { return consensusWaitingForCertificates != null; } } public void registerConsensusChangedHandler(EventHandler handler) { consensusChangedManager.addListener(handler); } public void unregisterConsensusChangedHandler(EventHandler handler) { consensusChangedManager.removeListener(handler); } public Router getRouterByName(String name) { if(name.equals("Unnamed")) { return null; } if(name.length() == 41 && name.charAt(0) == '$') { try { final HexDigest identity = HexDigest.createFromString(name.substring(1)); return getRouterByIdentity(identity); } catch (Exception e) { return null; } } waitUntilLoaded(); return routersByNickname.get(name); } public Router getRouterByIdentity(HexDigest identity) { waitUntilLoaded(); synchronized (routersByIdentity) { return routersByIdentity.get(identity); } } public List<Router> getRouterListByNames(List<String> names) { waitUntilLoaded(); final List<Router> routers = new ArrayList<Router>(); for(String n: names) { final Router r = getRouterByName(n); if(r == null) throw new TorException("Could not find router named: "+ n); routers.add(r); } return routers; } public List<Router> getAllRouters() { waitUntilLoaded(); synchronized(routersByIdentity) { return new ArrayList<Router>(routersByIdentity.values()); } } public GuardEntry createGuardEntryFor(Router router) { waitUntilLoaded(); return stateFile.createGuardEntryFor(router); } public List<GuardEntry> getGuardEntries() { waitUntilLoaded(); return stateFile.getGuardEntries(); } public void removeGuardEntry(GuardEntry entry) { waitUntilLoaded(); stateFile.removeGuardEntry(entry); } public void addGuardEntry(GuardEntry entry) { waitUntilLoaded(); stateFile.addGuardEntry(entry); } public RouterMicrodescriptor getMicrodescriptorFromCache(HexDigest descriptorDigest) { return microdescriptorCache.getDescriptor(descriptorDigest); } public RouterDescriptor getBasicDescriptorFromCache(HexDigest descriptorDigest) { return basicDescriptorCache.getDescriptor(descriptorDigest); } }