package org.klomp.snark; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; import java.util.Locale; import net.i2p.data.Base32; /** * * @since 0.9.4 moved from I2PSnarkServlet */ public class MagnetURI { private final String _tracker; private final String _name; private final byte[] _ih; /** BEP 9 */ public static final String MAGNET = "magnet:"; public static final String MAGNET_FULL = MAGNET + "?xt=urn:btih:"; /** http://sponge.i2p/files/maggotspec.txt */ public static final String MAGGOT = "maggot://"; /** * @param url non-null */ public MagnetURI(I2PSnarkUtil util, String url) throws IllegalArgumentException { String ihash; String name; String trackerURL = null; if (url.startsWith(MAGNET)) { // magnet:?xt=urn:btih:0691e40aae02e552cfcb57af1dca56214680c0c5&tr=http://tracker2.postman.i2p/announce.php String xt = getParam("xt", url); if (xt == null || !xt.startsWith("urn:btih:")) throw new IllegalArgumentException(); ihash = xt.substring("urn:btih:".length()); trackerURL = getTrackerParam(url); name = util.getString("Magnet") + ' ' + ihash; String dn = getParam("dn", url); if (dn != null) name += " (" + dn + ')'; } else if (url.startsWith(MAGGOT)) { // maggot://0691e40aae02e552cfcb57af1dca56214680c0c5:0b557bbdf8718e95d352fbe994dec3a383e2ede7 ihash = url.substring(MAGGOT.length()).trim(); int col = ihash.indexOf(':'); if (col >= 0) ihash = ihash.substring(0, col); name = util.getString("Magnet") + ' ' + ihash; } else { throw new IllegalArgumentException(); } byte[] ih = null; if (ihash.length() == 32) { ih = Base32.decode(ihash); } else if (ihash.length() == 40) { // Like DataHelper.fromHexString() but ensures no loss of leading zero bytes ih = new byte[20]; try { for (int i = 0; i < 20; i++) { ih[i] = (byte) (Integer.parseInt(ihash.substring(i*2, (i*2) + 2), 16) & 0xff); } } catch (NumberFormatException nfe) { ih = null; } } if (ih == null || ih.length != 20) throw new IllegalArgumentException(); _ih = ih; _name = name; _tracker = trackerURL; } /** * @return 20 bytes or null */ public byte[] getInfoHash() { return _ih; } /** * @return pretty name or null, NOT HTML escaped */ public String getName() { return _name; } /** * @return tracker url or null */ public String getTrackerURL() { return _tracker; } /** * @return first decoded parameter or null */ private static String getParam(String key, String uri) { int idx = uri.indexOf('?' + key + '='); if (idx >= 0) { idx += key.length() + 2; } else { idx = uri.indexOf('&' + key + '='); if (idx >= 0) idx += key.length() + 2; } if (idx < 0 || idx > uri.length()) return null; String rv = uri.substring(idx); idx = rv.indexOf('&'); if (idx >= 0) rv = rv.substring(0, idx); else rv = rv.trim(); return decode(rv); } /** * @return all decoded parameters or null * @since 0.9.1 */ private static List<String> getMultiParam(String key, String uri) { int idx = uri.indexOf('?' + key + '='); if (idx >= 0) { idx += key.length() + 2; } else { idx = uri.indexOf('&' + key + '='); if (idx >= 0) idx += key.length() + 2; } if (idx < 0 || idx > uri.length()) return null; List<String> rv = new ArrayList<String>(); while (true) { String p = uri.substring(idx); uri = p; idx = p.indexOf('&'); if (idx >= 0) p = p.substring(0, idx); else p = p.trim(); rv.add(decode(p)); idx = uri.indexOf('&' + key + '='); if (idx < 0) break; idx += key.length() + 2; } return rv; } /** * @return first valid I2P tracker or null * @since 0.9.1 */ private static String getTrackerParam(String uri) { List<String> trackers = getMultiParam("tr", uri); if (trackers == null) return null; for (String t : trackers) { try { URI u = new URI(t); String protocol = u.getScheme(); String host = u.getHost(); if (protocol == null || host == null || !protocol.toLowerCase(Locale.US).equals("http") || !host.toLowerCase(Locale.US).endsWith(".i2p")) continue; return t; } catch(URISyntaxException use) {} } return null; } /** * Decode %xx encoding, convert to UTF-8 if necessary. * Copied from i2ptunnel LocalHTTPServer. * Also converts '+' to ' ' so the dn parameter comes out right * These are coming in via a application/x-www-form-urlencoded form so * the pluses are in there... * hopefully any real + is encoded as %2B. * * @since 0.9.1 */ private static String decode(String s) { if (!(s.contains("%") || s.contains("+"))) return s; StringBuilder buf = new StringBuilder(s.length()); boolean utf8 = false; for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); if (c == '+') { buf.append(' '); } else if (c != '%') { buf.append(c); } else { try { int val = Integer.parseInt(s.substring(++i, (++i) + 1), 16); if ((val & 0x80) != 0) utf8 = true; buf.append((char) val); } catch (IndexOutOfBoundsException ioobe) { break; } catch (NumberFormatException nfe) { break; } } } if (utf8) { try { return new String(buf.toString().getBytes("ISO-8859-1"), "UTF-8"); } catch (UnsupportedEncodingException uee) {} } return buf.toString(); } }