* I2PTunnel standard and IRC clients:

- Allow host:port targets; set defaults in i2ptunnel.config (ticket #1066)
   - Don't fail start if hostname is unresolvable; retry at connect time (ticket #946)
   - Output IRC message on connect fail
   - Update target list on-the-fly when configuration changes
This commit is contained in:
zzz
2013-10-23 20:20:54 +00:00
parent 0f5a0b6b1b
commit 8f7b31aed3
10 changed files with 265 additions and 76 deletions

View File

@@ -4,24 +4,35 @@
package net.i2p.i2ptunnel;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;
import net.i2p.client.streaming.I2PSocket;
import net.i2p.client.streaming.I2PSocketAddress;
import net.i2p.data.Destination;
import net.i2p.util.EventDispatcher;
import net.i2p.util.Log;
public class I2PTunnelClient extends I2PTunnelClientBase {
/** list of Destination objects that we point at */
/**
* list of Destination objects that we point at
* @deprecated why protected? Is anybody using out-of-tree? Protected from the beginning (2004)
*/
protected List<Destination> dests;
/**
* replacement for dests
*/
private final List<I2PSocketAddress> _addrs;
private static final long DEFAULT_READ_TIMEOUT = 5*60*1000; // -1
protected long readTimeout = DEFAULT_READ_TIMEOUT;
/**
* @param destinations comma delimited list of peers we target
* @param destinations peers we target, comma- or space-separated. Since 0.9.9, each dest may be appended with :port
* @throws IllegalArgumentException if the I2PTunnel does not contain
* valid config to contact the router
*/
@@ -32,30 +43,21 @@ public class I2PTunnelClient extends I2PTunnelClientBase {
"Standard client on " + tunnel.listenHost + ':' + localPort,
tunnel, pkf);
_addrs = new ArrayList(1);
if (waitEventValue("openBaseClientResult").equals("error")) {
notifyEvent("openClientResult", "error");
return;
}
StringTokenizer tok = new StringTokenizer(destinations, ", ");
dests = new ArrayList(1);
while (tok.hasMoreTokens()) {
String destination = tok.nextToken();
Destination destN = _context.namingService().lookup(destination);
if (destN == null)
l.log("Could not resolve " + destination);
else
dests.add(destN);
}
buildAddresses(destinations);
if (dests.isEmpty()) {
if (_addrs.isEmpty()) {
l.log("No valid target destinations found");
notifyEvent("openClientResult", "error");
// Nothing is listening for the above event, so it's useless
// Maybe figure out where to put a waitEventValue("openClientResult") ??
// In the meantime, let's do this the easy way
// Note that b32 dests will often not be resolvable at instantiation time;
// a delayed resolution system would be even better.
// Don't close() here, because it does a removeSession() and then
// TunnelController can't acquire() it to release() it.
@@ -72,14 +74,53 @@ public class I2PTunnelClient extends I2PTunnelClientBase {
notifyEvent("openClientResult", "ok");
}
/** @since 0.9.9 moved from constructor */
private void buildAddresses(String destinations) {
if (destinations == null)
return;
StringTokenizer tok = new StringTokenizer(destinations, ", ");
synchronized(_addrs) {
_addrs.clear();
while (tok.hasMoreTokens()) {
String destination = tok.nextToken();
try {
// Try to resolve here but only log if it doesn't.
// Note that b32 _addrs will often not be resolvable at instantiation time.
// We will try again to resolve in clientConnectionRun()
I2PSocketAddress addr = new I2PSocketAddress(destination);
_addrs.add(addr);
if (addr.isUnresolved()) {
String name = addr.getHostName();
if (name.length() == 60 && name.endsWith(".b32.i2p"))
l.log("Warning - Could not resolve " + name +
", perhaps it is not up, will retry when connecting.");
else
l.log("Warning - Could not resolve " + name +
", you must add it to your address book for it to work.");
} else {
dests.add(addr.getAddress());
}
} catch (IllegalArgumentException iae) {
l.log("Bad destination " + destination + " - " + iae);
}
}
}
}
public void setReadTimeout(long ms) { readTimeout = ms; }
public long getReadTimeout() { return readTimeout; }
protected void clientConnectionRun(Socket s) {
Destination destN = pickDestination();
I2PSocket i2ps = null;
try {
i2ps = createI2PSocket(destN);
I2PSocketAddress addr = pickDestination();
if (addr == null)
throw new UnknownHostException("No valid destination configured");
Destination clientDest = addr.getAddress();
if (clientDest == null)
throw new UnknownHostException("Could not resolve " + addr.getHostName());
int port = addr.getPort();
i2ps = createI2PSocket(clientDest, port);
i2ps.setReadTimeout(readTimeout);
new I2PTunnelRunner(s, i2ps, sockLock, null, mySockets);
} catch (Exception ex) {
@@ -95,16 +136,34 @@ public class I2PTunnelClient extends I2PTunnelClientBase {
}
}
private final Destination pickDestination() {
int size = dests.size();
if (size <= 0) {
if (_log.shouldLog(Log.ERROR))
_log.error("No client targets?!");
return null;
private final I2PSocketAddress pickDestination() {
synchronized(_addrs) {
int size = _addrs.size();
if (size <= 0) {
if (_log.shouldLog(Log.ERROR))
_log.error("No client targets?!");
return null;
}
if (size == 1) // skip the rand in the most common case
return _addrs.get(0);
int index = _context.random().nextInt(size);
return _addrs.get(index);
}
if (size == 1) // skip the rand in the most common case
return dests.get(0);
int index = _context.random().nextInt(size);
return dests.get(index);
}
/**
* Update the dests then call super.
*
* @since 0.9.9
*/
@Override
public void optionsUpdated(I2PTunnel tunnel) {
if (getTunnel() != tunnel)
return;
Properties props = tunnel.getClientOptions();
// see TunnelController.setSessionOptions()
String targets = props.getProperty("targetDestination");
buildAddresses(targets);
super.optionsUpdated(tunnel);
}
}

View File

@@ -521,8 +521,25 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
* @return a new I2PSocket
*/
public I2PSocket createI2PSocket(Destination dest) throws I2PException, ConnectException, NoRouteToHostException, InterruptedIOException {
return createI2PSocket(dest, 0);
}
/**
* Create a new I2PSocket towards to the specified destination,
* adding it to the list of connections actually managed by this
* tunnel.
*
* @param dest The destination to connect to
* @param port The destination port to connect to 0 - 65535
* @return a new I2PSocket
* @since 0.9.9
*/
public I2PSocket createI2PSocket(Destination dest, int port)
throws I2PException, ConnectException, NoRouteToHostException, InterruptedIOException {
verifySocketManager();
return createI2PSocket(dest, getDefaultOptions());
I2PSocketOptions opts = getDefaultOptions();
opts.setPort(port);
return createI2PSocket(dest, opts);
}
/**

View File

@@ -1,13 +1,18 @@
package net.i2p.i2ptunnel;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;
import net.i2p.I2PException;
import net.i2p.client.streaming.I2PSocket;
import net.i2p.client.streaming.I2PSocketAddress;
import net.i2p.data.Base32;
import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
import net.i2p.i2ptunnel.irc.DCCClientManager;
import net.i2p.i2ptunnel.irc.DCCHelper;
@@ -28,7 +33,7 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase {
private static volatile long __clientId = 0;
/** list of Destination objects that we point at */
protected List<Destination> dests;
private final List<I2PSocketAddress> _addrs;
private static final long DEFAULT_READ_TIMEOUT = 5*60*1000; // -1
protected long readTimeout = DEFAULT_READ_TIMEOUT;
private final boolean _dccEnabled;
@@ -41,6 +46,7 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase {
public static final String PROP_DCC = "i2ptunnel.ircclient.enableDCC";
/**
* @param destinations peers we target, comma- or space-separated. Since 0.9.9, each dest may be appended with :port
* @throws IllegalArgumentException if the I2PTunnel does not contain
* valid config to contact the router
*/
@@ -57,25 +63,15 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase {
notifyThis,
"IRC Client on " + tunnel.listenHost + ':' + localPort + " #" + (++__clientId), tunnel, pkf);
StringTokenizer tok = new StringTokenizer(destinations, ", ");
dests = new ArrayList(2);
while (tok.hasMoreTokens()) {
String destination = tok.nextToken();
Destination destN = _context.namingService().lookup(destination);
if (destN == null)
l.log("Could not resolve " + destination);
else
dests.add(destN);
}
_addrs = new ArrayList(4);
buildAddresses(destinations);
if (dests.isEmpty()) {
if (_addrs.isEmpty()) {
l.log("No target destinations found");
notifyEvent("openClientResult", "error");
// Nothing is listening for the above event, so it's useless
// Maybe figure out where to put a waitEventValue("openClientResult") ??
// In the meantime, let's do this the easy way
// Note that b32 dests will often not be resolvable at instantiation time;
// a delayed resolution system would be even better.
// Don't close() here, because it does a removeSession() and then
// TunnelController can't acquire() it to release() it.
@@ -95,14 +91,51 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase {
notifyEvent("openIRCClientResult", "ok");
}
/** @since 0.9.9 moved from constructor */
private void buildAddresses(String destinations) {
if (destinations == null)
return;
StringTokenizer tok = new StringTokenizer(destinations, ", ");
synchronized(_addrs) {
_addrs.clear();
while (tok.hasMoreTokens()) {
String destination = tok.nextToken();
try {
// Try to resolve here but only log if it doesn't.
// Note that b32 _addrs will often not be resolvable at instantiation time.
// We will try again to resolve in clientConnectionRun()
I2PSocketAddress addr = new I2PSocketAddress(destination);
_addrs.add(addr);
if (addr.isUnresolved()) {
String name = addr.getHostName();
if (name.length() == 60 && name.endsWith(".b32.i2p"))
l.log("Warning - Could not resolve " + name +
", perhaps it is not up, will retry when connecting.");
else
l.log("Warning - Could not resolve " + name +
", you must add it to your address book for it to work.");
}
} catch (IllegalArgumentException iae) {
l.log("Bad destination " + destination + " - " + iae);
}
}
}
}
protected void clientConnectionRun(Socket s) {
if (_log.shouldLog(Log.INFO))
_log.info("New connection local addr is: " + s.getLocalAddress() +
" from: " + s.getInetAddress());
Destination clientDest = pickDestination();
I2PSocket i2ps = null;
I2PSocketAddress addr = pickDestination();
try {
i2ps = createI2PSocket(clientDest);
if (addr == null)
throw new UnknownHostException("No valid destination configured");
Destination clientDest = addr.getAddress();
if (clientDest == null)
throw new UnknownHostException("Could not resolve " + addr.getHostName());
int port = addr.getPort();
i2ps = createI2PSocket(clientDest, port);
i2ps.setReadTimeout(readTimeout);
StringBuffer expectedPong = new StringBuffer();
DCCHelper dcc = _dccEnabled ? new DCC(s.getLocalAddress().getAddress()) : null;
@@ -110,21 +143,18 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase {
in.start();
Thread out = new I2PAppThread(new IrcOutboundFilter(s,i2ps, expectedPong, _log, dcc), "IRC Client " + __clientId + " out", true);
out.start();
} catch (I2PException ex) {
if (_log.shouldLog(Log.ERROR))
_log.error("Error connecting", ex);
//l.log("Error connecting: " + ex.getMessage());
closeSocket(s);
if (i2ps != null) {
synchronized (sockLock) {
mySockets.remove(sockLock);
}
}
} catch (Exception ex) {
// generally NoRouteToHostException
if (_log.shouldLog(Log.WARN))
_log.warn("Error connecting", ex);
//l.log("Error connecting: " + ex.getMessage());
try {
// Send a response so the user doesn't just see a disconnect
// and blame his router or the network.
String name = addr != null ? addr.getHostName() : "undefined";
String msg = ":" + name + " 499 you :" + ex + "\r\n";
s.getOutputStream().write(DataHelper.getUTF8(msg));
} catch (IOException ioe) {}
closeSocket(s);
if (i2ps != null) {
synchronized (sockLock) {
@@ -135,17 +165,35 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase {
}
private final Destination pickDestination() {
int size = dests.size();
if (size <= 0) {
if (_log.shouldLog(Log.ERROR))
_log.error("No client targets?!");
return null;
private final I2PSocketAddress pickDestination() {
synchronized(_addrs) {
int size = _addrs.size();
if (size <= 0) {
if (_log.shouldLog(Log.ERROR))
_log.error("No client targets?!");
return null;
}
if (size == 1) // skip the rand in the most common case
return _addrs.get(0);
int index = _context.random().nextInt(size);
return _addrs.get(index);
}
if (size == 1) // skip the rand in the most common case
return dests.get(0);
int index = _context.random().nextInt(size);
return dests.get(index);
}
/**
* Update the dests then call super.
*
* @since 0.9.9
*/
@Override
public void optionsUpdated(I2PTunnel tunnel) {
if (getTunnel() != tunnel)
return;
Properties props = tunnel.getClientOptions();
// see TunnelController.setSessionOptions()
String targets = props.getProperty("targetDestination");
buildAddresses(targets);
super.optionsUpdated(tunnel);
}
@Override

View File

@@ -424,7 +424,14 @@ public class TunnelController implements Logging {
}
private void setSessionOptions() {
_tunnel.setClientOptions(getClientOptionProps());
Properties opts = getClientOptionProps();
// targetDestination does NOT start with "option.", but we still want
// to allow a change on the fly, so we pass it through this way,
// as a "spoofed" option. Since 0.9.9.
String target = getTargetDestination();
if (target != null)
opts.setProperty("targetDestination", target);
_tunnel.setClientOptions(opts);
}
private void setI2CPOptions() {