SAM: Fix parser to allow spaces in quoted values (tickets #1325, #1488)

Map keys to upper case
Catch some other parse errors
This commit is contained in:
zzz
2015-11-28 18:28:15 +00:00
parent 38c8e017a8
commit 87fa1cb1ac
5 changed files with 189 additions and 124 deletions

View File

@ -13,7 +13,6 @@ import java.net.Socket;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.nio.channels.SocketChannel; import java.nio.channels.SocketChannel;
import java.util.Properties; import java.util.Properties;
import java.util.StringTokenizer;
import net.i2p.I2PAppContext; import net.i2p.I2PAppContext;
import net.i2p.data.DataHelper; import net.i2p.data.DataHelper;
@ -41,7 +40,7 @@ class SAMHandlerFactory {
*/ */
public static SAMHandler createSAMHandler(SocketChannel s, Properties i2cpProps, public static SAMHandler createSAMHandler(SocketChannel s, Properties i2cpProps,
SAMBridge parent) throws SAMException { SAMBridge parent) throws SAMException {
StringTokenizer tok; String line;
Log log = I2PAppContext.getGlobalContext().logManager().getLog(SAMHandlerFactory.class); Log log = I2PAppContext.getGlobalContext().logManager().getLog(SAMHandlerFactory.class);
try { try {
@ -49,9 +48,8 @@ class SAMHandlerFactory {
sock.setKeepAlive(true); sock.setKeepAlive(true);
StringBuilder buf = new StringBuilder(128); StringBuilder buf = new StringBuilder(128);
ReadLine.readLine(sock, buf, HELLO_TIMEOUT); ReadLine.readLine(sock, buf, HELLO_TIMEOUT);
String line = buf.toString();
sock.setSoTimeout(0); sock.setSoTimeout(0);
tok = new StringTokenizer(line.trim(), " "); line = buf.toString();
} catch (SocketTimeoutException e) { } catch (SocketTimeoutException e) {
throw new SAMException("Timeout waiting for HELLO VERSION", e); throw new SAMException("Timeout waiting for HELLO VERSION", e);
} catch (IOException e) { } catch (IOException e) {
@ -61,15 +59,13 @@ class SAMHandlerFactory {
} }
// Message format: HELLO VERSION [MIN=v1] [MAX=v2] // Message format: HELLO VERSION [MIN=v1] [MAX=v2]
if (tok.countTokens() < 2) { Properties props = SAMUtils.parseParams(line);
if (!"HELLO".equals(props.getProperty(SAMUtils.COMMAND)) ||
!"VERSION".equals(props.getProperty(SAMUtils.OPCODE))) {
throw new SAMException("Must start with HELLO VERSION"); throw new SAMException("Must start with HELLO VERSION");
} }
if (!tok.nextToken().equals("HELLO") || props.remove(SAMUtils.COMMAND);
!tok.nextToken().equals("VERSION")) { props.remove(SAMUtils.OPCODE);
throw new SAMException("Must start with HELLO VERSION");
}
Properties props = SAMUtils.parseParams(tok);
String minVer = props.getProperty("MIN"); String minVer = props.getProperty("MIN");
if (minVer == null) { if (minVer == null) {

View File

@ -11,9 +11,9 @@ package net.i2p.sam;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Properties; import java.util.Properties;
import java.util.StringTokenizer;
import net.i2p.I2PAppContext; import net.i2p.I2PAppContext;
import net.i2p.I2PException; import net.i2p.I2PException;
@ -159,95 +159,176 @@ class SAMUtils {
return d; return d;
} }
public static final String COMMAND = "\"\"COMMAND\"\"";
public static final String OPCODE = "\"\"OPCODE\"\"";
/** /**
* Parse SAM parameters, and put them into a Propetries object * Parse SAM parameters, and put them into a Propetries object
* *
* @param tok A StringTokenizer pointing to the SAM parameters * Modified from EepGet.
* All keys, major, and minor are mapped to upper case.
* Double quotes around values are stripped.
* *
* @throws SAMException if the data was formatted incorrectly * Possible input:
* @return Properties with the parsed SAM params, never null *<pre>
* COMMAND
* COMMAND OPCODE
* COMMAND OPCODE [key=val]...
* COMMAND OPCODE [key=" val with spaces "]...
* PING
* PONG
* PING any thing goes
* PONG any thing goes
*
* No escaping of '"' or anything else is allowed or defined
* No spaces before or after '=' allowed
* Keys may not be quoted
* COMMAND and OPCODE may not have '='
* Duplicate keys not allowed
*</pre>
*
* A key without a value is not allowed by the spec, but is
* returned with the value "true".
*
* COMMAND is returned as the value of the key ""COMMAND"".
* OPCODE, or the remainder of the PING/PONG line if any, is returned as the value of the key ""OPCODE"".
*
* @param args non-null
* @throws SAMException on some errors but not all
* @return non-null, may be empty. Does not throw on missing COMMAND or OPCODE; caller must check.
*/ */
public static Properties parseParams(StringTokenizer tok) throws SAMException { public static Properties parseParams(String args) throws SAMException {
int ntoks = tok.countTokens(); final Properties rv = new Properties();
Properties props = new Properties(); final StringBuilder buf = new StringBuilder(32);
final int length = args.length();
StringBuilder value = new StringBuilder(); boolean isQuoted = false;
for (int i = 0; i < ntoks; ++i) { String key = null;
String token = tok.nextToken(); // We go one past the end to force a fake trailing space
// to make things easier, so we don't need cleanup at the end
for (int i = 0; i <= length; i++) {
char c = (i < length) ? args.charAt(i) : ' ';
switch (c) {
case '"':
if (isQuoted) {
// keys never quoted
if (key != null) {
if (rv.setProperty(key, buf.length() > 0 ? buf.toString() : "true") != null)
throw new SAMException("Duplicate parameter " + key);
key = null;
}
buf.setLength(0);
}
isQuoted = !isQuoted;
break;
int pos = token.indexOf("="); case '\r':
if (pos <= 0) { case '\n':
//_log.debug("Error in params format"); break;
if (pos == 0) {
throw new SAMException("No param specified [" + token + "]");
} else {
throw new SAMException("Bad formatting for param [" + token + "]");
}
}
String param = token.substring(0, pos);
value.append(token.substring(pos+1));
if (value.length() == 0)
throw new SAMException("Empty value for param " + param);
// FIXME: The following code does not take into account that there
// may have been multiple subsequent space chars in the input that
// StringTokenizer treates as one.
if (value.charAt(0) == '"') {
while ( (i < ntoks) && (value.lastIndexOf("\"") <= 0) ) {
value.append(' ').append(tok.nextToken());
i++;
}
}
props.setProperty(param, value.toString()); case ' ':
value.setLength(0); case '\b':
case '\f':
case '\t':
// whitespace - if we're in a quoted section, keep this as part of the quote,
// otherwise use it as a delim
if (isQuoted) {
buf.append(c);
} else {
if (key != null) {
if (rv.setProperty(key, buf.length() > 0 ? buf.toString() : "true") != null)
throw new SAMException("Duplicate parameter " + key);
key = null;
} else if (buf.length() > 0) {
// key without value
String k = buf.toString().trim().toUpperCase(Locale.US);
if (rv.isEmpty()) {
rv.setProperty(COMMAND, k);
if (k.equals("PING") || k.equals("PONG")) {
// eat the rest of the line
if (i + 1 < args.length()) {
String pingData = args.substring(i + 1);
rv.setProperty(OPCODE, pingData);
}
// this will force an end of the loop
i = length + 1;
}
} else if (rv.size() == 1) {
rv.setProperty(OPCODE, k);
} else {
if (rv.setProperty(k, "true") != null)
throw new SAMException("Duplicate parameter " + k);
}
}
buf.setLength(0);
}
break;
case '=':
if (isQuoted) {
buf.append(c);
} else {
if (buf.length() == 0)
throw new SAMException("Empty parameter name");
key = buf.toString().toUpperCase(Locale.US);
buf.setLength(0);
}
break;
default:
buf.append(c);
break;
}
} }
// nothing needed here, as we forced a trailing space in the loop
//if (_log.shouldLog(Log.DEBUG)) { // unterminated quoted content will be lost
// _log.debug("Parsed properties: " + dumpProperties(props)); if (isQuoted)
//} throw new SAMException("Unterminated quote");
return rv;
return props;
} }
/* Dump a Properties object in an human-readable form */
/****
private static String dumpProperties(Properties props) {
StringBuilder builder = new StringBuilder();
String key, val;
boolean firstIter = true;
for (Map.Entry<Object, Object> entry : props.entrySet()) {
key = (String) entry.getKey();
val = (String) entry.getValue();
if (!firstIter) {
builder.append(";");
} else {
firstIter = false;
}
builder.append(" \"" + key + "\" -> \"" + val + "\"");
}
return builder.toString();
}
****/
/**** /****
public static void main(String args[]) { public static void main(String args[]) {
try { try {
test("a=b c=d e=\"f g h\""); test("a=b c=d e=\"f g h\"");
test("a=\"b c d\" e=\"f g h\" i=\"j\""); test("a=\"b c d\" e=\"f g h\" i=\"j\"");
test("a=\"b c d\" e=f i=\"j\""); test("a=\"b c d\" e=f i=\"j\"");
if (args.length == 0) {
System.out.println("Usage: CommandParser file || CommandParser text to parse");
return;
}
if (args.length > 1 || !(new java.io.File(args[0])).exists()) {
StringBuilder buf = new StringBuilder(128);
for (int i = 0; i < args.length; i++) {
if (i != 0)
buf.append(' ');
buf.append(args[i]);
}
test(buf.toString());
} else {
java.io.InputStream in = new java.io.FileInputStream(args[0]);
String line;
while ((line = net.i2p.data.DataHelper.readLine(in)) != null) {
try {
test(line);
} catch (Exception e) {
e.printStackTrace();
}
}
}
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
} }
private static void test(String props) throws Exception { private static void test(String props) throws Exception {
StringTokenizer tok = new StringTokenizer(props); System.out.println("Testing: " + props);
Properties p = parseParams(tok); Properties m = parseParams(props);
System.out.println(p); System.out.println("Found " + m.size() + " keys");
for (Map.Entry e : m.entrySet()) {
System.out.println(e.getKey() + "=[" + e.getValue() + ']');
}
System.out.println("-------------");
} }
****/ ****/
} }

View File

@ -18,7 +18,6 @@ import java.net.NoRouteToHostException;
import java.nio.channels.SocketChannel; import java.nio.channels.SocketChannel;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Properties; import java.util.Properties;
import java.util.StringTokenizer;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import net.i2p.I2PException; import net.i2p.I2PException;
@ -98,7 +97,6 @@ class SAMv1Handler extends SAMHandler implements SAMRawReceiver, SAMDatagramRece
String domain = null; String domain = null;
String opcode = null; String opcode = null;
boolean canContinue = false; boolean canContinue = false;
StringTokenizer tok;
Properties props; Properties props;
this.thread.setName("SAMv1Handler " + _id); this.thread.setName("SAMv1Handler " + _id);
@ -132,32 +130,29 @@ class SAMv1Handler extends SAMHandler implements SAMRawReceiver, SAMDatagramRece
_log.info("Connection closed by client (line read : null)"); _log.info("Connection closed by client (line read : null)");
break; break;
} }
msg = msg.trim();
if (_log.shouldLog(Log.DEBUG)) { if (_log.shouldLog(Log.DEBUG)) {
_log.debug("New message received: [" + msg + "]"); _log.debug("New message received: [" + msg + "]");
} }
props = SAMUtils.parseParams(msg);
if(msg.equals("")) { domain = props.getProperty(SAMUtils.COMMAND);
if (domain == null) {
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug("Ignoring newline"); _log.debug("Ignoring newline");
continue; continue;
} }
opcode = props.getProperty(SAMUtils.OPCODE);
tok = new StringTokenizer(msg, " "); if (opcode == null) {
if (tok.countTokens() < 2) {
// This is not a correct message, for sure
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug("Error in message format"); _log.debug("Error in message format");
break; break;
} }
domain = tok.nextToken(); props.remove(SAMUtils.COMMAND);
opcode = tok.nextToken(); props.remove(SAMUtils.OPCODE);
if (_log.shouldLog(Log.DEBUG)) { if (_log.shouldLog(Log.DEBUG)) {
_log.debug("Parsing (domain: \"" + domain _log.debug("Parsing (domain: \"" + domain
+ "\"; opcode: \"" + opcode + "\")"); + "\"; opcode: \"" + opcode + "\")");
} }
props = SAMUtils.parseParams(tok);
if (domain.equals("STREAM")) { if (domain.equals("STREAM")) {
canContinue = execStreamMessage(opcode, props); canContinue = execStreamMessage(opcode, props);

View File

@ -135,6 +135,7 @@ class SAMv3DatagramServer implements Handler {
public void run() { public void run() {
try { try {
String header = DataHelper.readLine(is).trim(); String header = DataHelper.readLine(is).trim();
// we cannot use SAMUtils.parseParams() here
StringTokenizer tok = new StringTokenizer(header, " "); StringTokenizer tok = new StringTokenizer(header, " ");
if (tok.countTokens() < 3) { if (tok.countTokens() < 3) {
// This is not a correct message, for sure // This is not a correct message, for sure

View File

@ -24,7 +24,6 @@ import java.nio.channels.SocketChannel;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Properties; import java.util.Properties;
import java.util.HashMap; import java.util.HashMap;
import java.util.StringTokenizer;
import net.i2p.I2PAppContext; import net.i2p.I2PAppContext;
import net.i2p.I2PException; import net.i2p.I2PException;
@ -262,7 +261,6 @@ class SAMv3Handler extends SAMv1Handler
String domain = null; String domain = null;
String opcode = null; String opcode = null;
boolean canContinue = false; boolean canContinue = false;
StringTokenizer tok;
Properties props; Properties props;
this.thread.setName("SAMv3Handler " + _id); this.thread.setName("SAMv3Handler " + _id);
@ -341,53 +339,46 @@ class SAMv3Handler extends SAMv1Handler
_log.debug("Connection closed by client (line read : null)"); _log.debug("Connection closed by client (line read : null)");
break; break;
} }
msg = line.trim();
if (_log.shouldLog(Log.DEBUG)) { if (_log.shouldLog(Log.DEBUG)) {
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug("New message received: [" + msg + "]"); _log.debug("New message received: [" + msg + "]");
} }
props = SAMUtils.parseParams(line);
if(msg.equals("")) { domain = props.getProperty(SAMUtils.COMMAND);
if (domain == null) {
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug("Ignoring newline"); _log.debug("Ignoring newline");
continue; continue;
} }
opcode = props.getProperty(SAMUtils.OPCODE);
tok = new StringTokenizer(msg, " "); props.remove(SAMUtils.COMMAND);
int count = tok.countTokens(); props.remove(SAMUtils.OPCODE);
if (count <= 0) { if (_log.shouldLog(Log.DEBUG)) {
// This is not a correct message, for sure _log.debug("Parsing (domain: \"" + domain
if (_log.shouldLog(Log.DEBUG)) + "\"; opcode: \"" + opcode + "\")");
_log.debug("Ignoring whitespace");
continue;
} }
domain = tok.nextToken();
// these may not have a second token // these may not have a second token
if (domain.equals("PING")) { if (domain.equals("PING")) {
execPingMessage(tok); execPingMessage(opcode);
continue; continue;
} else if (domain.equals("PONG")) { } else if (domain.equals("PONG")) {
execPongMessage(tok); execPongMessage(opcode);
continue; continue;
} else if (domain.equals("QUIT") || domain.equals("STOP") || } else if (domain.equals("QUIT") || domain.equals("STOP") ||
domain.equals("EXIT")) { domain.equals("EXIT")) {
writeString(domain + " STATUS RESULT=OK MESSAGE=bye\n"); writeString(domain + " STATUS RESULT=OK MESSAGE=bye\n");
break; break;
} }
if (count <= 1) {
if (opcode == null) {
// This is not a correct message, for sure // This is not a correct message, for sure
if (writeString(domain + " STATUS RESULT=I2P_ERROR MESSAGE=\"command not specified\"\n")) if (writeString(domain + " STATUS RESULT=I2P_ERROR MESSAGE=\"command not specified\"\n"))
continue; continue;
else else
break; break;
} }
opcode = tok.nextToken();
if (_log.shouldLog(Log.DEBUG)) {
_log.debug("Parsing (domain: \"" + domain
+ "\"; opcode: \"" + opcode + "\")");
}
props = SAMUtils.parseParams(tok);
if (domain.equals("STREAM")) { if (domain.equals("STREAM")) {
canContinue = execStreamMessage(opcode, props); canContinue = execStreamMessage(opcode, props);
@ -909,13 +900,15 @@ class SAMv3Handler extends SAMv1Handler
/** /**
* Handle a PING. * Handle a PING.
* Send a PONG. * Send a PONG.
*
* @param msg to append, may be null
* @since 0.9.24 * @since 0.9.24
*/ */
private void execPingMessage(StringTokenizer tok) { private void execPingMessage(String msg) {
StringBuilder buf = new StringBuilder(); StringBuilder buf = new StringBuilder();
buf.append("PONG"); buf.append("PONG");
while (tok.hasMoreTokens()) { if (msg != null) {
buf.append(' ').append(tok.nextToken()); buf.append(' ').append(msg);
} }
buf.append('\n'); buf.append('\n');
writeString(buf.toString()); writeString(buf.toString());
@ -923,13 +916,12 @@ class SAMv3Handler extends SAMv1Handler
/** /**
* Handle a PONG. * Handle a PONG.
*
* @param s received, may be null
* @since 0.9.24 * @since 0.9.24
*/ */
private void execPongMessage(StringTokenizer tok) { private void execPongMessage(String s) {
String s; if (s == null) {
if (tok.hasMoreTokens()) {
s = tok.nextToken();
} else {
s = ""; s = "";
} }
if (_lastPing > 0) { if (_lastPing > 0) {