package com.subgraph.orchid.directory;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.List;
import java.util.TimeZone;
import java.util.logging.Logger;
import com.subgraph.orchid.TorException;
import com.subgraph.orchid.TorParsingException;
import com.subgraph.orchid.crypto.TorMessageDigest;
import com.subgraph.orchid.crypto.TorNTorKeyAgreement;
import com.subgraph.orchid.crypto.TorPublicKey;
import com.subgraph.orchid.crypto.TorSignature;
import com.subgraph.orchid.data.HexDigest;
import com.subgraph.orchid.data.IPv4Address;
import com.subgraph.orchid.data.Timestamp;
import com.subgraph.orchid.directory.parsing.DocumentFieldParser;
import com.subgraph.orchid.directory.parsing.DocumentObject;
import com.subgraph.orchid.directory.parsing.DocumentParsingHandler;
import com.subgraph.orchid.directory.parsing.NameIntegerParameter;
import com.subgraph.orchid.encoders.Base64;
public class DocumentFieldParserImpl implements DocumentFieldParser {
private final static Logger logger = Logger.getLogger(DocumentFieldParserImpl.class.getName());
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 ByteBuffer inputBuffer;
private final SimpleDateFormat dateFormat;
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 TorMessageDigest signatureDigest256;
private StringBuilder rawDocumentBuffer;
private DocumentParsingHandler callbackHandler;
public DocumentFieldParserImpl(ByteBuffer buffer) {
buffer.rewind();
this.inputBuffer = buffer;
rawDocumentBuffer = new StringBuilder();
dateFormat = createDateFormat();
}
private static SimpleDateFormat createDateFormat() {
final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
format.setTimeZone(TimeZone.getTimeZone("GMT"));
format.setLenient(false);
return format;
}
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[] parseIntegerList() {
final String item = getItem();
final String[] ns = item.split(",");
final int[] result = new int[ns.length];
for(int i = 0; i < result.length; i++) {
result[i] = parseInteger(ns[i]);
}
return result;
}
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() {
String timeAndDate = getItem() + " " + getItem();
try {
return new Timestamp(dateFormat.parse(timeAndDate));
} catch (ParseException e) {
throw new TorParsingException("Could not parse timestamp value: "+ timeAndDate);
}
}
public HexDigest parseHexDigest() {
return HexDigest.createFromString(parseString());
}
public HexDigest parseBase32Digest() {
return HexDigest.createFromBase32String(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("=");
break;
default:
break;
}
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 byte[] parseNtorPublicKey() {
final byte[] key = parseBase64Data();
if(key.length != TorNTorKeyAgreement.CURVE25519_PUBKEY_LEN) {
throw new TorParsingException("NTor public key was not expected length after base64 decoding. Length is "+ key.length);
}
return key;
}
public TorSignature parseSignature() {
final DocumentObject documentObject = parseObject();
TorSignature s = TorSignature.createFromPEMBuffer(documentObject.getContent());
return s;
}
public NameIntegerParameter parseParameter() {
final String item = getItem();
final int eq = item.indexOf('=');
if(eq == -1) {
throw new TorParsingException("Parameter not in expected form name=value");
}
final String name = item.substring(0, eq);
validateParameterName(name);
final int value = parseInteger(item.substring(eq + 1));
return new NameIntegerParameter(name, value);
}
private void validateParameterName(String name) {
if(name.isEmpty()) {
throw new TorParsingException("Parameter name cannot be empty");
}
for(char c: name.toCharArray()) {
if(!(Character.isLetterOrDigit(c) || c == '_')) {
throw new TorParsingException("Parameter name can only contain letters. Rejecting: "+ name);
}
}
}
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, 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.addFooterLine(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();
signatureDigest256 = new TorMessageDigest(true);
}
public void endSignedEntity() {
isProcessingSignedEntity = false;
}
public void setSignatureIgnoreToken(String token) {
signatureIgnoreToken = token;
}
public TorMessageDigest getSignatureMessageDigest() {
return signatureDigest;
}
public TorMessageDigest getSignatureMessageDigest256() {
return signatureDigest256;
}
private void updateRawDocument(String line) {
rawDocumentBuffer.append(line);
rawDocumentBuffer.append('\n');
}
public String getRawDocument() {
return rawDocumentBuffer.toString();
}
public void resetRawDocument() {
rawDocumentBuffer = new StringBuilder();
}
public void resetRawDocument(String initialContent) {
rawDocumentBuffer = new StringBuilder();
rawDocumentBuffer.append(initialContent);
}
public boolean verifySignedEntity(TorPublicKey publicKey, TorSignature signature) {
isProcessingSignedEntity = false;
return publicKey.verifySignature(signature, signatureDigest);
}
private String readLine() {
final String line = nextLineFromInputBuffer();
if(line != null) {
updateCurrentSignature(line);
updateRawDocument(line);
}
return line;
}
private String nextLineFromInputBuffer() {
if(!inputBuffer.hasRemaining()) {
return null;
}
final StringBuilder sb = new StringBuilder();
while(inputBuffer.hasRemaining()) {
char c = (char) (inputBuffer.get() & 0xFF);
if(c == '\n') {
return sb.toString();
} else if(c != '\r') {
sb.append(c);
}
}
return sb.toString();
}
private void updateCurrentSignature(String line) {
if(!isProcessingSignedEntity)
return;
if(signatureIgnoreToken != null && line.startsWith(signatureIgnoreToken))
return;
signatureDigest.update(line + "\n");
signatureDigest256.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.fine(message);
}
public void logError(String message) {
logger.warning(message);
}
public void logWarn(String message) {
logger.info(message);
}
}