package com.subgraph.orchid.directory; import java.nio.ByteBuffer; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.logging.Logger; import com.subgraph.orchid.Directory; import com.subgraph.orchid.DirectoryStore; import com.subgraph.orchid.GuardEntry; import com.subgraph.orchid.Router; import com.subgraph.orchid.Tor; import com.subgraph.orchid.DirectoryStore.CacheFile; import com.subgraph.orchid.crypto.TorRandom; public class StateFile { private final static Logger logger = Logger.getLogger(StateFile.class.getName()); private final static int DATE_LENGTH = 19; final static String KEYWORD_ENTRY_GUARD = "EntryGuard"; final static String KEYWORD_ENTRY_GUARD_ADDED_BY = "EntryGuardAddedBy"; final static String KEYWORD_ENTRY_GUARD_DOWN_SINCE = "EntryGuardDownSince"; final static String KEYWORD_ENTRY_GUARD_UNLISTED_SINCE = "EntryGuardUnlistedSince"; private final List<GuardEntryImpl> guardEntries = new ArrayList<GuardEntryImpl>(); private final TorRandom random = new TorRandom(); private final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); private class Line { final String line; int offset; Line(String line) { this.line = line; offset = 0; } private boolean hasChars() { return offset < line.length(); } private char getChar() { return line.charAt(offset); } private void incrementOffset(int n) { offset += n; if(offset > line.length()) { offset = line.length(); } } private void skipWhitespace() { while(hasChars() && Character.isWhitespace(getChar())) { offset += 1; } } String nextToken() { skipWhitespace(); if(!hasChars()) { return null; } final StringBuilder token = new StringBuilder(); while(hasChars() && !Character.isWhitespace(getChar())) { token.append(getChar()); offset += 1; } return token.toString(); } Date parseDate() { skipWhitespace(); if(!hasChars()) { return null; } try { final Date date = dateFormat.parse(line.substring(offset)); incrementOffset(DATE_LENGTH); return date; } catch (ParseException e) { return null; } } } String formatDate(Date date) { return dateFormat.format(date); } private final DirectoryStore directoryStore; private final Directory directory; StateFile(DirectoryStore store, Directory directory) { this.directoryStore = store; this.directory = directory; } public GuardEntry createGuardEntryFor(Router router) { final GuardEntryImpl entry = new GuardEntryImpl(directory, this, router.getNickname(), router.getIdentityHash().toString()); final String version = Tor.getImplementation() + "-" + Tor.getVersion(); entry.setVersion(version); /* * "Choose expiry time smudged over the last month." * * See add_an_entry_guard() in entrynodes.c */ final long createTime = (new Date()).getTime() - (random.nextInt(3600 * 24 * 30) * 1000L); entry.setCreatedTime(new Date(createTime)); return entry; } public List<GuardEntry> getGuardEntries() { synchronized (guardEntries) { return new ArrayList<GuardEntry>(guardEntries); } } public void removeGuardEntry(GuardEntry entry) { synchronized (guardEntries) { guardEntries.remove(entry); writeFile(); } } public void addGuardEntry(GuardEntry entry) { addGuardEntry(entry, true); } private void addGuardEntry(GuardEntry entry, boolean writeFile) { synchronized(guardEntries) { if(guardEntries.contains(entry)) { return; } final GuardEntryImpl impl = (GuardEntryImpl) entry; guardEntries.add(impl); synchronized (impl) { impl.setAddedFlag(); if(writeFile) { writeFile(); } } } } void writeFile() { directoryStore.writeData(CacheFile.STATE, getFileContents()); } ByteBuffer getFileContents() { final StringBuilder sb = new StringBuilder(); synchronized (guardEntries) { for(GuardEntryImpl entry: guardEntries) { sb.append(entry.writeToString()); } } return ByteBuffer.wrap(sb.toString().getBytes(Tor.getDefaultCharset())); } void parseBuffer(ByteBuffer buffer) { synchronized (guardEntries) { guardEntries.clear(); loadGuardEntries(buffer); } } private void loadGuardEntries(ByteBuffer buffer) { GuardEntryImpl currentEntry = null; while(true) { Line line = readLine(buffer); if(line == null) { addEntryIfValid(currentEntry); return; } currentEntry = processLine(line, currentEntry); } } private GuardEntryImpl processLine(Line line, GuardEntryImpl current) { final String keyword = line.nextToken(); if(keyword == null) { return current; } else if(keyword.equals(KEYWORD_ENTRY_GUARD)) { addEntryIfValid(current); GuardEntryImpl newEntry = processEntryGuardLine(line); if(newEntry == null) { return current; } else { return newEntry; } } else if(keyword.equals(KEYWORD_ENTRY_GUARD_ADDED_BY)) { processEntryGuardAddedBy(line, current); return current; } else if(keyword.equals(KEYWORD_ENTRY_GUARD_DOWN_SINCE)) { processEntryGuardDownSince(line, current); return current; } else if(keyword.equals(KEYWORD_ENTRY_GUARD_UNLISTED_SINCE)) { processEntryGuardUnlistedSince(line, current); return current; } else { return current; } } private GuardEntryImpl processEntryGuardLine(Line line) { final String name = line.nextToken(); final String identity = line.nextToken(); if(name == null || name.isEmpty() || identity == null || identity.isEmpty()) { logger.warning("Failed to parse EntryGuard line: "+ line.line); return null; } return new GuardEntryImpl(directory, this, name, identity); } private void processEntryGuardAddedBy(Line line, GuardEntryImpl current) { if(current == null) { logger.warning("EntryGuardAddedBy line seen before EntryGuard in state file"); return; } final String identity = line.nextToken(); final String version = line.nextToken(); final Date created = line.parseDate(); if(identity == null || identity.isEmpty() || version == null || version.isEmpty() || created == null) { logger.warning("Missing EntryGuardAddedBy field in state file"); return; } current.setVersion(version); current.setCreatedTime(created); } private void processEntryGuardDownSince(Line line, GuardEntryImpl current) { if(current == null) { logger.warning("EntryGuardDownSince line seen before EntryGuard in state file"); return; } final Date downSince = line.parseDate(); final Date lastTried = line.parseDate(); if(downSince == null) { logger.warning("Failed to parse date field in EntryGuardDownSince line in state file"); return; } current.setDownSince(downSince, lastTried); } private void processEntryGuardUnlistedSince(Line line, GuardEntryImpl current) { if(current == null) { logger.warning("EntryGuardUnlistedSince line seen before EntryGuard in state file"); return; } final Date unlistedSince = line.parseDate(); if(unlistedSince == null) { logger.warning("Failed to parse date field in EntryGuardUnlistedSince line in state file"); return; } current.setUnlistedSince(unlistedSince); } private void addEntryIfValid(GuardEntryImpl entry) { if(isValidEntry(entry)) { addGuardEntry(entry, false); } } private boolean isValidEntry(GuardEntryImpl entry) { return entry != null && entry.getNickname() != null && entry.getIdentity() != null && entry.getVersion() != null && entry.getCreatedTime() != null; } private Line readLine(ByteBuffer buffer) { if(!buffer.hasRemaining()) { return null; } final StringBuilder sb = new StringBuilder(); while(buffer.hasRemaining()) { char c = (char) (buffer.get() & 0xFF); if(c == '\n') { return new Line(sb.toString()); } else if(c != '\r') { sb.append(c); } } return new Line(sb.toString()); } }