Changeset 87fa1cb


Ignore:
Timestamp:
Nov 28, 2015 6:28:15 PM (5 years ago)
Author:
zzz <zzz@…>
Branches:
master
Children:
dffd441
Parents:
38c8e01
Message:

SAM: Fix parser to allow spaces in quoted values (tickets #1325, #1488)
Map keys to upper case
Catch some other parse errors

Location:
apps/sam/java/src/net/i2p/sam
Files:
5 edited

Legend:

Unmodified
Added
Removed
  • apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java

    r38c8e01 r87fa1cb  
    1414import java.nio.channels.SocketChannel;
    1515import java.util.Properties;
    16 import java.util.StringTokenizer;
    1716
    1817import net.i2p.I2PAppContext;
     
    4241    public static SAMHandler createSAMHandler(SocketChannel s, Properties i2cpProps,
    4342                                              SAMBridge parent) throws SAMException {
    44         StringTokenizer tok;
     43        String line;
    4544        Log log = I2PAppContext.getGlobalContext().logManager().getLog(SAMHandlerFactory.class);
    4645
     
    5049            StringBuilder buf = new StringBuilder(128);
    5150            ReadLine.readLine(sock, buf, HELLO_TIMEOUT);
    52             String line = buf.toString();
    5351            sock.setSoTimeout(0);
    54             tok = new StringTokenizer(line.trim(), " ");
     52            line = buf.toString();
    5553        } catch (SocketTimeoutException e) {
    5654            throw new SAMException("Timeout waiting for HELLO VERSION", e);
     
    6260
    6361        // Message format: HELLO VERSION [MIN=v1] [MAX=v2]
    64         if (tok.countTokens() < 2) {
     62        Properties props = SAMUtils.parseParams(line);
     63        if (!"HELLO".equals(props.getProperty(SAMUtils.COMMAND)) ||
     64            !"VERSION".equals(props.getProperty(SAMUtils.OPCODE))) {
    6565            throw new SAMException("Must start with HELLO VERSION");
    6666        }
    67         if (!tok.nextToken().equals("HELLO") ||
    68             !tok.nextToken().equals("VERSION")) {
    69             throw new SAMException("Must start with HELLO VERSION");
    70         }
    71 
    72         Properties props = SAMUtils.parseParams(tok);
     67        props.remove(SAMUtils.COMMAND);
     68        props.remove(SAMUtils.OPCODE);
    7369
    7470        String minVer = props.getProperty("MIN");
  • apps/sam/java/src/net/i2p/sam/SAMUtils.java

    r38c8e01 r87fa1cb  
    1212import java.io.IOException;
    1313import java.io.OutputStream;
     14import java.util.Locale;
    1415import java.util.Map;
    1516import java.util.Properties;
    16 import java.util.StringTokenizer;
    1717
    1818import net.i2p.I2PAppContext;
     
    160160    }
    161161
    162     /**
    163      * Parse SAM parameters, and put them into a Propetries object
    164      *
    165      * @param tok A StringTokenizer pointing to the SAM parameters
    166      *
    167      * @throws SAMException if the data was formatted incorrectly
    168      * @return Properties with the parsed SAM params, never null
    169      */
    170     public static Properties parseParams(StringTokenizer tok) throws SAMException {
    171         int ntoks = tok.countTokens();
    172         Properties props = new Properties();
    173        
    174         StringBuilder value = new StringBuilder();
    175         for (int i = 0; i < ntoks; ++i) {
    176             String token = tok.nextToken();
    177 
    178             int pos = token.indexOf("=");
    179             if (pos <= 0) {
    180                 //_log.debug("Error in params format");
    181                 if (pos == 0) {
    182                     throw new SAMException("No param specified [" + token + "]");
    183                 } else {
    184                     throw new SAMException("Bad formatting for param [" + token + "]");
    185                 }
     162    public static final String COMMAND = "\"\"COMMAND\"\"";
     163    public static final String OPCODE = "\"\"OPCODE\"\"";
     164
     165    /**
     166     *  Parse SAM parameters, and put them into a Propetries object
     167     *
     168     *  Modified from EepGet.
     169     *  All keys, major, and minor are mapped to upper case.
     170     *  Double quotes around values are stripped.
     171     *
     172     *  Possible input:
     173     *<pre>
     174     *  COMMAND
     175     *  COMMAND OPCODE
     176     *  COMMAND OPCODE [key=val]...
     177     *  COMMAND OPCODE [key=" val with spaces "]...
     178     *  PING
     179     *  PONG
     180     *  PING any   thing goes
     181     *  PONG any   thing   goes
     182     *
     183     *  No escaping of '"' or anything else is allowed or defined
     184     *  No spaces before or after '=' allowed
     185     *  Keys may not be quoted
     186     *  COMMAND and OPCODE may not have '='
     187     *  Duplicate keys not allowed
     188     *</pre>
     189     *
     190     *  A key without a value is not allowed by the spec, but is
     191     *  returned with the value "true".
     192     *
     193     *  COMMAND is returned as the value of the key ""COMMAND"".
     194     *  OPCODE, or the remainder of the PING/PONG line if any, is returned as the value of the key ""OPCODE"".
     195     *
     196     *  @param args non-null
     197     *  @throws SAMException on some errors but not all
     198     *  @return non-null, may be empty. Does not throw on missing COMMAND or OPCODE; caller must check.
     199     */
     200    public static Properties parseParams(String args) throws SAMException {
     201        final Properties rv = new Properties();
     202        final StringBuilder buf = new StringBuilder(32);
     203        final int length = args.length();
     204        boolean isQuoted = false;
     205        String key = null;
     206        // We go one past the end to force a fake trailing space
     207        // to make things easier, so we don't need cleanup at the end
     208        for (int i = 0; i <= length; i++) {
     209            char c = (i < length) ? args.charAt(i) : ' ';
     210            switch (c) {
     211                case '"':
     212                    if (isQuoted) {
     213                        // keys never quoted
     214                        if (key != null) {
     215                            if (rv.setProperty(key, buf.length() > 0 ? buf.toString() : "true") != null)
     216                                throw new SAMException("Duplicate parameter " + key);
     217                            key = null;
     218                        }
     219                        buf.setLength(0);
     220                    }
     221                    isQuoted = !isQuoted;
     222                    break;
     223
     224                case '\r':
     225                case '\n':
     226                    break;
     227
     228                case ' ':
     229                case '\b':
     230                case '\f':
     231                case '\t':
     232                    // whitespace - if we're in a quoted section, keep this as part of the quote,
     233                    // otherwise use it as a delim
     234                    if (isQuoted) {
     235                        buf.append(c);
     236                    } else {
     237                        if (key != null) {
     238                            if (rv.setProperty(key, buf.length() > 0 ? buf.toString() : "true") != null)
     239                                throw new SAMException("Duplicate parameter " + key);
     240                            key = null;
     241                        } else if (buf.length() > 0) {
     242                            // key without value
     243                            String k = buf.toString().trim().toUpperCase(Locale.US);
     244                            if (rv.isEmpty()) {
     245                                rv.setProperty(COMMAND, k);
     246                                if (k.equals("PING") || k.equals("PONG")) {
     247                                    // eat the rest of the line
     248                                    if (i + 1 < args.length()) {
     249                                        String pingData = args.substring(i + 1);
     250                                        rv.setProperty(OPCODE, pingData);
     251                                    }
     252                                    // this will force an end of the loop
     253                                    i = length + 1;
     254                                }
     255                            } else if (rv.size() == 1) {
     256                                rv.setProperty(OPCODE, k);
     257                            } else {
     258                                if (rv.setProperty(k, "true") != null)
     259                                    throw new SAMException("Duplicate parameter " + k);
     260                            }
     261                        }
     262                        buf.setLength(0);
     263                    }
     264                    break;
     265
     266                case '=':
     267                    if (isQuoted) {
     268                        buf.append(c);
     269                    } else {
     270                        if (buf.length() == 0)
     271                            throw new SAMException("Empty parameter name");
     272                        key = buf.toString().toUpperCase(Locale.US);
     273                        buf.setLength(0);
     274                    }
     275                    break;
     276
     277                default:
     278                    buf.append(c);
     279                    break;
    186280            }
    187            
    188             String param = token.substring(0, pos);
    189             value.append(token.substring(pos+1));
    190             if (value.length() == 0)
    191                 throw new SAMException("Empty value for param " + param);
    192            
    193             // FIXME: The following code does not take into account that there
    194             // may have been multiple subsequent space chars in the input that
    195             // StringTokenizer treates as one.
    196             if (value.charAt(0) == '"') {
    197                 while ( (i < ntoks) && (value.lastIndexOf("\"") <= 0) ) {
    198                     value.append(' ').append(tok.nextToken());
    199                     i++;
    200                 }
    201             }
    202 
    203             props.setProperty(param, value.toString());
    204             value.setLength(0);
    205         }
    206 
    207         //if (_log.shouldLog(Log.DEBUG)) {
    208         //    _log.debug("Parsed properties: " + dumpProperties(props));
    209         //}
    210 
    211         return props;
    212     }
    213 
    214     /* Dump a Properties object in an human-readable form */
    215 /****
    216     private static String dumpProperties(Properties props) {
    217         StringBuilder builder = new StringBuilder();
    218         String key, val;
    219         boolean firstIter = true;
    220        
    221         for (Map.Entry<Object, Object> entry : props.entrySet()) {
    222             key = (String) entry.getKey();
    223             val = (String) entry.getValue();
    224            
    225             if (!firstIter) {
    226                 builder.append(";");
    227             } else {
    228                 firstIter = false;
    229             }
    230             builder.append(" \"" + key + "\" -> \"" + val + "\"");
    231         }
    232        
    233         return builder.toString();
    234     }
    235 ****/
    236    
     281        }
     282        // nothing needed here, as we forced a trailing space in the loop
     283        // unterminated quoted content will be lost
     284        if (isQuoted)
     285            throw new SAMException("Unterminated quote");
     286        return rv;
     287    }
     288
    237289/****
    238290    public static void main(String args[]) {
     
    241293            test("a=\"b c d\" e=\"f g h\" i=\"j\"");
    242294            test("a=\"b c d\" e=f i=\"j\"");
     295            if (args.length == 0) {
     296                System.out.println("Usage: CommandParser file || CommandParser text to parse");
     297                return;
     298            }
     299            if (args.length > 1 || !(new java.io.File(args[0])).exists()) {
     300                StringBuilder buf = new StringBuilder(128);
     301                for (int i = 0; i < args.length; i++) {
     302                    if (i != 0)
     303                        buf.append(' ');
     304                    buf.append(args[i]);
     305                }
     306                test(buf.toString());
     307            } else {
     308                java.io.InputStream in = new java.io.FileInputStream(args[0]);
     309                String line;
     310                while ((line = net.i2p.data.DataHelper.readLine(in)) != null) {
     311                    try {
     312                        test(line);
     313                    } catch (Exception e) {
     314                        e.printStackTrace();
     315                    }
     316                }
     317            }
    243318        } catch (Exception e) {
    244319            e.printStackTrace();
    245320        }
    246321    }
     322
    247323    private static void test(String props) throws Exception {
    248         StringTokenizer tok = new StringTokenizer(props);
    249         Properties p = parseParams(tok);
    250         System.out.println(p);
     324        System.out.println("Testing: " + props);
     325        Properties m = parseParams(props);
     326        System.out.println("Found " + m.size() + " keys");
     327        for (Map.Entry e : m.entrySet()) {
     328            System.out.println(e.getKey() + "=[" + e.getValue() + ']');
     329        }
     330        System.out.println("-------------");
    251331    }
    252332****/
    253333}
     334
  • apps/sam/java/src/net/i2p/sam/SAMv1Handler.java

    r38c8e01 r87fa1cb  
    1919import java.nio.ByteBuffer;
    2020import java.util.Properties;
    21 import java.util.StringTokenizer;
    2221import java.util.concurrent.atomic.AtomicLong;
    2322
     
    9998        String opcode = null;
    10099        boolean canContinue = false;
    101         StringTokenizer tok;
    102100        Properties props;
    103101
     
    133131                    break;
    134132                }
    135                 msg = msg.trim();
    136133
    137134                if (_log.shouldLog(Log.DEBUG)) {
    138135                    _log.debug("New message received: [" + msg + "]");
    139136                }
    140 
    141                 if(msg.equals("")) {
     137                props = SAMUtils.parseParams(msg);
     138                domain = props.getProperty(SAMUtils.COMMAND);
     139                if (domain == null) {
    142140                    if (_log.shouldLog(Log.DEBUG))
    143141                        _log.debug("Ignoring newline");
    144142                    continue;
    145143                }
    146 
    147                 tok = new StringTokenizer(msg, " ");
    148                 if (tok.countTokens() < 2) {
    149                     // This is not a correct message, for sure
     144                opcode = props.getProperty(SAMUtils.OPCODE);
     145                if (opcode == null) {
    150146                    if (_log.shouldLog(Log.DEBUG))
    151147                        _log.debug("Error in message format");
    152148                    break;
    153149                }
    154                 domain = tok.nextToken();
    155                 opcode = tok.nextToken();
     150                props.remove(SAMUtils.COMMAND);
     151                props.remove(SAMUtils.OPCODE);
    156152                if (_log.shouldLog(Log.DEBUG)) {
    157153                    _log.debug("Parsing (domain: \"" + domain
    158154                               + "\"; opcode: \"" + opcode + "\")");
    159155                }
    160                 props = SAMUtils.parseParams(tok);
    161156
    162157                if (domain.equals("STREAM")) {
  • apps/sam/java/src/net/i2p/sam/SAMv3DatagramServer.java

    r38c8e01 r87fa1cb  
    136136                        try {
    137137                                String header = DataHelper.readLine(is).trim();
     138                                // we cannot use SAMUtils.parseParams() here
    138139                                StringTokenizer tok = new StringTokenizer(header, " ");
    139140                                if (tok.countTokens() < 3) {
  • apps/sam/java/src/net/i2p/sam/SAMv3Handler.java

    r38c8e01 r87fa1cb  
    2525import java.util.Properties;
    2626import java.util.HashMap;
    27 import java.util.StringTokenizer;
    2827
    2928import net.i2p.I2PAppContext;
     
    263262                String opcode = null;
    264263                boolean canContinue = false;
    265                 StringTokenizer tok;
    266264                Properties props;
    267265
     
    342340                                        break;
    343341                                }
    344                                 msg = line.trim();
    345342
    346343                                if (_log.shouldLog(Log.DEBUG)) {
     
    348345                                                _log.debug("New message received: [" + msg + "]");
    349346                                }
    350 
    351                                 if(msg.equals("")) {
     347                                props = SAMUtils.parseParams(line);
     348                                domain = props.getProperty(SAMUtils.COMMAND);
     349                                if (domain == null) {
    352350                                        if (_log.shouldLog(Log.DEBUG))
    353351                                                _log.debug("Ignoring newline");
    354352                                        continue;
    355353                                }
    356 
    357                                 tok = new StringTokenizer(msg, " ");
    358                                 int count = tok.countTokens();
    359                                 if (count <= 0) {
    360                                         // This is not a correct message, for sure
    361                                         if (_log.shouldLog(Log.DEBUG))
    362                                                 _log.debug("Ignoring whitespace");
    363                                         continue;
    364                                 }
    365                                 domain = tok.nextToken();
     354                                opcode = props.getProperty(SAMUtils.OPCODE);
     355                                props.remove(SAMUtils.COMMAND);
     356                                props.remove(SAMUtils.OPCODE);
     357                                if (_log.shouldLog(Log.DEBUG)) {
     358                                        _log.debug("Parsing (domain: \"" + domain
     359                                                        + "\"; opcode: \"" + opcode + "\")");
     360                                }
     361
    366362                                // these may not have a second token
    367363                                if (domain.equals("PING")) {
    368                                         execPingMessage(tok);
     364                                        execPingMessage(opcode);
    369365                                        continue;
    370366                                } else if (domain.equals("PONG")) {
    371                                         execPongMessage(tok);
     367                                        execPongMessage(opcode);
    372368                                        continue;
    373369                                } else if (domain.equals("QUIT") || domain.equals("STOP") ||
     
    376372                                        break;
    377373                                }
    378                                 if (count <= 1) {
     374
     375                                if (opcode == null) {
    379376                                        // This is not a correct message, for sure
    380377                                        if (writeString(domain + " STATUS RESULT=I2P_ERROR MESSAGE=\"command not specified\"\n"))
     
    383380                                                break;
    384381                                }
    385                                 opcode = tok.nextToken();
    386                                 if (_log.shouldLog(Log.DEBUG)) {
    387                                         _log.debug("Parsing (domain: \"" + domain
    388                                                         + "\"; opcode: \"" + opcode + "\")");
    389                                 }
    390                                 props = SAMUtils.parseParams(tok);
    391382
    392383                                if (domain.equals("STREAM")) {
     
    910901         * Handle a PING.
    911902         * Send a PONG.
     903         *
     904         * @param msg to append, may be null
    912905         * @since 0.9.24
    913906         */
    914         private void execPingMessage(StringTokenizer tok) {
     907        private void execPingMessage(String msg) {
    915908                StringBuilder buf = new StringBuilder();
    916909                buf.append("PONG");
    917                 while (tok.hasMoreTokens()) {
    918                         buf.append(' ').append(tok.nextToken());
     910                if (msg != null) {
     911                        buf.append(' ').append(msg);
    919912                }
    920913                buf.append('\n');
     
    924917        /**
    925918         * Handle a PONG.
     919         *
     920         * @param s received, may be null
    926921         * @since 0.9.24
    927922         */
    928         private void execPongMessage(StringTokenizer tok) {
    929                 String s;
    930                 if (tok.hasMoreTokens()) {
    931                         s = tok.nextToken();
    932                 } else {
     923        private void execPongMessage(String s) {
     924                if (s == null) {
    933925                        s = "";
    934926                }
Note: See TracChangeset for help on using the changeset viewer.