diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java index 614bc1f7436662ec38db5c08a2008898bf896a67..955d3abd1e82afb47427608c7176579a7a608ea5 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java @@ -14,6 +14,7 @@ import net.i2p.I2PException; import net.i2p.client.I2PClient; import net.i2p.client.I2PClientFactory; import net.i2p.client.I2PSession; +import net.i2p.data.Base32; import net.i2p.data.Destination; import net.i2p.util.I2PThread; import net.i2p.util.Log; @@ -361,6 +362,19 @@ public class TunnelController implements Logging { return null; } + public String getMyDestHashBase32() { + if (_tunnel != null) { + List sessions = _tunnel.getSessions(); + for (int i = 0; i < sessions.size(); i++) { + I2PSession session = (I2PSession)sessions.get(i); + Destination dest = session.getMyDestination(); + if (dest != null) + return Base32.encode(dest.calculateHash().getData()); + } + } + return null; + } + public boolean getIsRunning() { return _running; } public boolean getIsStarting() { return _starting; } diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java index c8d321ea24589c1046b1a5cfa87e90df44f0a834..8c361195add42d262599b83dd731901b95c13538 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java @@ -437,6 +437,19 @@ public class IndexBean { } } + public String getDestHashBase32(int tunnel) { + TunnelController tun = getController(tunnel); + if (tun != null) { + String rv = tun.getMyDestHashBase32(); + if (rv != null) + return rv; + else + return ""; + } else { + return ""; + } + } + /// /// bean props for form submission /// diff --git a/apps/i2ptunnel/jsp/index.jsp b/apps/i2ptunnel/jsp/index.jsp index bcec39c17759788b4fe368322a1bfdf583035d0a..cc68096ad59cbef7d6d22e6e96df10f5854bd40e 100644 --- a/apps/i2ptunnel/jsp/index.jsp +++ b/apps/i2ptunnel/jsp/index.jsp @@ -180,13 +180,23 @@ </div> <div class="targetField rowItem"> <label>Points at:</label> - <span class="text"><%=indexBean.getServerTarget(curServer)%></span> + <span class="text"> + <% + if ("httpserver".equals(indexBean.getInternalType(curServer))) { + %> + <a href="http://<%=indexBean.getServerTarget(curServer)%>/" title="Test HTTP server, bypassing I2P"><%=indexBean.getServerTarget(curServer)%></a> + <% + } else { + %><%=indexBean.getServerTarget(curServer)%> + <% + } + %></span> </div> <div class="previewField rowItem"> <% if ("httpserver".equals(indexBean.getInternalType(curServer)) && indexBean.getTunnelStatus(curServer) == IndexBean.RUNNING) { %><label>Preview:</label> - <a class="control" title="Preview this Tunnel" href="http://<%=(new java.util.Random()).nextLong()%>.i2p/?i2paddresshelper=<%=indexBean.getDestinationBase64(curServer)%>" target="_new">Preview</a> + <a class="control" title="Test HTTP server through I2P" href="http://<%=indexBean.getDestHashBase32(curServer)%>.i2p">Preview</a> <% } else { %><span class="comment">No Preview</span> diff --git a/core/java/src/net/i2p/client/DestReplyMessageHandler.java b/core/java/src/net/i2p/client/DestReplyMessageHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..25699ad02412c2d06550d02cf78623c2e8ff8389 --- /dev/null +++ b/core/java/src/net/i2p/client/DestReplyMessageHandler.java @@ -0,0 +1,25 @@ +package net.i2p.client; + +/* + * Released into the public domain + * with no warranty of any kind, either expressed or implied. + */ + +import net.i2p.I2PAppContext; +import net.i2p.data.i2cp.I2CPMessage; +import net.i2p.data.i2cp.DestReplyMessage; + +/** + * Handle I2CP dest replies from the router + */ +class DestReplyMessageHandler extends HandlerImpl { + public DestReplyMessageHandler(I2PAppContext ctx) { + super(ctx, DestReplyMessage.MESSAGE_TYPE); + } + + public void handleMessage(I2CPMessage message, I2PSessionImpl session) { + _log.debug("Handle message " + message); + DestReplyMessage msg = (DestReplyMessage) message; + ((I2PSimpleSession)session).destReceived(msg.getDestination()); + } +} diff --git a/core/java/src/net/i2p/client/I2PClientMessageHandlerMap.java b/core/java/src/net/i2p/client/I2PClientMessageHandlerMap.java index 50b7955719f244923cbf3a21bf03c7d08423c39d..6f0d950514db09e3187743e67d87b2c8006b5a5e 100644 --- a/core/java/src/net/i2p/client/I2PClientMessageHandlerMap.java +++ b/core/java/src/net/i2p/client/I2PClientMessageHandlerMap.java @@ -16,7 +16,6 @@ import net.i2p.data.i2cp.MessageStatusMessage; import net.i2p.data.i2cp.RequestLeaseSetMessage; import net.i2p.data.i2cp.SessionStatusMessage; import net.i2p.data.i2cp.SetDateMessage; -import net.i2p.util.Log; /** * Contains a map of message handlers that a session will want to use @@ -24,9 +23,11 @@ import net.i2p.util.Log; * @author jrandom */ class I2PClientMessageHandlerMap { - private final static Log _log = new Log(I2PClientMessageHandlerMap.class); /** map of message type id --> I2CPMessageHandler */ - private I2CPMessageHandler _handlers[]; + protected I2CPMessageHandler _handlers[]; + + /** for extension */ + public I2PClientMessageHandlerMap() {} public I2PClientMessageHandlerMap(I2PAppContext context) { int highest = DisconnectMessage.MESSAGE_TYPE; @@ -49,4 +50,4 @@ class I2PClientMessageHandlerMap { if ( (messageTypeId < 0) || (messageTypeId >= _handlers.length) ) return null; return _handlers[messageTypeId]; } -} \ No newline at end of file +} diff --git a/core/java/src/net/i2p/client/I2PSession.java b/core/java/src/net/i2p/client/I2PSession.java index 9d053ef5d8f88f2c6398525da054d7acffffd25c..627d1775a5f6e7943428e43346c9be5a0ca640ee 100644 --- a/core/java/src/net/i2p/client/I2PSession.java +++ b/core/java/src/net/i2p/client/I2PSession.java @@ -12,6 +12,7 @@ package net.i2p.client; import java.util.Set; import net.i2p.data.Destination; +import net.i2p.data.Hash; import net.i2p.data.PrivateKey; import net.i2p.data.SessionKey; import net.i2p.data.SigningPrivateKey; @@ -126,4 +127,10 @@ public interface I2PSession { * Retrieve the signing SigningPrivateKey associated with the Destination */ public SigningPrivateKey getPrivateKey(); + + /** + * Lookup up a Hash + * + */ + public Destination lookupDest(Hash h) throws I2PSessionException; } diff --git a/core/java/src/net/i2p/client/I2PSessionImpl.java b/core/java/src/net/i2p/client/I2PSessionImpl.java index 6b62513c5b1c651675ff9316c60ec9e32b599855..78f4ba763c4682b38678d9f4016cc60d516718eb 100644 --- a/core/java/src/net/i2p/client/I2PSessionImpl.java +++ b/core/java/src/net/i2p/client/I2PSessionImpl.java @@ -26,6 +26,7 @@ import java.util.Set; import net.i2p.I2PAppContext; import net.i2p.data.DataFormatException; import net.i2p.data.Destination; +import net.i2p.data.Hash; import net.i2p.data.LeaseSet; import net.i2p.data.PrivateKey; import net.i2p.data.SessionKey; @@ -48,7 +49,7 @@ import net.i2p.util.SimpleTimer; * @author jrandom */ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessageEventListener { - private Log _log; + protected Log _log; /** who we are */ private Destination _myDestination; /** private key for decryption */ @@ -63,15 +64,15 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa private LeaseSet _leaseSet; /** hostname of router */ - private String _hostname; + protected String _hostname; /** port num to router */ - private int _portNum; + protected int _portNum; /** socket for comm */ - private Socket _socket; + protected Socket _socket; /** reader that always searches for messages */ - private I2CPMessageReader _reader; + protected I2CPMessageReader _reader; /** where we pipe our messages */ - private OutputStream _out; + protected OutputStream _out; /** who we send events to */ private I2PSessionListener _sessionListener; @@ -90,10 +91,10 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa private Object _leaseSetWait = new Object(); /** whether the session connection has already been closed (or not yet opened) */ - private boolean _closed; + protected boolean _closed; /** whether the session connection is in the process of being closed */ - private boolean _closing; + protected boolean _closing; /** have we received the current date from the router yet? */ private boolean _dateReceived; @@ -106,7 +107,7 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa * reading of other messages (in turn, potentially leading to deadlock) * */ - private AvailabilityNotifier _availabilityNotifier; + protected AvailabilityNotifier _availabilityNotifier; void dateUpdated() { _dateReceived = true; @@ -117,6 +118,9 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa public static final int LISTEN_PORT = 7654; + /** for extension */ + public I2PSessionImpl() {} + /** * Create a new session, reading the Destination, PrivateKey, and SigningPrivateKey * from the destKeyStream, and using the specified options to connect to the router @@ -151,7 +155,7 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa * Parse the config for anything we know about * */ - private void loadConfig(Properties options) { + protected void loadConfig(Properties options) { _options = new Properties(); _options.putAll(filter(options)); _hostname = _options.getProperty(I2PClient.PROP_TCP_HOST, "localhost"); @@ -385,7 +389,7 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa } } - private class AvailabilityNotifier implements Runnable { + protected class AvailabilityNotifier implements Runnable { private List _pendingIds; private List _pendingSizes; private boolean _alive; @@ -566,7 +570,7 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa if (_log.shouldLog(Log.INFO)) _log.info(getPrefix() + "Destroy the session", new Exception("DestroySession()")); _closing = true; // we use this to prevent a race - if (sendDisconnect) { + if (sendDisconnect && _producer != null) { // only null if overridden by I2PSimpleSession try { _producer.disconnect(this); } catch (I2PSessionException ipe) { @@ -659,4 +663,8 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa } protected String getPrefix() { return "[" + (_sessionId == null ? -1 : _sessionId.getSessionId()) + "]: "; } + + public Destination lookupDest(Hash h) throws I2PSessionException { + return null; + } } diff --git a/core/java/src/net/i2p/client/I2PSessionImpl2.java b/core/java/src/net/i2p/client/I2PSessionImpl2.java index bbaf399f4afd20209af4c754dedc79b6aa395edb..81c6ef22f066d670fe7e00530e124d9131e071bd 100644 --- a/core/java/src/net/i2p/client/I2PSessionImpl2.java +++ b/core/java/src/net/i2p/client/I2PSessionImpl2.java @@ -31,7 +31,6 @@ import net.i2p.util.Log; * @author jrandom */ class I2PSessionImpl2 extends I2PSessionImpl { - private Log _log; /** set of MessageState objects, representing all of the messages in the process of being sent */ private Set _sendingStates; @@ -41,6 +40,9 @@ class I2PSessionImpl2 extends I2PSessionImpl { private final static boolean SHOULD_COMPRESS = true; private final static boolean SHOULD_DECOMPRESS = true; + /** for extension */ + public I2PSessionImpl2() {} + /** * Create a new session, reading the Destination, PrivateKey, and SigningPrivateKey * from the destKeyStream, and using the specified options to connect to the router @@ -396,6 +398,8 @@ class I2PSessionImpl2 extends I2PSessionImpl { } private void clearStates() { + if (_sendingStates == null) // only null if overridden by I2PSimpleSession + return; synchronized (_sendingStates) { for (Iterator iter = _sendingStates.iterator(); iter.hasNext();) { MessageState state = (MessageState) iter.next(); diff --git a/core/java/src/net/i2p/client/I2PSimpleClient.java b/core/java/src/net/i2p/client/I2PSimpleClient.java new file mode 100644 index 0000000000000000000000000000000000000000..9ce4b8d6f5dfb26e3867e365211c7ee77eb90132 --- /dev/null +++ b/core/java/src/net/i2p/client/I2PSimpleClient.java @@ -0,0 +1,47 @@ +package net.i2p.client; + +/* + * Released into the public domain + * with no warranty of any kind, either expressed or implied. + */ + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Properties; + +import net.i2p.I2PAppContext; +import net.i2p.I2PException; +import net.i2p.data.Certificate; +import net.i2p.data.Destination; + +/** + * Simple client implementation with no Destination, + * just used to talk to the router. + */ +public class I2PSimpleClient implements I2PClient { + /** Don't do this */ + public Destination createDestination(OutputStream destKeyStream) throws I2PException, IOException { + return null; + } + + /** or this */ + public Destination createDestination(OutputStream destKeyStream, Certificate cert) throws I2PException, IOException { + return null; + } + + /** + * Create a new session (though do not connect it yet) + * + */ + public I2PSession createSession(InputStream destKeyStream, Properties options) throws I2PSessionException { + return createSession(I2PAppContext.getGlobalContext(), options); + } + /** + * Create a new session (though do not connect it yet) + * + */ + public I2PSession createSession(I2PAppContext context, Properties options) throws I2PSessionException { + return new I2PSimpleSession(context, options); + } +} diff --git a/core/java/src/net/i2p/client/I2PSimpleSession.java b/core/java/src/net/i2p/client/I2PSimpleSession.java new file mode 100644 index 0000000000000000000000000000000000000000..fcfafe7672885b09bc532febc0618506c4aa4bfd --- /dev/null +++ b/core/java/src/net/i2p/client/I2PSimpleSession.java @@ -0,0 +1,123 @@ +package net.i2p.client; + +/* + * Released into the public domain + * with no warranty of any kind, either expressed or implied. + */ + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.Properties; +import java.util.Set; + +import net.i2p.I2PAppContext; +import net.i2p.data.DataHelper; +import net.i2p.data.Destination; +import net.i2p.data.Hash; +import net.i2p.data.i2cp.DestLookupMessage; +import net.i2p.data.i2cp.DestReplyMessage; +import net.i2p.data.i2cp.I2CPMessageReader; +import net.i2p.util.I2PThread; +import net.i2p.util.Log; + +/** + * Create a new session for doing naming queries only. Do not create a Destination. + * Don't create a producer. Do not send/receive messages to other Destinations. + * Cannot handle multiple simultaneous queries atm. + * Could be expanded to ask the router other things. + */ +class I2PSimpleSession extends I2PSessionImpl2 { + private boolean _destReceived; + private Object _destReceivedLock; + private Destination _destination; + + /** + * Create a new session for doing naming queries only. Do not create a destination. + * + * @throws I2PSessionException if there is a problem + */ + public I2PSimpleSession(I2PAppContext context, Properties options) throws I2PSessionException { + _context = context; + _log = context.logManager().getLog(I2PSimpleSession.class); + _handlerMap = new SimpleMessageHandlerMap(context); + _closed = true; + _closing = false; + _availabilityNotifier = new AvailabilityNotifier(); + if (options == null) + options = System.getProperties(); + loadConfig(options); + } + + /** + * Connect to the router and establish a session. This call blocks until + * a session is granted. + * + * @throws I2PSessionException if there is a configuration error or the router is + * not reachable + */ + public void connect() throws I2PSessionException { + _closed = false; + _availabilityNotifier.stopNotifying(); + I2PThread notifier = new I2PThread(_availabilityNotifier); + notifier.setName("Simple Notifier"); + notifier.setDaemon(true); + notifier.start(); + + try { + _socket = new Socket(_hostname, _portNum); + _out = _socket.getOutputStream(); + synchronized (_out) { + _out.write(I2PClient.PROTOCOL_BYTE); + } + InputStream in = _socket.getInputStream(); + _reader = new I2CPMessageReader(in, this); + _reader.startReading(); + + } catch (UnknownHostException uhe) { + _closed = true; + throw new I2PSessionException(getPrefix() + "Bad host ", uhe); + } catch (IOException ioe) { + _closed = true; + throw new I2PSessionException(getPrefix() + "Problem connecting to " + _hostname + " on port " + _portNum, ioe); + } + } + + /** called by the message handler */ + void destReceived(Destination d) { + _destReceived = true; + _destination = d; + synchronized (_destReceivedLock) { + _destReceivedLock.notifyAll(); + } + } + + public Destination lookupDest(Hash h) throws I2PSessionException { + if (_closed) + return null; + _destReceivedLock = new Object(); + sendMessage(new DestLookupMessage(h)); + for (int i = 0; i < 10 && !_destReceived; i++) { + try { + synchronized (_destReceivedLock) { + _destReceivedLock.wait(1000); + } + } catch (InterruptedException ie) {} + } + _destReceived = false; + return _destination; + } + + /** + * Only map message handlers that we will use + */ + class SimpleMessageHandlerMap extends I2PClientMessageHandlerMap { + public SimpleMessageHandlerMap(I2PAppContext context) { + int highest = DestReplyMessage.MESSAGE_TYPE; + _handlers = new I2CPMessageHandler[highest+1]; + _handlers[DestReplyMessage.MESSAGE_TYPE] = new DestReplyMessageHandler(context); + } + } +} diff --git a/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java b/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java index d4ee7e3458b0d6d3e57ef23044e57e0b60db085b..4cfb07d40653f8945665e3eb8891a4a27fcdf99e 100644 --- a/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java +++ b/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java @@ -55,6 +55,8 @@ public class HostsTxtNamingService extends NamingService { return rv; } + private static final int BASE32_HASH_LENGTH = 52; // 1 + Hash.HASH_LENGTH * 8 / 5 + @Override public Destination lookup(String hostname) { Destination d = getCache(hostname); @@ -69,6 +71,15 @@ public class HostsTxtNamingService extends NamingService { return d; } + // Try Base32 decoding + if (hostname.length() == BASE32_HASH_LENGTH + 4 && hostname.endsWith(".i2p")) { + d = LookupDest.lookupBase32Hash(_context, hostname.substring(0, BASE32_HASH_LENGTH)); + if (d != null) { + putCache(hostname, d); + return d; + } + } + List filenames = getFilenames(); for (int i = 0; i < filenames.size(); i++) { String hostsfile = (String)filenames.get(i); diff --git a/core/java/src/net/i2p/client/naming/LookupDest.java b/core/java/src/net/i2p/client/naming/LookupDest.java new file mode 100644 index 0000000000000000000000000000000000000000..775ae6bcc1b03be35664f610fa47e074d619aacd --- /dev/null +++ b/core/java/src/net/i2p/client/naming/LookupDest.java @@ -0,0 +1,72 @@ +/* + * Released into the public domain + * with no warranty of any kind, either expressed or implied. + */ +package net.i2p.client.naming; + +import java.util.Properties; + +import net.i2p.I2PAppContext; +import net.i2p.client.I2PSessionException; +import net.i2p.client.I2PClient; +import net.i2p.client.I2PSession; +import net.i2p.client.I2PSimpleClient; +import net.i2p.data.Base32; +import net.i2p.data.Base64; +import net.i2p.data.Destination; +import net.i2p.data.Hash; +import net.i2p.data.LeaseSet; + +/** + * Connect via I2CP and ask the router to look up + * the lease of a hash, convert it to a Destination and return it. + * Obviously this can take a while. + * + * All calls are blocking and return null on failure. + * Timeout is set to 10 seconds in I2PSimpleSession. + */ +class LookupDest { + + protected LookupDest(I2PAppContext context) {} + + static Destination lookupBase32Hash(I2PAppContext ctx, String key) { + byte[] h = Base32.decode(key); + if (h == null) + return null; + return lookupHash(ctx, h); + } + + /* Might be useful but not in the context of urls due to upper/lower case */ + /**** + static Destination lookupBase64Hash(I2PAppContext ctx, String key) { + byte[] h = Base64.decode(key); + if (h == null) + return null; + return lookupHash(ctx, h); + } + ****/ + + static Destination lookupHash(I2PAppContext ctx, byte[] h) { + Hash key = new Hash(h); + Destination rv = null; + try { + I2PClient client = new I2PSimpleClient(); + Properties opts = new Properties(); + String s = ctx.getProperty(I2PClient.PROP_TCP_HOST); + if (s != null) + opts.put(I2PClient.PROP_TCP_HOST, s); + s = ctx.getProperty(I2PClient.PROP_TCP_PORT); + if (s != null) + opts.put(I2PClient.PROP_TCP_PORT, s); + I2PSession session = client.createSession(null, opts); + session.connect(); + rv = session.lookupDest(key); + session.destroySession(); + } catch (I2PSessionException ise) {} + return rv; + } + + public static void main(String args[]) { + System.out.println(lookupBase32Hash(I2PAppContext.getGlobalContext(), args[0])); + } +} diff --git a/core/java/src/net/i2p/data/Base32.java b/core/java/src/net/i2p/data/Base32.java new file mode 100644 index 0000000000000000000000000000000000000000..b2cc2d548306d42693e852fb8cb1ac518075067b --- /dev/null +++ b/core/java/src/net/i2p/data/Base32.java @@ -0,0 +1,245 @@ +package net.i2p.data; + +/* + * Released into the public domain + * with no warranty of any kind, either expressed or implied. + */ + +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import net.i2p.util.Log; + +/** + * Encodes and decodes to and from Base32 notation. + * Ref: RFC 3548 + * + * Don't bother with '=' padding characters on encode or + * accept them on decode (i.e. don't require 5-character groups). + * No whitespace allowed. + * + * Decode accepts upper or lower case. + */ +public class Base32 { + + private final static Log _log = new Log(Base32.class); + + /** The 64 valid Base32 values. */ + private final static char[] ALPHABET = {'a', 'b', 'c', 'd', + 'e', 'f', 'g', 'h', 'i', 'j', + 'k', 'l', 'm', 'n', 'o', 'p', + 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', + '2', '3', '4', '5', '6', '7'}; + + /** + * Translates a Base32 value to either its 5-bit reconstruction value + * or a negative number indicating some other meaning. + * Allow upper or lower case. + **/ + private final static byte[] DECODABET = { + 26, 27, 28, 29, 30, 31, -9, -9, // Numbers two through nine + -9, -9, -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, -9, -9, // Decimal 62 - 64 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, // Letters 'A' through 'M' + 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'N' through 'Z' + -9, -9, -9, -9, -9, -9, // Decimal 91 - 96 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, // Letters 'a' through 'm' + 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'n' through 'z' + -9, -9, -9, -9, -9 // Decimal 123 - 127 + }; + + private final static byte BAD_ENCODING = -9; // Indicates error in encoding + private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding + + /** Defeats instantiation. */ + private Base32() { // nop + } + + public static void main(String[] args) { + if (args.length == 0) { + help(); + return; + } + runApp(args); + } + + private static void runApp(String args[]) { + try { + if ("encodestring".equalsIgnoreCase(args[0])) { + System.out.println(encode(args[1].getBytes())); + return; + } + InputStream in = System.in; + OutputStream out = System.out; + if (args.length >= 3) { + out = new FileOutputStream(args[2]); + } + if (args.length >= 2) { + in = new FileInputStream(args[1]); + } + if ("encode".equalsIgnoreCase(args[0])) { + encode(in, out); + return; + } + if ("decode".equalsIgnoreCase(args[0])) { + decode(in, out); + return; + } + } catch (IOException ioe) { + ioe.printStackTrace(System.err); + } + } + + private static byte[] read(InputStream in) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(4096); + byte buf[] = new byte[4096]; + while (true) { + int read = in.read(buf); + if (read < 0) break; + baos.write(buf, 0, read); + } + return baos.toByteArray(); + } + + private static void encode(InputStream in, OutputStream out) throws IOException { + String encoded = encode(read(in)); + for (int i = 0; i < encoded.length(); i++) + out.write((byte)(encoded.charAt(i) & 0xFF)); + } + + private static void decode(InputStream in, OutputStream out) throws IOException { + byte decoded[] = decode(new String(read(in))); + if (decoded == null) { + System.out.println("FAIL"); + return; + } + out.write(decoded); + } + + private static void help() { + System.out.println("Syntax: Base32 encode <inFile> <outFile>"); + System.out.println("or : Base32 encode <inFile>"); + System.out.println("or : Base32 encodestring <string>"); + System.out.println("or : Base32 encode"); + System.out.println("or : Base32 decode <inFile> <outFile>"); + System.out.println("or : Base32 decode <inFile>"); + System.out.println("or : Base32 decode"); + } + + public static String encode(String source) { + return (source != null ? encode(source.getBytes()) : ""); + } + + public static String encode(byte[] source) { + StringBuffer buf = new StringBuffer((source.length + 7) * 8 / 5); + encodeBytes(source, buf); + return buf.toString(); + } + + private final static byte[] emask = { (byte) 0x1f, + (byte) 0x01, (byte) 0x03, (byte) 0x07, (byte) 0x0f }; + /** + * Encodes a byte array into Base32 notation. + * + * @param source The data to convert + */ + private static void encodeBytes(byte[] source, StringBuffer out) { + int usedbits = 0; + for (int i = 0; i < source.length; ) { + int fivebits; + if (usedbits < 3) { + fivebits = (source[i] >> (3 - usedbits)) & 0x1f; + usedbits += 5; + } else if (usedbits == 3) { + fivebits = source[i++] & 0x1f; + usedbits = 0; + } else { + fivebits = (source[i++] << (usedbits - 3)) & 0x1f; + if (i < source.length) { + usedbits -= 3; + fivebits |= (source[i] >> (8 - usedbits)) & emask[usedbits]; + } + } + out.append(ALPHABET[fivebits]); + } + } + + /** + * Decodes data from Base32 notation and + * returns it as a string. + * + * @param s the string to decode + * @return The data as a string or null on failure + */ + public static String decodeToString(String s) { + byte[] b = decode(s); + if (b == null) + return null; + return new String(b); + } + + public static byte[] decode(String s) { + return decode(s.getBytes()); + } + + private final static byte[] dmask = { (byte) 0xf8, (byte) 0x7c, (byte) 0x3e, (byte) 0x1f, + (byte) 0x0f, (byte) 0x07, (byte) 0x03, (byte) 0x01 }; + /** + * Decodes Base32 content in byte array format and returns + * the decoded byte array. + * + * @param source The Base32 encoded data + * @return decoded data + */ + private static byte[] decode(byte[] source) { + int len58; + if (source.length <= 1) + len58 = source.length; + else + len58 = source.length * 5 / 8; + byte[] outBuff = new byte[len58]; + int outBuffPosn = 0; + + int usedbits = 0; + for (int i = 0; i < source.length; i++) { + int fivebits; + if ((source[i] & 0x80) != 0 || source[i] < '2' || source[i] > 'z') + fivebits = BAD_ENCODING; + else + fivebits = DECODABET[source[i] - '2']; + + if (fivebits >= 0) { + if (usedbits == 0) { + outBuff[outBuffPosn] = (byte) ((fivebits << 3) & 0xf8); + usedbits = 5; + } else if (usedbits < 3) { + outBuff[outBuffPosn] |= (fivebits << (3 - usedbits)) & dmask[usedbits]; + usedbits += 5; + } else if (usedbits == 3) { + outBuff[outBuffPosn++] |= fivebits; + usedbits = 0; + } else { + outBuff[outBuffPosn++] |= (fivebits >> (usedbits - 3)) & dmask[usedbits]; + byte next = (byte) (fivebits << (11 - usedbits)); + if (outBuffPosn < len58) { + outBuff[outBuffPosn] = next; + usedbits -= 3; + } else if (next != 0) { + _log.warn("Extra data at the end: " + next + "(decimal)"); + return null; + } + } + } else { + _log.warn("Bad Base32 input character at " + i + ": " + source[i] + "(decimal)"); + return null; + } + } + return outBuff; + } +} diff --git a/core/java/src/net/i2p/data/i2cp/DestLookupMessage.java b/core/java/src/net/i2p/data/i2cp/DestLookupMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..13135a2a41150016461ea5a726ff9bb15af658e7 --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/DestLookupMessage.java @@ -0,0 +1,76 @@ +package net.i2p.data.i2cp; + +/* + * Released into the public domain + * with no warranty of any kind, either expressed or implied. + */ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.Hash; + +/** + * Request the router look up the dest for a hash + */ +public class DestLookupMessage extends I2CPMessageImpl { + public final static int MESSAGE_TYPE = 34; + private Hash _hash; + + public DestLookupMessage() { + super(); + } + + public DestLookupMessage(Hash h) { + _hash = h; + } + + public Hash getHash() { + return _hash; + } + + protected void doReadMessage(InputStream in, int size) throws I2CPMessageException, IOException { + Hash h = new Hash(); + try { + h.readBytes(in); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Unable to load the hash", dfe); + } + _hash = h; + } + + protected byte[] doWriteMessage() throws I2CPMessageException, IOException { + if (_hash == null) + throw new I2CPMessageException("Unable to write out the message as there is not enough data"); + ByteArrayOutputStream os = new ByteArrayOutputStream(Hash.HASH_LENGTH); + try { + _hash.writeBytes(os); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Error writing out the hash", dfe); + } + return os.toByteArray(); + } + + public int getType() { + return MESSAGE_TYPE; + } + + public boolean equals(Object object) { + if ((object != null) && (object instanceof DestLookupMessage)) { + DestLookupMessage msg = (DestLookupMessage) object; + return DataHelper.eq(getHash(), msg.getHash()); + } + return false; + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[DestLookupMessage: "); + buf.append("\n\tHash: ").append(_hash); + buf.append("]"); + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/data/i2cp/DestReplyMessage.java b/core/java/src/net/i2p/data/i2cp/DestReplyMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..1ed601dc28075bc73bfe273882fde94551f4978b --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/DestReplyMessage.java @@ -0,0 +1,78 @@ +package net.i2p.data.i2cp; + +/* + * Released into the public domain + * with no warranty of any kind, either expressed or implied. + * + */ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.Destination; + +/** + * Response to DestLookupMessage + * + */ +public class DestReplyMessage extends I2CPMessageImpl { + public final static int MESSAGE_TYPE = 35; + private Destination _dest; + + public DestReplyMessage() { + super(); + } + + public DestReplyMessage(Destination d) { + _dest = d; + } + + public Destination getDestination() { + return _dest; + } + + protected void doReadMessage(InputStream in, int size) throws I2CPMessageException, IOException { + try { + Destination d = new Destination(); + d.readBytes(in); + _dest = d; + } catch (DataFormatException dfe) { + _dest = null; // null dest allowed + } + } + + protected byte[] doWriteMessage() throws I2CPMessageException, IOException { + if (_dest == null) + return new byte[0]; // null response allowed + ByteArrayOutputStream os = new ByteArrayOutputStream(_dest.size()); + try { + _dest.writeBytes(os); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Error writing out the dest", dfe); + } + return os.toByteArray(); + } + + public int getType() { + return MESSAGE_TYPE; + } + + public boolean equals(Object object) { + if ((object != null) && (object instanceof DestReplyMessage)) { + DestReplyMessage msg = (DestReplyMessage) object; + return DataHelper.eq(getDestination(), msg.getDestination()); + } + return false; + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[DestReplyMessage: "); + buf.append("\n\tDestination: ").append(_dest); + buf.append("]"); + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java b/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java index 481d26f0e23a6c686e63713f819fa621d6f63cbf..128c312dce4cc1150c4f7f8e54c9c4a50e066fb6 100644 --- a/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java +++ b/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java @@ -81,6 +81,10 @@ public class I2CPMessageHandler { return new GetDateMessage(); case SetDateMessage.MESSAGE_TYPE: return new SetDateMessage(); + case DestLookupMessage.MESSAGE_TYPE: + return new DestLookupMessage(); + case DestReplyMessage.MESSAGE_TYPE: + return new DestReplyMessage(); default: throw new I2CPMessageException("The type " + type + " is an unknown I2CP message"); } @@ -94,4 +98,4 @@ public class I2CPMessageHandler { e.printStackTrace(); } } -} \ No newline at end of file +} diff --git a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java index d75e27fb481dce8b64faec858b2ae0ce87c7f5b3..033e28f2f00b6f8f59acaccddd2332ee5e4d4c20 100644 --- a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java +++ b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java @@ -11,6 +11,7 @@ package net.i2p.router.client; import net.i2p.data.Payload; import net.i2p.data.i2cp.CreateLeaseSetMessage; import net.i2p.data.i2cp.CreateSessionMessage; +import net.i2p.data.i2cp.DestLookupMessage; import net.i2p.data.i2cp.DestroySessionMessage; import net.i2p.data.i2cp.GetDateMessage; import net.i2p.data.i2cp.I2CPMessage; @@ -78,6 +79,9 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi case DestroySessionMessage.MESSAGE_TYPE: handleDestroySession(reader, (DestroySessionMessage)message); break; + case DestLookupMessage.MESSAGE_TYPE: + handleDestLookup(reader, (DestLookupMessage)message); + break; default: if (_log.shouldLog(Log.ERROR)) _log.error("Unhandled I2CP type received: " + message.getType()); @@ -85,13 +89,14 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi } /** - * Handle notifiation that there was an error + * Handle notification that there was an error * */ public void readError(I2CPMessageReader reader, Exception error) { if (_runner.isDead()) return; if (_log.shouldLog(Log.ERROR)) _log.error("Error occurred", error); + // Is this is a little drastic for an unknown message type? _runner.stopRunning(); } @@ -228,6 +233,10 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi _runner.leaseSetCreated(message.getLeaseSet()); } + private void handleDestLookup(I2CPMessageReader reader, DestLookupMessage message) { + _context.jobQueue().addJob(new LookupDestJob(_context, _runner, message.getHash())); + } + // this *should* be mod 65536, but UnsignedInteger is still b0rked. FIXME private final static int MAX_SESSION_ID = 32767; diff --git a/router/java/src/net/i2p/router/client/LookupDestJob.java b/router/java/src/net/i2p/router/client/LookupDestJob.java new file mode 100644 index 0000000000000000000000000000000000000000..68edbcaa06f5a7a85e269121d265f522b1bf62fe --- /dev/null +++ b/router/java/src/net/i2p/router/client/LookupDestJob.java @@ -0,0 +1,54 @@ +/* + * Released into the public domain + * with no warranty of any kind, either expressed or implied. + */ +package net.i2p.router.client; + +import net.i2p.data.Destination; +import net.i2p.data.Hash; +import net.i2p.data.LeaseSet; +import net.i2p.data.i2cp.DestReplyMessage; +import net.i2p.data.i2cp.I2CPMessageException; +import net.i2p.router.JobImpl; +import net.i2p.router.RouterContext; + +/** + * Look up the lease of a hash, to convert it to a Destination for the client + */ +class LookupDestJob extends JobImpl { + private ClientConnectionRunner _runner; + private Hash _hash; + + public LookupDestJob(RouterContext context, ClientConnectionRunner runner, Hash h) { + super(context); + _runner = runner; + _hash = h; + } + + public String getName() { return "LeaseSet Lookup for Client"; } + public void runJob() { + DoneJob done = new DoneJob(getContext()); + getContext().netDb().lookupLeaseSet(_hash, done, done, 10*1000); + } + + private class DoneJob extends JobImpl { + public DoneJob(RouterContext enclosingContext) { + super(enclosingContext); + } + public String getName() { return "LeaseSet Lookup Reply to Client"; } + public void runJob() { + LeaseSet ls = getContext().netDb().lookupLeaseSetLocally(_hash); + if (ls != null) + returnDest(ls.getDestination()); + else + returnDest(null); + } + } + + private void returnDest(Destination d) { + DestReplyMessage msg = new DestReplyMessage(d); + try { + _runner.doSend(msg); + } catch (I2CPMessageException ime) {} + } +}