diff --git a/apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java b/apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java index 71a617e25f..c484c1b841 100644 --- a/apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java +++ b/apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java @@ -13,7 +13,6 @@ import java.net.Socket; import java.net.SocketTimeoutException; import java.nio.channels.SocketChannel; import java.util.Properties; -import java.util.StringTokenizer; import net.i2p.I2PAppContext; import net.i2p.data.DataHelper; @@ -41,7 +40,7 @@ class SAMHandlerFactory { */ public static SAMHandler createSAMHandler(SocketChannel s, Properties i2cpProps, SAMBridge parent) throws SAMException { - StringTokenizer tok; + String line; Log log = I2PAppContext.getGlobalContext().logManager().getLog(SAMHandlerFactory.class); try { @@ -49,9 +48,8 @@ class SAMHandlerFactory { sock.setKeepAlive(true); StringBuilder buf = new StringBuilder(128); ReadLine.readLine(sock, buf, HELLO_TIMEOUT); - String line = buf.toString(); sock.setSoTimeout(0); - tok = new StringTokenizer(line.trim(), " "); + line = buf.toString(); } catch (SocketTimeoutException e) { throw new SAMException("Timeout waiting for HELLO VERSION", e); } catch (IOException e) { @@ -61,15 +59,13 @@ class SAMHandlerFactory { } // 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"); } - if (!tok.nextToken().equals("HELLO") || - !tok.nextToken().equals("VERSION")) { - throw new SAMException("Must start with HELLO VERSION"); - } - - Properties props = SAMUtils.parseParams(tok); + props.remove(SAMUtils.COMMAND); + props.remove(SAMUtils.OPCODE); String minVer = props.getProperty("MIN"); if (minVer == null) { diff --git a/apps/sam/java/src/net/i2p/sam/SAMUtils.java b/apps/sam/java/src/net/i2p/sam/SAMUtils.java index e1e52f4fc4..723defab62 100644 --- a/apps/sam/java/src/net/i2p/sam/SAMUtils.java +++ b/apps/sam/java/src/net/i2p/sam/SAMUtils.java @@ -11,9 +11,9 @@ package net.i2p.sam; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.OutputStream; +import java.util.Locale; import java.util.Map; import java.util.Properties; -import java.util.StringTokenizer; import net.i2p.I2PAppContext; import net.i2p.I2PException; @@ -159,95 +159,176 @@ class SAMUtils { 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 - * @return Properties with the parsed SAM params, never null + * Possible input: + *
+ * 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 + *+ * + * 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 { - int ntoks = tok.countTokens(); - Properties props = new Properties(); - - StringBuilder value = new StringBuilder(); - for (int i = 0; i < ntoks; ++i) { - String token = tok.nextToken(); + public static Properties parseParams(String args) throws SAMException { + final Properties rv = new Properties(); + final StringBuilder buf = new StringBuilder(32); + final int length = args.length(); + boolean isQuoted = false; + String key = null; + // 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("="); - if (pos <= 0) { - //_log.debug("Error in params format"); - 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++; - } - } + case '\r': + case '\n': + break; - props.setProperty(param, value.toString()); - value.setLength(0); + case ' ': + 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; + } } - - //if (_log.shouldLog(Log.DEBUG)) { - // _log.debug("Parsed properties: " + dumpProperties(props)); - //} - - return props; + // nothing needed here, as we forced a trailing space in the loop + // unterminated quoted content will be lost + if (isQuoted) + throw new SAMException("Unterminated quote"); + return rv; } - /* 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