package net.i2p.sam; /* * free (adj.): unencumbered; not under the control of others * Written by human in 2004 and released into the public domain * with no warranty of any kind, either expressed or implied. * It probably won't make your computer catch on fire, or eat * your children, but it might. Use at your own risk. * */ 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 net.i2p.I2PAppContext; import net.i2p.I2PException; import net.i2p.client.I2PClient; import net.i2p.client.I2PClientFactory; import net.i2p.client.naming.NamingService; import net.i2p.crypto.SigType; import net.i2p.data.Base64; import net.i2p.data.DataFormatException; import net.i2p.data.Destination; import net.i2p.data.PrivateKey; import net.i2p.data.SigningPrivateKey; /** * Miscellaneous utility methods used by SAM protocol handlers. * * @author human */ class SAMUtils { //private final static Log _log = new Log(SAMUtils.class); /** * Generate a random destination key using DSA_SHA1 signature type. * Caller must close streams. Fails silently. * * @param priv Stream used to write the destination and private keys * @param pub Stream used to write the destination (may be null) */ public static void genRandomKey(OutputStream priv, OutputStream pub) { genRandomKey(priv, pub, SigType.DSA_SHA1); } /** * Generate a random destination key. * Caller must close streams. Fails silently. * * @param priv Stream used to write the destination and private keys * @param pub Stream used to write the destination (may be null) * @param sigType what signature type * @since 0.9.14 */ public static void genRandomKey(OutputStream priv, OutputStream pub, SigType sigType) { //_log.debug("Generating random keys..."); try { I2PClient c = I2PClientFactory.createClient(); Destination d = c.createDestination(priv, sigType); priv.flush(); if (pub != null) { d.writeBytes(pub); pub.flush(); } } catch (I2PException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } /** * Check whether a base64-encoded dest is valid * * @param dest The base64-encoded destination to be checked * * @return True if the destination is valid, false otherwise */ /**** public static boolean checkDestination(String dest) { try { Destination d = new Destination(); d.fromBase64(dest); return true; } catch (DataFormatException e) { return false; } } ****/ /** * Check whether a base64-encoded {dest,privkey,signingprivkey} is valid * * @param dest The base64-encoded destination and keys to be checked (same format as PrivateKeyFile) * @return true if valid */ public static boolean checkPrivateDestination(String dest) { byte[] b = Base64.decode(dest); if (b == null || b.length < 663) return false; ByteArrayInputStream destKeyStream = new ByteArrayInputStream(b); try { Destination d = Destination.create(destKeyStream); new PrivateKey().readBytes(destKeyStream); SigningPrivateKey spk = new SigningPrivateKey(d.getSigningPublicKey().getType()); spk.readBytes(destKeyStream); } catch (DataFormatException e) { return false; } catch (IOException e) { return false; } return destKeyStream.available() == 0; } /** * Resolved the specified hostname. * * @param name Hostname to be resolved * * @return the Destination for the specified hostname, or null if not found */ private static Destination lookupHost(String name) { NamingService ns = I2PAppContext.getGlobalContext().namingService(); Destination dest = ns.lookup(name); return dest; } /** * Resolve the destination from a key or a hostname * * @param s Hostname or key to be resolved * * @return the Destination for the specified hostname, non-null * @throws DataFormatException on bad Base 64 or name not found */ public static Destination getDest(String s) throws DataFormatException { // NamingService caches b64 so just use it for everything // TODO: Add a static local cache here so SAM doesn't flush the // NamingService cache Destination d = lookupHost(s); if (d == null) { String msg; if (s.length() >= 516) msg = "Bad Base64 dest: "; else if (s.length() == 60 && s.endsWith(".b32.i2p")) msg = "Lease set not found: "; else msg = "Host name not found: "; throw new DataFormatException(msg + s); } return d; } public static final String COMMAND = "\"\"COMMAND\"\""; public static final String OPCODE = "\"\"OPCODE\"\""; /** * Parse SAM parameters, and put them into a Propetries object * * Modified from EepGet. * COMMAND and OPCODE are mapped to upper case; keys, values, and ping data are not. * Double quotes around values are stripped. * * Possible input: *<pre> * COMMAND * COMMAND OPCODE * COMMAND OPCODE [key=val]... * COMMAND OPCODE [key=" val with spaces "]... * PING * PONG * PING any thing goes * PONG any thing goes * * Escaping is allowed with a backslash, e.g. \" * No spaces before or after '=' allowed * Keys may not be quoted * COMMAND, OPCODE, and keys may not have '=' or whitespace unless escaped * 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(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; case '\r': case '\n': break; 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(); if (rv.isEmpty()) { k = k.toUpperCase(Locale.US); 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.toUpperCase(Locale.US)); } 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 (key != null) { // '=' in a value buf.append(c); } else { if (buf.length() == 0) throw new SAMException("Empty parameter name"); key = buf.toString(); buf.setLength(0); } break; case '\\': if (++i >= length) throw new SAMException("Unterminated escape"); c = args.charAt(i); // fall through... default: buf.append(c); break; } } // 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; } /**** public static void main(String args[]) { try { 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 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) { e.printStackTrace(); } } private static void test(String props) throws Exception { System.out.println("Testing: " + props); Properties m = parseParams(props); System.out.println("Found " + m.size() + " keys"); for (Map.Entry e : m.entrySet()) { System.out.println(e.getKey() + "=[" + e.getValue() + ']'); } System.out.println("-------------"); } ****/ }