package org.torproject.jtor.directory.impl;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.List;
import org.bouncycastle.util.encoders.Base64;
import org.torproject.jtor.TorException;
import org.torproject.jtor.TorParsingException;
import org.torproject.jtor.crypto.TorMessageDigest;
import org.torproject.jtor.crypto.TorPublicKey;
import org.torproject.jtor.crypto.TorSignature;
import org.torproject.jtor.data.HexDigest;
import org.torproject.jtor.data.IPv4Address;
import org.torproject.jtor.data.Timestamp;
import org.torproject.jtor.directory.parsing.DocumentFieldParser;
import org.torproject.jtor.directory.parsing.DocumentObject;
import org.torproject.jtor.directory.parsing.DocumentParsingHandler;
import org.torproject.jtor.logging.Logger;
public class DocumentFieldParserImpl implements DocumentFieldParser {
private final static String BEGIN_TAG = "-----BEGIN";
private final static String END_TAG = "-----END";
private final static String TAG_DELIMITER = "-----";
private final static String DEFAULT_DELIMITER = " ";
private final BufferedReader reader;
private final Logger logger;
private String delimiter = DEFAULT_DELIMITER;
private String currentKeyword;
private List<String> currentItems;
private int currentItemsPosition;
private boolean recognizeOpt;
/* If a line begins with this string do not include it in the current signature. */
private String signatureIgnoreToken;
private boolean isProcessingSignedEntity = false;
private TorMessageDigest signatureDigest;
private StringBuilder rawDocumentBuffer;
private DocumentParsingHandler callbackHandler;
public DocumentFieldParserImpl(InputStream input, Logger logger) {
try {
reader = new BufferedReader(new InputStreamReader(input, "ISO-8859-1"));
} catch (UnsupportedEncodingException e) {
throw new TorException(e);
}
this.logger = logger;
rawDocumentBuffer = new StringBuilder();
}
public DocumentFieldParserImpl(Reader reader, Logger logger) {
if(reader instanceof BufferedReader) {
this.reader = (BufferedReader) reader;
} else {
this.reader = new BufferedReader(reader);
}
this.logger = logger;
rawDocumentBuffer = new StringBuilder();
}
public String parseNickname() {
// XXX verify valid nickname
return getItem();
}
public String parseString() {
return getItem();
}
public void setRecognizeOpt() {
recognizeOpt = true;
}
public void setHandler(DocumentParsingHandler handler) {
callbackHandler = handler;
}
public void setDelimiter(String delimiter) {
this.delimiter = delimiter;
}
public int argumentsRemaining() {
return currentItems.size() - currentItemsPosition;
}
private String getItem() {
if(currentItemsPosition >= currentItems.size())
throw new TorParsingException("Overrun while reading arguments");
return currentItems.get(currentItemsPosition++);
}
/*
* Return a string containing all remaining arguments concatenated together
*/
public String parseConcatenatedString() {
StringBuilder result = new StringBuilder();
while(argumentsRemaining() > 0) {
if(result.length() > 0)
result.append(" ");
result.append(getItem());
}
return result.toString();
}
public boolean parseBoolean() {
final int i = parseInteger();
if(i == 1)
return true;
else if(i == 0)
return false;
else
throw new TorParsingException("Illegal boolean value: "+ i);
}
public int parseInteger() {
return parseInteger(getItem());
}
public int parseInteger(String item) {
try {
return Integer.parseInt(item);
} catch(NumberFormatException e) {
throw new TorParsingException("Failed to parse expected integer value: " + item);
}
}
public int parsePort() {
return parsePort(getItem());
}
public int parsePort(String item) {
final int port = parseInteger(item);
if(port < 0 || port > 65535)
throw new TorParsingException("Illegal port value: " + port);
return port;
}
public Timestamp parseTimestamp() {
return Timestamp.createFromDateAndTimeString(getItem() +" "+ getItem());
}
public HexDigest parseHexDigest() {
return HexDigest.createFromString(parseString());
}
public HexDigest parseFingerprint() {
return HexDigest.createFromString(parseConcatenatedString());
}
public void verifyExpectedArgumentCount(String keyword, int argumentCount) {
verifyExpectedArgumentCount(keyword, argumentCount, argumentCount);
}
private void verifyExpectedArgumentCount(String keyword, int expectedMin, int expectedMax) {
final int argumentCount = argumentsRemaining();
if(expectedMin != -1 && argumentCount < expectedMin)
throw new TorParsingException("Not enough arguments for keyword '"+ keyword +"' expected "+ expectedMin +" and got "+ argumentCount);
if(expectedMax != -1 && argumentCount > expectedMax)
// Is this the correct thing to do, or should just be a warning?
throw new TorParsingException("Too many arguments for keyword '"+ keyword +"' expected "+ expectedMax +" and got "+ argumentCount);
}
public byte[] parseBase64Data() {
final StringBuilder string = new StringBuilder(getItem());
switch(string.length() % 4) {
case 2:
string.append("==");
break;
case 3:
string.append("=");
}
try {
return Base64.decode(string.toString().getBytes("ISO-8859-1"));
} catch (UnsupportedEncodingException e) {
throw new TorException(e);
}
}
public IPv4Address parseAddress() {
return IPv4Address.createFromString(getItem());
}
public TorPublicKey parsePublicKey() {
final DocumentObject documentObject = parseObject();
return TorPublicKey.createFromPEMBuffer(documentObject.getContent());
}
public TorSignature parseSignature() {
final DocumentObject documentObject = parseObject();
TorSignature s = TorSignature.createFromPEMBuffer(documentObject.getContent());
return s;
}
public DocumentObject parseTypedObject(String type) {
final DocumentObject object = parseObject();
if(!type.equals(object.getKeyword()))
throw new TorParsingException("Unexpected object type. Expecting: "+ type +", but got: "+ object.getKeyword());
return object;
}
public DocumentObject parseObject() {
final String line = readLine();
final String keyword = parseObjectHeader(line);
final DocumentObject object = new DocumentObject(keyword);
object.addContent(line);
parseObjectBody(object, keyword);
return object;
}
private String parseObjectHeader(String headerLine) {
if(!(headerLine.startsWith(BEGIN_TAG) && headerLine.endsWith(TAG_DELIMITER)))
throw new TorParsingException("Did not find expected object start tag.");
return headerLine.substring(BEGIN_TAG.length() + 1,
headerLine.length() - TAG_DELIMITER.length());
}
private void parseObjectBody(DocumentObject object, String keyword) {
final String endTag = END_TAG +" "+ keyword +TAG_DELIMITER;
while(true) {
final String line = readLine();
if(line == null) {
throw new TorParsingException("EOF reached before end of '"+ keyword +"' object.");
}
if(line.equals(endTag)) {
object.addContent(line);
return;
}
parseObjectContent(object, line);
}
}
private void parseObjectContent(DocumentObject object, String content) {
// XXX verify legal base64 data
object.addContent(content);
}
public String getCurrentKeyword() {
return currentKeyword;
}
public void processDocument() {
if(callbackHandler == null)
throw new TorException("DocumentFieldParser#processDocument() called with null callbackHandler");
while(true) {
final String line = readLine();
if(line == null) {
callbackHandler.endOfDocument();
return;
}
if(processLine(line))
callbackHandler.parseKeywordLine();
}
}
public void startSignedEntity() {
isProcessingSignedEntity = true;
signatureDigest = new TorMessageDigest();
}
public void endSignedEntity() {
isProcessingSignedEntity = false;
}
public void setSignatureIgnoreToken(String token) {
signatureIgnoreToken = token;
}
public TorMessageDigest getSignatureMessageDigest() {
return signatureDigest;
}
private void updateRawDocument(String line) {
rawDocumentBuffer.append(line);
rawDocumentBuffer.append('\n');
}
public String getRawDocument() {
return rawDocumentBuffer.toString();
}
public void resetRawDocument() {
rawDocumentBuffer = new StringBuilder();
}
public boolean verifySignedEntity(TorPublicKey publicKey, TorSignature signature) {
isProcessingSignedEntity = false;
logger.debug("digest parsed: " + new String(signatureDigest.getDigestBytes()));
return publicKey.verifySignature(signature, signatureDigest);
}
private String readLine() {
try {
final String line = reader.readLine();
if(line != null) {
updateCurrentSignature(line);
updateRawDocument(line);
}
return line;
} catch (IOException e) {
throw new TorParsingException("I/O error parsing document: " + e.getMessage(), e);
}
}
private void updateCurrentSignature(String line) {
if (line == null)
return;
if(!isProcessingSignedEntity)
return;
if(signatureIgnoreToken != null && line.startsWith(signatureIgnoreToken))
return;
signatureDigest.update(line + "\n");
}
private boolean processLine(String line) {
final List<String> lineItems = Arrays.asList(line.split(delimiter));
if(lineItems.size() == 0 || lineItems.get(0).length() == 0) {
// XXX warn
return false;
}
currentKeyword = lineItems.get(0);
currentItems = lineItems;
currentItemsPosition = 1;
if(recognizeOpt && currentKeyword.equals("opt") && lineItems.size() > 1) {
currentKeyword = lineItems.get(1);
currentItemsPosition = 2;
}
return true;
}
public void logDebug(String message) {
logger.debug(message);
}
public void logError(String message) {
logger.error(message);
}
public void logWarn(String message) {
logger.warning(message);
}
}