forked from I2P_Developers/i2p.i2p
Compare commits
126 Commits
i2p_0_4_1_
...
i2p_0_4_2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64b5089909 | ||
|
|
e0e09bfa45 | ||
|
|
8c7f9f2c65 | ||
|
|
aff5cea949 | ||
|
|
8bd99f699f | ||
|
|
b0513fff8a | ||
|
|
f10db9d91a | ||
|
|
9b5fb17068 | ||
|
|
608d713dca | ||
|
|
6d5fc8ca21 | ||
|
|
8c3145b70f | ||
|
|
12a6f3e938 | ||
|
|
2c59435762 | ||
|
|
7336bf5c55 | ||
|
|
2b21b97277 | ||
|
|
603bc99a2f | ||
|
|
426ede1c99 | ||
|
|
21506c1d1b | ||
|
|
0b48b18e7e | ||
|
|
c8f6d9c7a1 | ||
|
|
ed8eced9dd | ||
|
|
4a029b7853 | ||
|
|
6bd9e58ece | ||
|
|
cd075fc8a6 | ||
|
|
e733427920 | ||
|
|
107da0ae22 | ||
|
|
3629d7a32c | ||
|
|
71e1152cde | ||
|
|
d01ab7fd23 | ||
|
|
f46d0a720c | ||
|
|
d943b4993a | ||
|
|
4a4f57d6ac | ||
|
|
085da16268 | ||
|
|
306f6b0037 | ||
|
|
3780d290fa | ||
|
|
ad7dc66f90 | ||
|
|
258244fed8 | ||
|
|
5f7982540f | ||
|
|
b1c0de4b77 | ||
|
|
45b3fecfff | ||
|
|
9774ded4dd | ||
|
|
7ec027854e | ||
|
|
b457001b42 | ||
|
|
6fc6866eb4 | ||
|
|
299e5528bc | ||
|
|
881524a5e4 | ||
|
|
ffc405138d | ||
|
|
f6ff74af16 | ||
|
|
73a12d47de | ||
|
|
83165df7e5 | ||
|
|
30074be5a5 | ||
|
|
16715aa309 | ||
|
|
53f3802a81 | ||
|
|
07626b5cc2 | ||
|
|
9ea603caf2 | ||
|
|
18ab9b80d2 | ||
|
|
71c1cb4e12 | ||
|
|
0c049f39d9 | ||
|
|
096b807c37 | ||
|
|
9018af4765 | ||
|
|
323f28e306 | ||
|
|
e9dbd00f42 | ||
|
|
98b5252a2d | ||
|
|
8abf42023a | ||
|
|
b792238f3f | ||
|
|
2486e5e75f | ||
|
|
5f113f1610 | ||
|
|
592e9dc3ff | ||
|
|
314316cee0 | ||
|
|
9cf663063e | ||
|
|
9ea9210a4b | ||
|
|
2bf1a94608 | ||
|
|
7a0236ad29 | ||
|
|
4341a0c198 | ||
|
|
8071612c12 | ||
|
|
ea9cc3da04 | ||
|
|
0df588fffb | ||
|
|
a622311dbc | ||
|
|
1c95ac2470 | ||
|
|
6ef22166f9 | ||
|
|
1107e50108 | ||
|
|
c19355a7b2 | ||
|
|
65d415fade | ||
|
|
b37313d3f9 | ||
|
|
58fcbad20a | ||
|
|
b571f331ec | ||
|
|
2547d4b3e7 | ||
|
|
892786bf0c | ||
|
|
0c51f2b583 | ||
|
|
d5607ca195 | ||
|
|
48cdf17a4f | ||
|
|
669a8fae15 | ||
|
|
d592936873 | ||
|
|
87898dd2f1 | ||
|
|
15c227f568 | ||
|
|
8de41acfe1 | ||
|
|
9680effb9f | ||
|
|
40df846e3f | ||
|
|
eee94fbf84 | ||
|
|
813679ba25 | ||
|
|
2b9e16c9c9 | ||
|
|
f9bb7f7cff | ||
|
|
41e9569094 | ||
|
|
336ee07191 | ||
|
|
81e0a145f1 | ||
|
|
a95a968fa8 | ||
|
|
6c08941d8b | ||
|
|
e13a5b3865 | ||
|
|
9011d5604a | ||
|
|
78aa4ca137 | ||
|
|
93111842df | ||
|
|
88693f8adc | ||
|
|
f904b012e9 | ||
|
|
cebe0a151f | ||
|
|
8fffad0891 | ||
|
|
fb1263dad7 | ||
|
|
28c5d6c10d | ||
|
|
e7a6f6836e | ||
|
|
f8ffe016d1 | ||
|
|
ec322f0966 | ||
|
|
0674709fc6 | ||
|
|
d91ac7ef21 | ||
|
|
2f0c3c7baf | ||
|
|
be68407707 | ||
|
|
f799a25aeb | ||
|
|
8329d045f1 |
@@ -274,9 +274,9 @@ public class PeerData {
|
||||
|
||||
_lostRate.addData(numTimedOut, 0);
|
||||
|
||||
_receiveRate.coallesceStats();
|
||||
_sendRate.coallesceStats();
|
||||
_lostRate.coallesceStats();
|
||||
_receiveRate.coalesceStats();
|
||||
_sendRate.coalesceStats();
|
||||
_lostRate.coalesceStats();
|
||||
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Peer data cleaned up " + numTimedOut + " timed out pings and removed " + numDropped
|
||||
@@ -409,4 +409,4 @@ public class PeerData {
|
||||
_wasPonged = pong;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,7 +284,7 @@ public class I2PTunnel implements Logging, EventDispatcher {
|
||||
l.log("textserver <host> <port> <privkey>");
|
||||
l.log("genkeys <privkeyfile> [<pubkeyfile>]");
|
||||
l.log("gentextkeys");
|
||||
l.log("client <port> <pubkey>|file:<pubkeyfile>");
|
||||
l.log("client <port> <pubkey>[,<pubkey,...]|file:<pubkeyfile>");
|
||||
l.log("httpclient <port>");
|
||||
l.log("lookup <name>");
|
||||
l.log("quit");
|
||||
@@ -449,9 +449,11 @@ public class I2PTunnel implements Logging, EventDispatcher {
|
||||
notifyEvent("clientTaskId", new Integer(-1));
|
||||
}
|
||||
} else {
|
||||
l.log("client <port> <pubkey>|file:<pubkeyfile>");
|
||||
l.log("client <port> <pubkey>[,<pubkey>]|file:<pubkeyfile>");
|
||||
l.log(" creates a client that forwards port to the pubkey.\n"
|
||||
+ " use 0 as port to get a free port assigned.");
|
||||
+ " use 0 as port to get a free port assigned. If you specify\n"
|
||||
+ " a comma delimited list of pubkeys, it will rotate among them\n"
|
||||
+ " randomlyl");
|
||||
notifyEvent("clientTaskId", new Integer(-1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
package net.i2p.i2ptunnel;
|
||||
|
||||
import java.net.Socket;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.client.streaming.I2PSocket;
|
||||
import net.i2p.data.DataFormatException;
|
||||
import net.i2p.data.Destination;
|
||||
@@ -15,15 +19,17 @@ public class I2PTunnelClient extends I2PTunnelClientBase {
|
||||
|
||||
private static final Log _log = new Log(I2PTunnelClient.class);
|
||||
|
||||
protected Destination dest;
|
||||
/** list of Destination objects that we point at */
|
||||
protected List dests;
|
||||
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
|
||||
* @throws IllegalArgumentException if the I2PTunnel does not contain
|
||||
* valid config to contact the router
|
||||
*/
|
||||
public I2PTunnelClient(int localPort, String destination, Logging l,
|
||||
public I2PTunnelClient(int localPort, String destinations, Logging l,
|
||||
boolean ownDest, EventDispatcher notifyThis,
|
||||
I2PTunnel tunnel) throws IllegalArgumentException {
|
||||
super(localPort, ownDest, l, notifyThis, "SynSender", tunnel);
|
||||
@@ -33,19 +39,28 @@ public class I2PTunnelClient extends I2PTunnelClientBase {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
dest = I2PTunnel.destFromName(destination);
|
||||
if (dest == null) {
|
||||
l.log("Could not resolve " + destination + ".");
|
||||
return;
|
||||
StringTokenizer tok = new StringTokenizer(destinations, ",");
|
||||
dests = new ArrayList(1);
|
||||
while (tok.hasMoreTokens()) {
|
||||
String destination = tok.nextToken();
|
||||
try {
|
||||
Destination dest = I2PTunnel.destFromName(destination);
|
||||
if (dest == null)
|
||||
l.log("Could not resolve " + destination);
|
||||
else
|
||||
dests.add(dest);
|
||||
} catch (DataFormatException dfe) {
|
||||
l.log("Bad format parsing \"" + destination + "\"");
|
||||
}
|
||||
} catch (DataFormatException e) {
|
||||
l.log("Bad format in destination \"" + destination + "\".");
|
||||
}
|
||||
|
||||
if (dests.size() <= 0) {
|
||||
l.log("No target destinations found");
|
||||
notifyEvent("openClientResult", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
setName(getLocalPort() + " -> " + destination);
|
||||
setName(getLocalPort() + " -> " + destinations);
|
||||
|
||||
startRunning();
|
||||
|
||||
@@ -56,14 +71,34 @@ public class I2PTunnelClient extends I2PTunnelClientBase {
|
||||
public long getReadTimeout() { return readTimeout; }
|
||||
|
||||
protected void clientConnectionRun(Socket s) {
|
||||
Destination dest = pickDestination();
|
||||
I2PSocket i2ps = null;
|
||||
try {
|
||||
I2PSocket i2ps = createI2PSocket(dest);
|
||||
i2ps = createI2PSocket(dest);
|
||||
i2ps.setReadTimeout(readTimeout);
|
||||
new I2PTunnelRunner(s, i2ps, sockLock, null);
|
||||
new I2PTunnelRunner(s, i2ps, sockLock, null, mySockets);
|
||||
} catch (Exception ex) {
|
||||
_log.info("Error connecting", ex);
|
||||
l.log(ex.getMessage());
|
||||
closeSocket(s);
|
||||
if (i2ps != null) {
|
||||
synchronized (sockLock) {
|
||||
mySockets.remove(sockLock);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final Destination pickDestination() {
|
||||
int size = dests.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 (Destination)dests.get(0);
|
||||
int index = I2PAppContext.getGlobalContext().random().nextInt(size);
|
||||
return (Destination)dests.get(index);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
|
||||
protected long _clientId;
|
||||
protected Object sockLock = new Object(); // Guards sockMgr and mySockets
|
||||
private I2PSocketManager sockMgr;
|
||||
private List mySockets = new ArrayList();
|
||||
protected List mySockets = new ArrayList();
|
||||
|
||||
protected Destination dest = null;
|
||||
private int localPort;
|
||||
@@ -57,6 +57,9 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
|
||||
|
||||
private String handlerName;
|
||||
|
||||
private Object conLock = new Object();
|
||||
private int pendingConnections = 0;
|
||||
|
||||
//public I2PTunnelClientBase(int localPort, boolean ownDest,
|
||||
// Logging l) {
|
||||
// I2PTunnelClientBase(localPort, ownDest, l, (EventDispatcher)null);
|
||||
@@ -96,7 +99,7 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
|
||||
t.start();
|
||||
open = true;
|
||||
synchronized (this) {
|
||||
while (!listenerReady) {
|
||||
while (!listenerReady && open) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException e) {
|
||||
@@ -109,7 +112,7 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
|
||||
l.log("Ready! Port " + getLocalPort());
|
||||
notifyEvent("openBaseClientResult", "ok");
|
||||
} else {
|
||||
l.log("Error!");
|
||||
l.log("Error listening - please see the logs!");
|
||||
notifyEvent("openBaseClientResult", "error");
|
||||
}
|
||||
}
|
||||
@@ -182,8 +185,10 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
|
||||
*
|
||||
*/
|
||||
private I2PSocketOptions getDefaultOptions() {
|
||||
I2PSocketOptions opts = new I2PSocketOptions();
|
||||
opts.setConnectTimeout(DEFAULT_CONNECT_TIMEOUT);
|
||||
Properties defaultOpts = getTunnel().getClientOptions();
|
||||
I2PSocketOptions opts = sockMgr.buildOptions(defaultOpts);
|
||||
if (!defaultOpts.containsKey(I2PSocketOptions.PROP_CONNECT_TIMEOUT))
|
||||
opts.setConnectTimeout(DEFAULT_CONNECT_TIMEOUT);
|
||||
return opts;
|
||||
}
|
||||
|
||||
@@ -227,7 +232,13 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
|
||||
public final void run() {
|
||||
try {
|
||||
InetAddress addr = getListenHost(l);
|
||||
if (addr == null) return;
|
||||
if (addr == null) {
|
||||
open = false;
|
||||
synchronized (this) {
|
||||
notifyAll();
|
||||
}
|
||||
return;
|
||||
}
|
||||
ss = new ServerSocket(localPort, 0, addr);
|
||||
|
||||
// If a free port was requested, find out what we got
|
||||
@@ -258,8 +269,15 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
|
||||
manageConnection(s);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
_log.error("Error listening for connections", ex);
|
||||
_log.error("Error listening for connections on " + localPort, ex);
|
||||
notifyEvent("openBaseClientResult", "error");
|
||||
synchronized (sockLock) {
|
||||
mySockets.clear();
|
||||
}
|
||||
open = false;
|
||||
synchronized (this) {
|
||||
notifyAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,7 +287,16 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
|
||||
* @param s Socket to take care of
|
||||
*/
|
||||
protected void manageConnection(Socket s) {
|
||||
new ClientConnectionRunner(s, handlerName);
|
||||
boolean useBlocking = false;
|
||||
synchronized (conLock) {
|
||||
pendingConnections++;
|
||||
if (pendingConnections > 5)
|
||||
useBlocking = true;
|
||||
}
|
||||
if (useBlocking)
|
||||
clientConnectionRun(s);
|
||||
else
|
||||
new ClientConnectionRunner(s, handlerName);
|
||||
}
|
||||
|
||||
public boolean close(boolean forced) {
|
||||
@@ -326,6 +353,9 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
|
||||
|
||||
public void run() {
|
||||
clientConnectionRun(s);
|
||||
synchronized (conLock) {
|
||||
pendingConnections--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -298,11 +298,17 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable
|
||||
line = "User-Agent: MYOB/6.66 (AN/ON)";
|
||||
} else if (line.startsWith("Referer: ")) {
|
||||
// Shouldn't we be more specific, like accepting in-site referers ?
|
||||
line = "Referer: i2p";
|
||||
//line = "Referer: i2p";
|
||||
line = null;
|
||||
continue; // completely strip the line
|
||||
} else if (line.startsWith("Via: ")) {
|
||||
line = "Via: i2p";
|
||||
//line = "Via: i2p";
|
||||
line = null;
|
||||
continue; // completely strip the line
|
||||
} else if (line.startsWith("From: ")) {
|
||||
line = "From: i2p";
|
||||
//line = "From: i2p";
|
||||
line = null;
|
||||
continue; // completely strip the line
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,7 +356,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable
|
||||
String remoteID;
|
||||
I2PSocket i2ps = createI2PSocket(dest);
|
||||
byte[] data = newRequest.toString().getBytes("ISO-8859-1");
|
||||
I2PTunnelRunner runner = new I2PTunnelRunner(s, i2ps, sockLock, data);
|
||||
I2PTunnelRunner runner = new I2PTunnelRunner(s, i2ps, sockLock, data, mySockets);
|
||||
timeoutThread = new InactivityTimeoutThread(runner, out, targetRequest, usingWWWProxy, currentProxy, s, requestId);
|
||||
timeoutThread.start();
|
||||
} catch (SocketException ex) {
|
||||
|
||||
@@ -11,9 +11,11 @@ import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import net.i2p.client.I2PSession;
|
||||
import net.i2p.client.streaming.I2PSocket;
|
||||
import net.i2p.data.ByteArray;
|
||||
import net.i2p.util.ByteCache;
|
||||
import net.i2p.util.Clock;
|
||||
import net.i2p.util.I2PThread;
|
||||
import net.i2p.util.Log;
|
||||
@@ -38,14 +40,17 @@ public class I2PTunnelRunner extends I2PThread implements I2PSocket.SocketErrorL
|
||||
Object slock, finishLock = new Object();
|
||||
boolean finished = false;
|
||||
HashMap ostreams, sockets;
|
||||
I2PSession session;
|
||||
byte[] initialData;
|
||||
/** when the last data was sent/received (or -1 if never) */
|
||||
private long lastActivityOn;
|
||||
/** when the runner started up */
|
||||
private long startedOn;
|
||||
private List sockList;
|
||||
|
||||
public I2PTunnelRunner(Socket s, I2PSocket i2ps, Object slock, byte[] initialData) {
|
||||
private volatile long __forwarderId;
|
||||
|
||||
public I2PTunnelRunner(Socket s, I2PSocket i2ps, Object slock, byte[] initialData, List sockList) {
|
||||
this.sockList = sockList;
|
||||
this.s = s;
|
||||
this.i2ps = i2ps;
|
||||
this.slock = slock;
|
||||
@@ -55,6 +60,7 @@ public class I2PTunnelRunner extends I2PThread implements I2PSocket.SocketErrorL
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("I2PTunnelRunner started");
|
||||
_runnerId = ++__runnerId;
|
||||
__forwarderId = i2ps.hashCode();
|
||||
setName("I2PTunnelRunner " + _runnerId);
|
||||
start();
|
||||
}
|
||||
@@ -91,20 +97,21 @@ public class I2PTunnelRunner extends I2PThread implements I2PSocket.SocketErrorL
|
||||
}
|
||||
|
||||
public void run() {
|
||||
boolean closedCleanly = false;
|
||||
try {
|
||||
InputStream in = s.getInputStream();
|
||||
OutputStream out = new BufferedOutputStream(s.getOutputStream(), NETWORK_BUFFER_SIZE);
|
||||
OutputStream out = s.getOutputStream(); // = new BufferedOutputStream(s.getOutputStream(), NETWORK_BUFFER_SIZE);
|
||||
i2ps.setSocketErrorListener(this);
|
||||
InputStream i2pin = i2ps.getInputStream();
|
||||
OutputStream i2pout = new BufferedOutputStream(i2ps.getOutputStream(), MAX_PACKET_SIZE);
|
||||
OutputStream i2pout = i2ps.getOutputStream(); //new BufferedOutputStream(i2ps.getOutputStream(), MAX_PACKET_SIZE);
|
||||
if (initialData != null) {
|
||||
synchronized (slock) {
|
||||
i2pout.write(initialData);
|
||||
i2pout.flush();
|
||||
//i2pout.flush();
|
||||
}
|
||||
}
|
||||
Thread t1 = new StreamForwarder(in, i2pout);
|
||||
Thread t2 = new StreamForwarder(i2pin, out);
|
||||
Thread t1 = new StreamForwarder(in, i2pout, "toI2P");
|
||||
Thread t2 = new StreamForwarder(i2pin, out, "fromI2P");
|
||||
synchronized (finishLock) {
|
||||
while (!finished) {
|
||||
finishLock.wait();
|
||||
@@ -112,29 +119,41 @@ public class I2PTunnelRunner extends I2PThread implements I2PSocket.SocketErrorL
|
||||
}
|
||||
// now one connection is dead - kill the other as well.
|
||||
s.close();
|
||||
s = null;
|
||||
i2ps.close();
|
||||
i2ps = null;
|
||||
t1.join();
|
||||
t2.join();
|
||||
closedCleanly = true;
|
||||
} catch (InterruptedException ex) {
|
||||
_log.error("Interrupted", ex);
|
||||
if (_log.shouldLog(Log.ERROR))
|
||||
_log.error("Interrupted", ex);
|
||||
} catch (IOException ex) {
|
||||
ex.printStackTrace();
|
||||
_log.debug("Error forwarding", ex);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Error forwarding", ex);
|
||||
} catch (Exception e) {
|
||||
_log.error("Internal error", e);
|
||||
if (_log.shouldLog(Log.ERROR))
|
||||
_log.error("Internal error", e);
|
||||
} finally {
|
||||
removeRef();
|
||||
try {
|
||||
if (s != null) s.close();
|
||||
if (i2ps != null) i2ps.close();
|
||||
if ( (s != null) && (!closedCleanly) )
|
||||
s.close();
|
||||
} catch (IOException ex) {
|
||||
ex.printStackTrace();
|
||||
_log.error("Could not close socket", ex);
|
||||
if (_log.shouldLog(Log.ERROR))
|
||||
_log.error("Could not close java socket", ex);
|
||||
}
|
||||
try {
|
||||
if (i2ps != null) {
|
||||
if (!closedCleanly)
|
||||
i2ps.close();
|
||||
i2ps.setSocketErrorListener(null);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
if (_log.shouldLog(Log.ERROR))
|
||||
_log.error("Could not close I2PSocket", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void errorOccurred() {
|
||||
synchronized (finishLock) {
|
||||
finished = true;
|
||||
@@ -142,22 +161,43 @@ public class I2PTunnelRunner extends I2PThread implements I2PSocket.SocketErrorL
|
||||
}
|
||||
}
|
||||
|
||||
private volatile long __forwarderId = 0;
|
||||
private void removeRef() {
|
||||
if (sockList != null) {
|
||||
synchronized (slock) {
|
||||
boolean removed = sockList.remove(i2ps);
|
||||
//System.out.println("Removal of i2psocket " + i2ps + " successful? "
|
||||
// + removed + " remaining: " + sockList.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class StreamForwarder extends I2PThread {
|
||||
|
||||
InputStream in;
|
||||
OutputStream out;
|
||||
String direction;
|
||||
private ByteCache _cache;
|
||||
|
||||
private StreamForwarder(InputStream in, OutputStream out) {
|
||||
private StreamForwarder(InputStream in, OutputStream out, String dir) {
|
||||
this.in = in;
|
||||
this.out = out;
|
||||
direction = dir;
|
||||
_cache = ByteCache.getInstance(256, NETWORK_BUFFER_SIZE);
|
||||
setName("StreamForwarder " + _runnerId + "." + (++__forwarderId));
|
||||
start();
|
||||
}
|
||||
|
||||
public void run() {
|
||||
byte[] buffer = new byte[NETWORK_BUFFER_SIZE];
|
||||
String from = i2ps.getThisDestination().calculateHash().toBase64().substring(0,6);
|
||||
String to = i2ps.getPeerDestination().calculateHash().toBase64().substring(0,6);
|
||||
|
||||
if (_log.shouldLog(Log.DEBUG)) {
|
||||
_log.debug(direction + ": Forwarding between "
|
||||
+ from + " and " + to);
|
||||
}
|
||||
|
||||
ByteArray ba = _cache.acquire();
|
||||
byte[] buffer = ba.getData(); // new byte[NETWORK_BUFFER_SIZE];
|
||||
try {
|
||||
int len;
|
||||
while ((len = in.read(buffer)) != -1) {
|
||||
@@ -166,45 +206,60 @@ public class I2PTunnelRunner extends I2PThread implements I2PSocket.SocketErrorL
|
||||
if (len > 0) updateActivity();
|
||||
|
||||
if (in.available() == 0) {
|
||||
//if (_log.shouldLog(Log.DEBUG))
|
||||
// _log.debug("Flushing after sending " + len + " bytes through");
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(direction + ": " + len + " bytes flushed through to "
|
||||
+ i2ps.getPeerDestination().calculateHash().toBase64().substring(0,6));
|
||||
try {
|
||||
Thread.sleep(I2PTunnel.PACKET_DELAY);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
if (in.available() == 0) {
|
||||
out.flush(); // make sure the data get though
|
||||
|
||||
if (in.available() <= 0)
|
||||
out.flush(); // make sure the data get though
|
||||
}
|
||||
}
|
||||
out.flush();
|
||||
} catch (SocketException ex) {
|
||||
// this *will* occur when the other threads closes the socket
|
||||
synchronized (finishLock) {
|
||||
if (!finished) {
|
||||
_log.debug("Socket closed - error reading and writing",
|
||||
ex);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(direction + ": Socket closed - error reading and writing",
|
||||
ex);
|
||||
}
|
||||
}
|
||||
} catch (InterruptedIOException ex) {
|
||||
_log.warn("Closing connection due to timeout (error: \""
|
||||
+ ex.getMessage() + "\")");
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn(direction + ": Closing connection due to timeout (error: \""
|
||||
+ ex.getMessage() + "\")");
|
||||
} catch (IOException ex) {
|
||||
if (!finished)
|
||||
_log.error("Error forwarding", ex);
|
||||
if (!finished) {
|
||||
if (_log.shouldLog(Log.ERROR))
|
||||
_log.error(direction + ": Error forwarding", ex);
|
||||
}
|
||||
//else
|
||||
// _log.warn("You may ignore this", ex);
|
||||
} finally {
|
||||
if (_log.shouldLog(Log.INFO)) {
|
||||
_log.info(direction + ": done forwarding between "
|
||||
+ from + " and " + to);
|
||||
}
|
||||
try {
|
||||
out.close();
|
||||
in.close();
|
||||
} catch (IOException ex) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Error closing streams", ex);
|
||||
_log.warn(direction + ": Error closing streams", ex);
|
||||
}
|
||||
synchronized (finishLock) {
|
||||
finished = true;
|
||||
finishLock.notifyAll();
|
||||
// the main thread will close sockets etc. now
|
||||
}
|
||||
_cache.release(ba);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable {
|
||||
_handleSocket.setReadTimeout(readTimeout);
|
||||
Socket s = new Socket(remoteHost, remotePort);
|
||||
afterSocket = I2PAppContext.getGlobalContext().clock().now();
|
||||
new I2PTunnelRunner(s, _handleSocket, slock, null);
|
||||
new I2PTunnelRunner(s, _handleSocket, slock, null, null);
|
||||
} catch (SocketException ex) {
|
||||
try {
|
||||
_handleSocket.close();
|
||||
|
||||
@@ -31,6 +31,7 @@ public class TunnelController implements Logging {
|
||||
private List _messages;
|
||||
private List _sessions;
|
||||
private boolean _running;
|
||||
private boolean _starting;
|
||||
|
||||
/**
|
||||
* Create a new controller for a tunnel out of the specific config options.
|
||||
@@ -58,8 +59,7 @@ public class TunnelController implements Logging {
|
||||
_running = false;
|
||||
if (createKey && ("server".equals(getType())) )
|
||||
createPrivateKey();
|
||||
if (getStartOnLoad())
|
||||
startTunnel();
|
||||
_starting = getStartOnLoad();
|
||||
}
|
||||
|
||||
private void createPrivateKey() {
|
||||
@@ -104,12 +104,14 @@ public class TunnelController implements Logging {
|
||||
*
|
||||
*/
|
||||
public void startTunnel() {
|
||||
_starting = true;
|
||||
try {
|
||||
doStartTunnel();
|
||||
} catch (Exception e) {
|
||||
_log.error("Error starting up the tunnel", e);
|
||||
log("Error starting up the tunnel - " + e.getMessage());
|
||||
}
|
||||
_starting = false;
|
||||
}
|
||||
private void doStartTunnel() {
|
||||
if (_running) {
|
||||
@@ -299,6 +301,7 @@ public class TunnelController implements Logging {
|
||||
public boolean getStartOnLoad() { return "true".equalsIgnoreCase(_config.getProperty("startOnLoad", "true")); }
|
||||
|
||||
public boolean getIsRunning() { return _running; }
|
||||
public boolean getIsStarting() { return _starting; }
|
||||
|
||||
public void getSummary(StringBuffer buf) {
|
||||
String type = getType();
|
||||
|
||||
@@ -8,6 +8,7 @@ import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
@@ -20,6 +21,8 @@ import java.util.TreeMap;
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.client.I2PSession;
|
||||
import net.i2p.client.I2PSessionException;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.util.I2PThread;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
@@ -43,28 +46,34 @@ public class TunnelControllerGroup {
|
||||
*/
|
||||
private Map _sessions;
|
||||
|
||||
public static TunnelControllerGroup getInstance() { return _instance; }
|
||||
public static TunnelControllerGroup getInstance() {
|
||||
synchronized (TunnelControllerGroup.class) {
|
||||
if (_instance == null)
|
||||
_instance = new TunnelControllerGroup(DEFAULT_CONFIG_FILE);
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
private TunnelControllerGroup(String configFile) {
|
||||
_log = I2PAppContext.getGlobalContext().logManager().getLog(TunnelControllerGroup.class);
|
||||
_instance = this;
|
||||
_controllers = new ArrayList();
|
||||
_controllers = Collections.synchronizedList(new ArrayList());
|
||||
_configFile = configFile;
|
||||
_sessions = new HashMap(4);
|
||||
loadControllers(_configFile);
|
||||
}
|
||||
|
||||
public static void main(String args[]) {
|
||||
if ( (args == null) || (args.length <= 0) ) {
|
||||
new TunnelControllerGroup(DEFAULT_CONFIG_FILE);
|
||||
} else if (args.length == 1) {
|
||||
if (DEFAULT_CONFIG_FILE.equals(args[0]))
|
||||
new TunnelControllerGroup(DEFAULT_CONFIG_FILE);
|
||||
else
|
||||
new TunnelControllerGroup(args[0]);
|
||||
} else {
|
||||
System.err.println("Usage: TunnelControllerGroup [filename]");
|
||||
return;
|
||||
synchronized (TunnelControllerGroup.class) {
|
||||
if (_instance != null) return; // already loaded through the web
|
||||
|
||||
if ( (args == null) || (args.length <= 0) ) {
|
||||
_instance = new TunnelControllerGroup(DEFAULT_CONFIG_FILE);
|
||||
} else if (args.length == 1) {
|
||||
_instance = new TunnelControllerGroup(args[0]);
|
||||
} else {
|
||||
System.err.println("Usage: TunnelControllerGroup [filename]");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,10 +98,24 @@ public class TunnelControllerGroup {
|
||||
_controllers.add(controller);
|
||||
i++;
|
||||
}
|
||||
I2PThread startupThread = new I2PThread(new StartControllers(), "Startup tunnels");
|
||||
startupThread.start();
|
||||
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info(i + " controllers loaded from " + configFile);
|
||||
}
|
||||
|
||||
private class StartControllers implements Runnable {
|
||||
public void run() {
|
||||
for (int i = 0; i < _controllers.size(); i++) {
|
||||
TunnelController controller = (TunnelController)_controllers.get(i);
|
||||
if (controller.getStartOnLoad())
|
||||
controller.startTunnel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void reloadControllers() {
|
||||
unloadControllers();
|
||||
loadControllers(_configFile);
|
||||
@@ -143,7 +166,6 @@ public class TunnelControllerGroup {
|
||||
controller.stopTunnel();
|
||||
msgs.addAll(controller.clearMessages());
|
||||
}
|
||||
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info(_controllers.size() + " controllers stopped");
|
||||
return msgs;
|
||||
@@ -161,7 +183,7 @@ public class TunnelControllerGroup {
|
||||
controller.startTunnel();
|
||||
msgs.addAll(controller.clearMessages());
|
||||
}
|
||||
|
||||
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info(_controllers.size() + " controllers started");
|
||||
return msgs;
|
||||
@@ -259,33 +281,13 @@ public class TunnelControllerGroup {
|
||||
}
|
||||
|
||||
Properties props = new Properties();
|
||||
FileInputStream fis = null;
|
||||
try {
|
||||
fis = new FileInputStream(cfgFile);
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(fis));
|
||||
String line = null;
|
||||
while ( (line = in.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (line.length() <= 0) continue;
|
||||
if (line.startsWith("#") || line.startsWith(";"))
|
||||
continue;
|
||||
int eq = line.indexOf('=');
|
||||
if ( (eq <= 0) || (eq >= line.length() - 1) )
|
||||
continue;
|
||||
String key = line.substring(0, eq);
|
||||
String val = line.substring(eq+1);
|
||||
props.setProperty(key, val);
|
||||
}
|
||||
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("Props loaded with " + props.size() + " lines");
|
||||
DataHelper.loadProps(props, cfgFile);
|
||||
return props;
|
||||
} catch (IOException ioe) {
|
||||
if (_log.shouldLog(Log.ERROR))
|
||||
_log.error("Error reading the controllers from " + configFile, ioe);
|
||||
return null;
|
||||
} finally {
|
||||
if (fis != null) try { fis.close(); } catch (IOException ioe) {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,10 +60,13 @@ class WebEditPageFormGenerator {
|
||||
buf.append("value=\"squid.i2p\" ");
|
||||
buf.append("/><br />\n");
|
||||
|
||||
buf.append("<hr />Note: the following options are shared across all client tunnels and");
|
||||
buf.append(" HTTP proxies<br />\n");
|
||||
|
||||
addOptions(buf, controller);
|
||||
buf.append("<input type=\"submit\" name=\"action\" value=\"Save\">\n");
|
||||
buf.append("<input type=\"submit\" name=\"action\" value=\"Remove\">\n");
|
||||
buf.append(" <i>confirm</i> <input type=\"checkbox\" name=\"removeConfirm\" value=\"true\" />\n");
|
||||
buf.append(" <i>confirm removal:</i> <input type=\"checkbox\" name=\"removeConfirm\" value=\"true\" />\n");
|
||||
buf.append("</form>\n");
|
||||
return buf.toString();
|
||||
}
|
||||
@@ -80,10 +83,13 @@ class WebEditPageFormGenerator {
|
||||
buf.append("value=\"").append(controller.getTargetDestination()).append("\" ");
|
||||
buf.append(" /> (either the hosts.txt name or the full base64 destination)<br />\n");
|
||||
|
||||
buf.append("<hr />Note: the following options are shared across all client tunnels and");
|
||||
buf.append(" HTTP proxies<br />\n");
|
||||
|
||||
addOptions(buf, controller);
|
||||
buf.append("<input type=\"submit\" name=\"action\" value=\"Save\"><br />\n");
|
||||
buf.append("<input type=\"submit\" name=\"action\" value=\"Remove\">\n");
|
||||
buf.append(" <i>confirm</i> <input type=\"checkbox\" name=\"removeConfirm\" value=\"true\" />\n");
|
||||
buf.append(" <i>confirm removal:</i> <input type=\"checkbox\" name=\"removeConfirm\" value=\"true\" />\n");
|
||||
buf.append("</form>\n");
|
||||
return buf.toString();
|
||||
}
|
||||
@@ -118,7 +124,7 @@ class WebEditPageFormGenerator {
|
||||
addOptions(buf, controller);
|
||||
buf.append("<input type=\"submit\" name=\"action\" value=\"Save\">\n");
|
||||
buf.append("<input type=\"submit\" name=\"action\" value=\"Remove\">\n");
|
||||
buf.append(" <i>confirm</i> <input type=\"checkbox\" name=\"removeConfirm\" value=\"true\" />\n");
|
||||
buf.append(" <i>confirm removal:</i> <input type=\"checkbox\" name=\"removeConfirm\" value=\"true\" />\n");
|
||||
buf.append("</form>\n");
|
||||
return buf.toString();
|
||||
}
|
||||
@@ -145,6 +151,13 @@ class WebEditPageFormGenerator {
|
||||
if ( (controller != null) && (controller.getDescription() != null) )
|
||||
buf.append("value=\"").append(controller.getDescription()).append("\" ");
|
||||
buf.append("/><br />\n");
|
||||
|
||||
buf.append("<b>Start automatically?</b> \n");
|
||||
buf.append("<input type=\"checkbox\" name=\"startOnLoad\" value=\"true\" ");
|
||||
if ( (controller != null) && (controller.getStartOnLoad()) )
|
||||
buf.append(" checked=\"true\" />\n<br />\n");
|
||||
else
|
||||
buf.append(" />\n<br />\n");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -194,6 +207,7 @@ class WebEditPageFormGenerator {
|
||||
private static void addOptions(StringBuffer buf, TunnelController controller) {
|
||||
int tunnelDepth = 2;
|
||||
int numTunnels = 2;
|
||||
int connectDelay = 0;
|
||||
Properties opts = getOptions(controller);
|
||||
if (opts != null) {
|
||||
String depth = opts.getProperty("tunnels.depthInbound");
|
||||
@@ -212,6 +226,14 @@ class WebEditPageFormGenerator {
|
||||
numTunnels = 2;
|
||||
}
|
||||
}
|
||||
String delay = opts.getProperty("i2p.streaming.connectDelay");
|
||||
if (delay != null) {
|
||||
try {
|
||||
connectDelay = Integer.parseInt(delay);
|
||||
} catch (NumberFormatException nfe) {
|
||||
connectDelay = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buf.append("<b>Tunnel depth:</b> ");
|
||||
@@ -251,6 +273,13 @@ class WebEditPageFormGenerator {
|
||||
}
|
||||
buf.append("</select><br />\n");
|
||||
|
||||
buf.append("<b>Delay connection briefly? </b> ");
|
||||
buf.append("<input type=\"checkbox\" name=\"connectDelay\" value=\"");
|
||||
buf.append((connectDelay > 0 ? connectDelay : 1000)).append("\" ");
|
||||
if (connectDelay > 0)
|
||||
buf.append("checked=\"true\" ");
|
||||
buf.append("/> (useful for brief request/response connections)<br />\n");
|
||||
|
||||
buf.append("<b>I2CP host:</b> ");
|
||||
buf.append("<input type=\"text\" name=\"clientHost\" size=\"20\" value=\"");
|
||||
if ( (controller != null) && (controller.getI2CPHost() != null) )
|
||||
@@ -275,18 +304,13 @@ class WebEditPageFormGenerator {
|
||||
String val = opts.getProperty(key);
|
||||
if ("tunnels.depthInbound".equals(key)) continue;
|
||||
if ("tunnels.numInbound".equals(key)) continue;
|
||||
if ("i2p.streaming.connectDelay".equals(key)) continue;
|
||||
if (i != 0) buf.append(' ');
|
||||
buf.append(key).append('=').append(val);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
buf.append("\" /><br />\n");
|
||||
buf.append("<b>Start automatically?</b> \n");
|
||||
buf.append("<input type=\"checkbox\" name=\"startOnLoad\" value=\"true\" ");
|
||||
if ( (controller != null) && (controller.getStartOnLoad()) )
|
||||
buf.append(" checked=\"true\" />\n<br />\n");
|
||||
else
|
||||
buf.append(" />\n<br />\n");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,6 +29,7 @@ public class WebEditPageHelper {
|
||||
private String _i2cpPort;
|
||||
private String _tunnelDepth;
|
||||
private String _tunnelCount;
|
||||
private boolean _connectDelay;
|
||||
private String _customOptions;
|
||||
private String _proxyList;
|
||||
private String _port;
|
||||
@@ -164,6 +165,9 @@ public class WebEditPageHelper {
|
||||
public void setStartOnLoad(String moo) {
|
||||
_startOnLoad = true;
|
||||
}
|
||||
public void setConnectDelay(String moo) {
|
||||
_connectDelay = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the form and display any resulting messages
|
||||
@@ -252,6 +256,27 @@ public class WebEditPageHelper {
|
||||
cur.setConfig(config, "");
|
||||
}
|
||||
|
||||
if ("httpclient".equals(cur.getType()) || "client".equals(cur.getType())) {
|
||||
// all clients use the same I2CP session, and as such, use the same
|
||||
// I2CP options
|
||||
List controllers = TunnelControllerGroup.getInstance().getControllers();
|
||||
for (int i = 0; i < controllers.size(); i++) {
|
||||
TunnelController c = (TunnelController)controllers.get(i);
|
||||
if (c == cur) continue;
|
||||
if ("httpclient".equals(c.getType()) || "client".equals(c.getType())) {
|
||||
Properties cOpt = c.getConfig("");
|
||||
if (_tunnelCount != null)
|
||||
cOpt.setProperty("option.tunnels.numInbound", _tunnelCount);
|
||||
if (_tunnelDepth != null)
|
||||
cOpt.setProperty("option.tunnels.depthInbound", _tunnelDepth);
|
||||
if (_connectDelay)
|
||||
cOpt.setProperty("option.i2p.streaming.connectDelay", "1000");
|
||||
else
|
||||
cOpt.setProperty("option.i2p.streaming.connectDelay", "0");
|
||||
c.setConfig(cOpt, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return getMessages(doSave());
|
||||
}
|
||||
@@ -324,6 +349,7 @@ public class WebEditPageHelper {
|
||||
String val = pair.substring(eq+1);
|
||||
if ("tunnels.numInbound".equals(key)) continue;
|
||||
if ("tunnels.depthInbound".equals(key)) continue;
|
||||
if ("i2p.streaming.connectDelay".equals(key)) continue;
|
||||
config.setProperty("option." + key, val);
|
||||
}
|
||||
}
|
||||
@@ -334,6 +360,10 @@ public class WebEditPageHelper {
|
||||
config.setProperty("option.tunnels.numInbound", _tunnelCount);
|
||||
if (_tunnelDepth != null)
|
||||
config.setProperty("option.tunnels.depthInbound", _tunnelDepth);
|
||||
if (_connectDelay)
|
||||
config.setProperty("option.i2p.streaming.connectDelay", "1000");
|
||||
else
|
||||
config.setProperty("option.i2p.streaming.connectDelay", "0");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -68,6 +68,8 @@ public class WebStatusPageHelper {
|
||||
if (controller.getIsRunning()) {
|
||||
buf.append("<i>running</i> ");
|
||||
buf.append("<a href=\"index.jsp?num=").append(num).append("&action=stop\">stop</a> ");
|
||||
} else if (controller.getIsStarting()) {
|
||||
buf.append("<i>startup in progress (please be patient)</i>");
|
||||
} else {
|
||||
buf.append("<i>not running</i> ");
|
||||
buf.append("<a href=\"index.jsp?num=").append(num).append("&action=start\">start</a> ");
|
||||
|
||||
@@ -47,7 +47,7 @@ public class I2PSOCKSTunnel extends I2PTunnelClientBase {
|
||||
SOCKSServer serv = SOCKSServerFactory.createSOCKSServer(s);
|
||||
Socket clientSock = serv.getClientSocket();
|
||||
I2PSocket destSock = serv.getDestinationI2PSocket();
|
||||
new I2PTunnelRunner(clientSock, destSock, sockLock, null);
|
||||
new I2PTunnelRunner(clientSock, destSock, sockLock, null, mySockets);
|
||||
} catch (SOCKSException e) {
|
||||
_log.error("Error from SOCKS connection: " + e.getMessage());
|
||||
closeSocket(s);
|
||||
|
||||
@@ -81,7 +81,7 @@ public abstract class SOCKSServer {
|
||||
if (connHostName.toLowerCase().endsWith(".i2p")) {
|
||||
_log.debug("connecting to " + connHostName + "...");
|
||||
I2PSocketManager sm = I2PSocketManagerFactory.createManager();
|
||||
destSock = sm.connect(I2PTunnel.destFromName(connHostName), new I2PSocketOptions());
|
||||
destSock = sm.connect(I2PTunnel.destFromName(connHostName), null);
|
||||
confirmConnection();
|
||||
_log.debug("connection confirmed - exchanging data...");
|
||||
} else {
|
||||
|
||||
@@ -5,7 +5,7 @@ package net.i2p.client.streaming;
|
||||
* so care should be taken when using in a multithreaded environment.
|
||||
*
|
||||
*/
|
||||
public class ByteCollector {
|
||||
class ByteCollector {
|
||||
byte[] contents;
|
||||
int size;
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ class I2PSocketImpl implements I2PSocket {
|
||||
public static final int MAX_PACKET_SIZE = 1024 * 32;
|
||||
public static final int PACKET_DELAY = 100;
|
||||
|
||||
private I2PSocketManager manager;
|
||||
private I2PSocketManagerImpl manager;
|
||||
private Destination local;
|
||||
private Destination remote;
|
||||
private String localID;
|
||||
@@ -69,7 +69,7 @@ class I2PSocketImpl implements I2PSocket {
|
||||
* @param outgoing did we initiate the connection (true) or did we receive it (false)?
|
||||
* @param localID what is our half of the socket ID?
|
||||
*/
|
||||
public I2PSocketImpl(Destination peer, I2PSocketManager mgr, boolean outgoing, String localID) {
|
||||
public I2PSocketImpl(Destination peer, I2PSocketManagerImpl mgr, boolean outgoing, String localID) {
|
||||
this.outgoing = outgoing;
|
||||
manager = mgr;
|
||||
remote = peer;
|
||||
@@ -157,7 +157,7 @@ class I2PSocketImpl implements I2PSocket {
|
||||
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("TIMING: RemoteID set to "
|
||||
+ I2PSocketManager.getReadableForm(remoteID) + " for "
|
||||
+ I2PSocketManagerImpl.getReadableForm(remoteID) + " for "
|
||||
+ this.hashCode());
|
||||
}
|
||||
return remoteID;
|
||||
@@ -249,9 +249,9 @@ class I2PSocketImpl implements I2PSocket {
|
||||
|
||||
private byte getMask(int add) {
|
||||
if (outgoing)
|
||||
return (byte)(I2PSocketManager.DATA_IN + (byte)add);
|
||||
return (byte)(I2PSocketManagerImpl.DATA_IN + (byte)add);
|
||||
else
|
||||
return (byte)(I2PSocketManager.DATA_OUT + (byte)add);
|
||||
return (byte)(I2PSocketManagerImpl.DATA_OUT + (byte)add);
|
||||
}
|
||||
|
||||
public void setOptions(I2PSocketOptions options) {
|
||||
@@ -614,7 +614,7 @@ class I2PSocketImpl implements I2PSocket {
|
||||
_log.info(getPrefix() + ":" + Thread.currentThread().getName()
|
||||
+ "Sending close packet: (we started? " + outgoing
|
||||
+ ") after reading " + _bytesRead + " and writing " + _bytesWritten);
|
||||
byte[] packet = I2PSocketManager.makePacket(getMask(0x02), remoteID, new byte[0]);
|
||||
byte[] packet = I2PSocketManagerImpl.makePacket(getMask(0x02), remoteID, new byte[0]);
|
||||
boolean sent = manager.getSession().sendMessage(remote, packet);
|
||||
if (!sent) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
@@ -645,7 +645,7 @@ class I2PSocketImpl implements I2PSocket {
|
||||
_log.error(getPrefix() + "NULL REMOTEID");
|
||||
return false;
|
||||
}
|
||||
byte[] packet = I2PSocketManager.makePacket(getMask(0x00), remoteID, data);
|
||||
byte[] packet = I2PSocketManagerImpl.makePacket(getMask(0x00), remoteID, data);
|
||||
boolean sent;
|
||||
synchronized (flagLock) {
|
||||
if (closed2) return false;
|
||||
|
||||
@@ -13,6 +13,7 @@ import java.net.NoRouteToHostException;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
@@ -34,62 +35,8 @@ import net.i2p.util.Log;
|
||||
* or receive any messages with its .receiveMessage
|
||||
*
|
||||
*/
|
||||
public class I2PSocketManager implements I2PSessionListener {
|
||||
private I2PAppContext _context;
|
||||
private Log _log;
|
||||
private I2PSession _session;
|
||||
private I2PServerSocketImpl _serverSocket = null;
|
||||
private Object lock = new Object(); // for locking socket lists
|
||||
private HashMap _outSockets;
|
||||
private HashMap _inSockets;
|
||||
private I2PSocketOptions _defaultOptions;
|
||||
private long _acceptTimeout;
|
||||
private String _name;
|
||||
private static int __managerId = 0;
|
||||
|
||||
public static final short ACK = 0x51;
|
||||
public static final short CLOSE_OUT = 0x52;
|
||||
public static final short DATA_OUT = 0x50;
|
||||
public static final short SYN = 0xA1;
|
||||
public static final short CLOSE_IN = 0xA2;
|
||||
public static final short DATA_IN = 0xA0;
|
||||
public static final short CHAFF = 0xFF;
|
||||
|
||||
/**
|
||||
* How long to wait for the client app to accept() before sending back CLOSE?
|
||||
* This includes the time waiting in the queue. Currently set to 5 seconds.
|
||||
*/
|
||||
private static final long ACCEPT_TIMEOUT_DEFAULT = 5*1000;
|
||||
|
||||
public I2PSocketManager() {
|
||||
this("SocketManager " + (++__managerId));
|
||||
}
|
||||
public I2PSocketManager(String name) {
|
||||
_name = name;
|
||||
_session = null;
|
||||
_inSockets = new HashMap(16);
|
||||
_outSockets = new HashMap(16);
|
||||
_acceptTimeout = ACCEPT_TIMEOUT_DEFAULT;
|
||||
_context = I2PAppContext.getGlobalContext();
|
||||
_log = _context.logManager().getLog(I2PSocketManager.class);
|
||||
_context.statManager().createRateStat("streaming.lifetime", "How long before the socket is closed?", "streaming", new long[] { 10*60*1000, 60*60*1000, 24*60*60*1000 });
|
||||
_context.statManager().createRateStat("streaming.sent", "How many bytes are sent in the stream?", "streaming", new long[] { 10*60*1000, 60*60*1000, 24*60*60*1000 });
|
||||
_context.statManager().createRateStat("streaming.received", "How many bytes are received in the stream?", "streaming", new long[] { 10*60*1000, 60*60*1000, 24*60*60*1000 });
|
||||
_context.statManager().createRateStat("streaming.transferBalance", "How many streams send more than they receive (positive means more sent, negative means more received)?", "streaming", new long[] { 10*60*1000, 60*60*1000, 24*60*60*1000 });
|
||||
_context.statManager().createRateStat("streaming.synNoAck", "How many times have we sent a SYN but not received an ACK?", "streaming", new long[] { 10*60*1000, 60*60*1000, 24*60*60*1000 });
|
||||
_context.statManager().createRateStat("streaming.ackSendFailed", "How many times have we tried to send an ACK to a SYN and failed?", "streaming", new long[] { 10*60*1000, 60*60*1000, 24*60*60*1000 });
|
||||
_context.statManager().createRateStat("streaming.nackSent", "How many times have we refused a SYN with a NACK?", "streaming", new long[] { 10*60*1000, 60*60*1000, 24*60*60*1000 });
|
||||
_context.statManager().createRateStat("streaming.nackReceived", "How many times have we received a NACK to our SYN?", "streaming", new long[] { 10*60*1000, 60*60*1000, 24*60*60*1000 });
|
||||
}
|
||||
|
||||
public I2PSession getSession() {
|
||||
return _session;
|
||||
}
|
||||
|
||||
public void setSession(I2PSession session) {
|
||||
_session = session;
|
||||
if (session != null) session.setSessionListener(this);
|
||||
}
|
||||
public interface I2PSocketManager {
|
||||
public I2PSession getSession();
|
||||
|
||||
/**
|
||||
* How long should we wait for the client to .accept() a socket before
|
||||
@@ -97,343 +44,14 @@ public class I2PSocketManager implements I2PSessionListener {
|
||||
*
|
||||
* @param ms milliseconds to wait, maximum
|
||||
*/
|
||||
public void setAcceptTimeout(long ms) { _acceptTimeout = ms; }
|
||||
public long getAcceptTimeout() { return _acceptTimeout; }
|
||||
|
||||
public void disconnected(I2PSession session) {
|
||||
_log.info(getName() + ": Disconnected from the session");
|
||||
destroySocketManager();
|
||||
}
|
||||
|
||||
public void errorOccurred(I2PSession session, String message, Throwable error) {
|
||||
_log.error(getName() + ": Error occurred: [" + message + "]", error);
|
||||
}
|
||||
public void setAcceptTimeout(long ms);
|
||||
public long getAcceptTimeout();
|
||||
public void setDefaultOptions(I2PSocketOptions options);
|
||||
public I2PSocketOptions getDefaultOptions();
|
||||
public I2PServerSocket getServerSocket();
|
||||
|
||||
public void messageAvailable(I2PSession session, int msgId, long size) {
|
||||
try {
|
||||
I2PSocketImpl s;
|
||||
byte msg[] = session.receiveMessage(msgId);
|
||||
if (msg.length == 1 && msg[0] == -1) {
|
||||
_log.debug(getName() + ": Ping received");
|
||||
return;
|
||||
}
|
||||
if (msg.length < 4) {
|
||||
_log.warn(getName() + ": ==== packet too short ====");
|
||||
return;
|
||||
}
|
||||
int type = msg[0] & 0xff;
|
||||
String id = toString(new byte[] { msg[1], msg[2], msg[3]});
|
||||
byte[] payload = new byte[msg.length - 4];
|
||||
System.arraycopy(msg, 4, payload, 0, payload.length);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(getName() + ": Message read: type = [" + Integer.toHexString(type)
|
||||
+ "] id = [" + getReadableForm(id)
|
||||
+ "] payload length: [" + payload.length + "]");
|
||||
switch (type) {
|
||||
case ACK:
|
||||
ackAvailable(id, payload);
|
||||
return;
|
||||
case CLOSE_OUT:
|
||||
disconnectAvailable(id, payload);
|
||||
return;
|
||||
case DATA_OUT:
|
||||
sendOutgoingAvailable(id, payload);
|
||||
return;
|
||||
case SYN:
|
||||
synIncomingAvailable(id, payload, session);
|
||||
return;
|
||||
case CLOSE_IN:
|
||||
disconnectIncoming(id, payload);
|
||||
return;
|
||||
case DATA_IN:
|
||||
sendIncoming(id, payload);
|
||||
case CHAFF:
|
||||
// ignore
|
||||
return;
|
||||
default:
|
||||
handleUnknown(type, id, payload);
|
||||
return;
|
||||
}
|
||||
} catch (I2PException ise) {
|
||||
_log.warn(getName() + ": Error processing", ise);
|
||||
} catch (IllegalStateException ise) {
|
||||
_log.debug(getName() + ": Error processing", ise);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We've received an ACK packet (hopefully, in response to a SYN that we
|
||||
* recently sent out). Notify the associated I2PSocket that we now have
|
||||
* the remote stream ID (which should get things going, since the handshake
|
||||
* is complete).
|
||||
*
|
||||
*/
|
||||
private void ackAvailable(String id, byte payload[]) {
|
||||
long begin = _context.clock().now();
|
||||
I2PSocketImpl s = null;
|
||||
synchronized (lock) {
|
||||
s = (I2PSocketImpl) _outSockets.get(id);
|
||||
}
|
||||
|
||||
if (s == null) {
|
||||
_log.warn(getName() + ": No socket responsible for ACK packet for id " + getReadableForm(id));
|
||||
return;
|
||||
}
|
||||
|
||||
long socketRetrieved = _context.clock().now();
|
||||
|
||||
String remoteId = null;
|
||||
remoteId = s.getRemoteID(false);
|
||||
|
||||
if ( (payload.length == 3) && (remoteId == null) ) {
|
||||
String newID = toString(payload);
|
||||
long beforeSetRemId = _context.clock().now();
|
||||
s.setRemoteID(newID);
|
||||
if (_log.shouldLog(Log.DEBUG)) {
|
||||
_log.debug(getName() + ": ackAvailable - socket retrieval took "
|
||||
+ (socketRetrieved-begin) + "ms, getRemoteId took "
|
||||
+ (beforeSetRemId-socketRetrieved) + "ms, setRemoteId took "
|
||||
+ (_context.clock().now()-beforeSetRemId) + "ms");
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
// (payload.length != 3 || getRemoteId != null)
|
||||
if (_log.shouldLog(Log.WARN)) {
|
||||
if (payload.length != 3)
|
||||
_log.warn(getName() + ": Ack packet had " + payload.length + " bytes");
|
||||
else
|
||||
_log.warn(getName() + ": Remote ID already exists? " + remoteId);
|
||||
}
|
||||
if (_log.shouldLog(Log.DEBUG)) {
|
||||
_log.debug(getName() + ": invalid ack - socket retrieval took "
|
||||
+ (socketRetrieved-begin) + "ms, overall took "
|
||||
+ (_context.clock().now()-begin) + "ms");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We received a disconnect packet, telling us to tear down the specified
|
||||
* stream.
|
||||
*/
|
||||
private void disconnectAvailable(String id, byte payload[]) {
|
||||
I2PSocketImpl s = null;
|
||||
synchronized (lock) {
|
||||
s = (I2PSocketImpl) _outSockets.get(id);
|
||||
}
|
||||
|
||||
_log.debug(getName() + ": *Disconnect outgoing for socket " + s + " on id "
|
||||
+ getReadableForm(id));
|
||||
try {
|
||||
if (s != null) {
|
||||
if (payload.length > 0) {
|
||||
_log.debug(getName() + ": Disconnect packet had "
|
||||
+ payload.length + " bytes");
|
||||
}
|
||||
if (s.getRemoteID(false) == null) {
|
||||
s.setRemoteID(null); // Just to wake up socket
|
||||
return;
|
||||
}
|
||||
s.internalClose();
|
||||
synchronized (lock) {
|
||||
_outSockets.remove(id);
|
||||
}
|
||||
}
|
||||
return;
|
||||
} catch (Exception t) {
|
||||
_log.warn(getName() + ": Ignoring error on disconnect for socket " + s, t);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We've received data on a stream we created - toss the data onto
|
||||
* the socket for handling.
|
||||
*
|
||||
* @throws IllegalStateException if the socket isn't open or isn't known
|
||||
*/
|
||||
private void sendOutgoingAvailable(String id, byte payload[]) throws IllegalStateException {
|
||||
I2PSocketImpl s = null;
|
||||
synchronized (lock) {
|
||||
s = (I2PSocketImpl) _outSockets.get(id);
|
||||
}
|
||||
|
||||
// packet send outgoing
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(getName() + ": *Packet send outgoing [" + payload.length + "] for socket "
|
||||
+ s + " on id " + getReadableForm(id));
|
||||
if (s != null) {
|
||||
s.queueData(payload);
|
||||
return;
|
||||
} else {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn(getName() + ": Null socket with data available");
|
||||
throw new IllegalStateException("Null socket with data available");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We've received a SYN packet (a request for a new stream). If the client has
|
||||
* said they want incoming sockets (by retrieving the serverSocket), the stream
|
||||
* will be ACKed, but if they have not, they'll be NACKed)
|
||||
*
|
||||
* @throws DataFormatException if the destination in the SYN was invalid
|
||||
* @throws I2PSessionException if there was an I2P error sending the ACK or NACK
|
||||
*/
|
||||
private void synIncomingAvailable(String id, byte payload[], I2PSession session)
|
||||
throws DataFormatException, I2PSessionException {
|
||||
Destination d = new Destination();
|
||||
d.fromByteArray(payload);
|
||||
|
||||
I2PSocketImpl s = null;
|
||||
boolean acceptConnections = (_serverSocket != null);
|
||||
String newLocalID = null;
|
||||
synchronized (lock) {
|
||||
newLocalID = makeID(_inSockets);
|
||||
if (acceptConnections) {
|
||||
s = new I2PSocketImpl(d, this, false, newLocalID);
|
||||
s.setRemoteID(id);
|
||||
}
|
||||
}
|
||||
_log.debug(getName() + ": *Syn! for socket " + s + " on id " + getReadableForm(newLocalID)
|
||||
+ " from " + d.calculateHash().toBase64().substring(0,6));
|
||||
|
||||
if (!acceptConnections) {
|
||||
// The app did not instantiate an I2PServerSocket
|
||||
byte[] packet = makePacket((byte) CLOSE_OUT, id, toBytes(newLocalID));
|
||||
boolean replySentOk = false;
|
||||
synchronized (_session) {
|
||||
replySentOk = _session.sendMessage(d, packet);
|
||||
}
|
||||
if (!replySentOk) {
|
||||
_log.warn(getName() + ": Error sending close to " + d.calculateHash().toBase64()
|
||||
+ " in response to a new con message",
|
||||
new Exception("Failed creation"));
|
||||
}
|
||||
_context.statManager().addRateData("streaming.nackSent", 1, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_serverSocket.addWaitForAccept(s, _acceptTimeout)) {
|
||||
_inSockets.put(newLocalID, s);
|
||||
byte[] packet = makePacket((byte) ACK, id, toBytes(newLocalID));
|
||||
boolean replySentOk = false;
|
||||
replySentOk = _session.sendMessage(d, packet);
|
||||
if (!replySentOk) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn(getName() + ": Error sending reply to " + d.calculateHash().toBase64()
|
||||
+ " in response to a new con message for socket " + s,
|
||||
new Exception("Failed creation"));
|
||||
s.internalClose();
|
||||
_context.statManager().addRateData("streaming.ackSendFailed", 1, 1);
|
||||
}
|
||||
} else {
|
||||
// timed out or serverSocket closed
|
||||
byte[] packet = toBytes(" " + id);
|
||||
packet[0] = CLOSE_OUT;
|
||||
boolean nackSent = session.sendMessage(d, packet);
|
||||
if (!nackSent) {
|
||||
_log.warn(getName() + ": Error sending NACK for session creation for socket " + s);
|
||||
}
|
||||
s.internalClose();
|
||||
_context.statManager().addRateData("streaming,nackSent", 1, 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* We've received a disconnect for a socket we didn't initiate, so kill
|
||||
* the socket.
|
||||
*
|
||||
*/
|
||||
private void disconnectIncoming(String id, byte payload[]) {
|
||||
I2PSocketImpl s = null;
|
||||
synchronized (lock) {
|
||||
s = (I2PSocketImpl) _inSockets.get(id);
|
||||
if (payload.length == 0 && s != null) {
|
||||
_inSockets.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
_log.debug(getName() + ": *Disconnect incoming for socket " + s);
|
||||
|
||||
try {
|
||||
if (payload.length == 0 && s != null) {
|
||||
s.internalClose();
|
||||
return;
|
||||
} else {
|
||||
if ( (payload.length > 0) && (_log.shouldLog(Log.ERROR)) )
|
||||
_log.warn(getName() + ": Disconnect packet had " + payload.length + " bytes");
|
||||
if (s != null)
|
||||
s.internalClose();
|
||||
return;
|
||||
}
|
||||
} catch (Exception t) {
|
||||
_log.warn(getName() + ": Ignoring error on disconnect", t);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We've received data on a stream we received - toss the data onto
|
||||
* the socket for handling.
|
||||
*
|
||||
* @throws IllegalStateException if the socket isn't open or isn't known
|
||||
*/
|
||||
private void sendIncoming(String id, byte payload[]) throws IllegalStateException {
|
||||
I2PSocketImpl s = null;
|
||||
synchronized (lock) {
|
||||
s = (I2PSocketImpl) _inSockets.get(id);
|
||||
}
|
||||
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(getName() + ": *Packet send incoming [" + payload.length + "] for socket " + s);
|
||||
|
||||
if (s != null) {
|
||||
s.queueData(payload);
|
||||
return;
|
||||
} else {
|
||||
_log.info(getName() + ": Null socket with data available");
|
||||
throw new IllegalStateException("Null socket with data available");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unknown packet. moo.
|
||||
*
|
||||
*/
|
||||
private void handleUnknown(int type, String id, byte payload[]) {
|
||||
_log.error(getName() + ": \n\n=============== Unknown packet! " + "============"
|
||||
+ "\nType: " + (int) type
|
||||
+ "\nID: " + getReadableForm(id)
|
||||
+ "\nBase64'ed Data: " + Base64.encode(payload)
|
||||
+ "\n\n\n");
|
||||
if (id != null) {
|
||||
synchronized (lock) {
|
||||
_inSockets.remove(id);
|
||||
_outSockets.remove(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void reportAbuse(I2PSession session, int severity) {
|
||||
_log.error(getName() + ": Abuse reported [" + severity + "]");
|
||||
}
|
||||
|
||||
public void setDefaultOptions(I2PSocketOptions options) {
|
||||
_defaultOptions = options;
|
||||
}
|
||||
|
||||
public I2PSocketOptions getDefaultOptions() {
|
||||
return _defaultOptions;
|
||||
}
|
||||
|
||||
public I2PServerSocket getServerSocket() {
|
||||
if (_serverSocket == null) {
|
||||
_serverSocket = new I2PServerSocketImpl(this);
|
||||
}
|
||||
return _serverSocket;
|
||||
}
|
||||
public I2PSocketOptions buildOptions();
|
||||
public I2PSocketOptions buildOptions(Properties opts);
|
||||
|
||||
/**
|
||||
* Create a new connected socket (block until the socket is created)
|
||||
@@ -448,117 +66,7 @@ public class I2PSocketManager implements I2PSessionListener {
|
||||
*/
|
||||
public I2PSocket connect(Destination peer, I2PSocketOptions options)
|
||||
throws I2PException, ConnectException,
|
||||
NoRouteToHostException, InterruptedIOException {
|
||||
String localID, lcID;
|
||||
I2PSocketImpl s;
|
||||
synchronized (lock) {
|
||||
localID = makeID(_outSockets);
|
||||
lcID = getReadableForm(localID);
|
||||
s = new I2PSocketImpl(peer, this, true, localID);
|
||||
_outSockets.put(localID, s);
|
||||
}
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(getName() + ": connect(" + peer.calculateHash().toBase64().substring(0,6)
|
||||
+ ", ...): localID = " + lcID);
|
||||
|
||||
try {
|
||||
ByteArrayOutputStream pubkey = new ByteArrayOutputStream();
|
||||
_session.getMyDestination().writeBytes(pubkey);
|
||||
String remoteID;
|
||||
byte[] packet = makePacket((byte) SYN, localID, pubkey.toByteArray());
|
||||
boolean sent = false;
|
||||
sent = _session.sendMessage(peer, packet);
|
||||
if (!sent) {
|
||||
_log.info(getName() + ": Unable to send & receive ack for SYN packet for socket "
|
||||
+ s + " with localID = " + lcID);
|
||||
synchronized (lock) {
|
||||
_outSockets.remove(s.getLocalID());
|
||||
}
|
||||
_context.statManager().addRateData("streaming.synNoAck", 1, 1);
|
||||
throw new I2PException("Error sending through I2P network");
|
||||
} else {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(getName() + ": syn sent ok to "
|
||||
+ peer.calculateHash().toBase64().substring(0,6)
|
||||
+ " with localID = " + lcID);
|
||||
}
|
||||
if (options != null)
|
||||
remoteID = s.getRemoteID(true, options.getConnectTimeout());
|
||||
else
|
||||
remoteID = s.getRemoteID(true, getDefaultOptions().getConnectTimeout());
|
||||
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(getName() + ": remoteID received from "
|
||||
+ peer.calculateHash().toBase64().substring(0,6)
|
||||
+ ": " + getReadableForm(remoteID)
|
||||
+ " with localID = " + lcID);
|
||||
|
||||
if (remoteID == null) {
|
||||
_context.statManager().addRateData("streaming.nackReceived", 1, 1);
|
||||
throw new ConnectException("Connection refused by peer for socket " + s);
|
||||
}
|
||||
if ("".equals(remoteID)) {
|
||||
_context.statManager().addRateData("streaming.synNoAck", 1, 1);
|
||||
throw new NoRouteToHostException("Unable to reach peer for socket " + s);
|
||||
}
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(getName() + ": TIMING: s given out for remoteID "
|
||||
+ getReadableForm(remoteID) + " for socket " + s);
|
||||
|
||||
return s;
|
||||
} catch (InterruptedIOException ioe) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn(getName() + ": Timeout waiting for ack from syn for id "
|
||||
+ lcID + " to " + peer.calculateHash().toBase64().substring(0,6)
|
||||
+ " for socket " + s, ioe);
|
||||
synchronized (lock) {
|
||||
_outSockets.remove(s.getLocalID());
|
||||
}
|
||||
s.internalClose();
|
||||
_context.statManager().addRateData("streaming.synNoAck", 1, 1);
|
||||
throw new InterruptedIOException("Timeout waiting for ack");
|
||||
} catch (ConnectException ex) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(getName() + ": Connection error waiting for ack from syn for id "
|
||||
+ lcID + " to " + peer.calculateHash().toBase64().substring(0,6)
|
||||
+ " for socket " + s, ex);
|
||||
s.internalClose();
|
||||
throw ex;
|
||||
} catch (NoRouteToHostException ex) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(getName() + ": No route to host waiting for ack from syn for id "
|
||||
+ lcID + " to " + peer.calculateHash().toBase64().substring(0,6)
|
||||
+ " for socket " + s, ex);
|
||||
s.internalClose();
|
||||
throw ex;
|
||||
} catch (IOException ex) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn(getName() + ": Error sending syn on id "
|
||||
+ lcID + " to " + peer.calculateHash().toBase64().substring(0,6)
|
||||
+ " for socket " + s, ex);
|
||||
synchronized (lock) {
|
||||
_outSockets.remove(s.getLocalID());
|
||||
}
|
||||
s.internalClose();
|
||||
throw new I2PException("Unhandled IOException occurred");
|
||||
} catch (I2PException ex) {
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info(getName() + ": Error sending syn on id "
|
||||
+ lcID + " to " + peer.calculateHash().toBase64().substring(0,6)
|
||||
+ " for socket " + s, ex);
|
||||
synchronized (lock) {
|
||||
_outSockets.remove(s.getLocalID());
|
||||
}
|
||||
s.internalClose();
|
||||
throw ex;
|
||||
} catch (Exception e) {
|
||||
s.internalClose();
|
||||
_log.warn(getName() + ": Unhandled error connecting on "
|
||||
+ lcID + " to " + peer.calculateHash().toBase64().substring(0,6)
|
||||
+ " for socket " + s, e);
|
||||
throw new ConnectException("Unhandled error connecting: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
NoRouteToHostException, InterruptedIOException;
|
||||
|
||||
/**
|
||||
* Create a new connected socket (block until the socket is created)
|
||||
@@ -571,182 +79,30 @@ public class I2PSocketManager implements I2PSessionListener {
|
||||
* @throws I2PException if there is some other I2P-related problem
|
||||
*/
|
||||
public I2PSocket connect(Destination peer) throws I2PException, ConnectException,
|
||||
NoRouteToHostException, InterruptedIOException {
|
||||
return connect(peer, null);
|
||||
}
|
||||
|
||||
NoRouteToHostException, InterruptedIOException;
|
||||
|
||||
/**
|
||||
* Destroy the socket manager, freeing all the associated resources. This
|
||||
* method will block untill all the managed sockets are closed.
|
||||
*
|
||||
*/
|
||||
public void destroySocketManager() {
|
||||
if (_serverSocket != null) {
|
||||
_serverSocket.close();
|
||||
_serverSocket = null;
|
||||
}
|
||||
|
||||
synchronized (lock) {
|
||||
Iterator iter;
|
||||
String id = null;
|
||||
I2PSocketImpl sock;
|
||||
|
||||
iter = _inSockets.keySet().iterator();
|
||||
while (iter.hasNext()) {
|
||||
id = (String)iter.next();
|
||||
sock = (I2PSocketImpl)_inSockets.get(id);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(getName() + ": Closing inSocket \""
|
||||
+ getReadableForm(sock.getLocalID()) + "\"");
|
||||
sock.internalClose();
|
||||
}
|
||||
|
||||
iter = _outSockets.keySet().iterator();
|
||||
while (iter.hasNext()) {
|
||||
id = (String)iter.next();
|
||||
sock = (I2PSocketImpl)_outSockets.get(id);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(getName() + ": Closing outSocket \""
|
||||
+ getReadableForm(sock.getLocalID()) + "\"");
|
||||
sock.internalClose();
|
||||
}
|
||||
}
|
||||
|
||||
_log.debug(getName() + ": Waiting for all open sockets to really close...");
|
||||
synchronized (lock) {
|
||||
while ((_inSockets.size() != 0) || (_outSockets.size() != 0)) {
|
||||
try {
|
||||
lock.wait();
|
||||
} catch (InterruptedException e) {}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
_log.debug(getName() + ": Destroying I2P session...");
|
||||
_session.destroySession();
|
||||
_log.debug(getName() + ": I2P session destroyed");
|
||||
} catch (I2PSessionException e) {
|
||||
_log.warn(getName() + ": Error destroying I2P session", e);
|
||||
}
|
||||
}
|
||||
public void destroySocketManager();
|
||||
|
||||
/**
|
||||
* Retrieve a set of currently connected I2PSockets, either initiated locally or remotely.
|
||||
*
|
||||
*/
|
||||
public Set listSockets() {
|
||||
Set sockets = new HashSet(8);
|
||||
synchronized (lock) {
|
||||
sockets.addAll(_inSockets.values());
|
||||
sockets.addAll(_outSockets.values());
|
||||
}
|
||||
return sockets;
|
||||
}
|
||||
public Set listSockets();
|
||||
|
||||
/**
|
||||
* Ping the specified peer, returning true if they replied to the ping within
|
||||
* the timeout specified, false otherwise. This call blocks.
|
||||
*
|
||||
*/
|
||||
public boolean ping(Destination peer, long timeoutMs) {
|
||||
try {
|
||||
return _session.sendMessage(peer, new byte[] { (byte) CHAFF});
|
||||
} catch (I2PException ex) {
|
||||
_log.warn(getName() + ": I2PException:", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public boolean ping(Destination peer, long timeoutMs);
|
||||
|
||||
public void removeSocket(I2PSocketImpl sock) {
|
||||
String localId = sock.getLocalID();
|
||||
boolean removed = false;
|
||||
synchronized (lock) {
|
||||
removed = (null != _inSockets.remove(localId));
|
||||
removed = removed || (null != _outSockets.remove(localId));
|
||||
lock.notify();
|
||||
}
|
||||
public String getName();
|
||||
public void setName(String name);
|
||||
|
||||
long now = _context.clock().now();
|
||||
long lifetime = now - sock.getCreatedOn();
|
||||
long timeSinceClose = now - sock.getClosedOn();
|
||||
long sent = sock.getBytesSent();
|
||||
long recv = sock.getBytesReceived();
|
||||
|
||||
if (_log.shouldLog(Log.DEBUG)) {
|
||||
_log.debug(getName() + ": Removing socket \"" + getReadableForm(localId) + "\" [" + sock
|
||||
+ ", send: " + sent + ", recv: " + recv
|
||||
+ ", lifetime: " + lifetime + "ms, time since close: " + timeSinceClose
|
||||
+ " removed? " + removed + ")]",
|
||||
new Exception("removeSocket called"));
|
||||
}
|
||||
|
||||
_context.statManager().addRateData("streaming.lifetime", lifetime, lifetime);
|
||||
_context.statManager().addRateData("streaming.sent", sent, lifetime);
|
||||
_context.statManager().addRateData("streaming.received", recv, lifetime);
|
||||
|
||||
if (sent > recv) {
|
||||
_context.statManager().addRateData("streaming.transferBalance", 1, lifetime);
|
||||
} else if (recv > sent) {
|
||||
_context.statManager().addRateData("streaming.transferBalance", -1, lifetime);
|
||||
} else {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
public String getName() { return _name; }
|
||||
public void setName(String name) { _name = name; }
|
||||
|
||||
public static String getReadableForm(String id) {
|
||||
if (id == null) return "(null)";
|
||||
if (id.length() != 3) return "Bogus";
|
||||
return Base64.encode(toBytes(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new part the connection ID that is locally unique
|
||||
*
|
||||
* @param uniqueIn map of already known local IDs so we don't collide. WARNING - NOT THREADSAFE!
|
||||
*/
|
||||
private static String makeID(HashMap uniqueIn) {
|
||||
String newID;
|
||||
do {
|
||||
int id = (int) (Math.random() * 16777215 + 1);
|
||||
byte[] nid = new byte[3];
|
||||
nid[0] = (byte) (id / 65536);
|
||||
nid[1] = (byte) ((id / 256) % 256);
|
||||
nid[2] = (byte) (id % 256);
|
||||
newID = toString(nid);
|
||||
} while (uniqueIn.get(newID) != null);
|
||||
return newID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new packet of the given type for the specified connection containing
|
||||
* the given payload
|
||||
*/
|
||||
public static byte[] makePacket(byte type, String id, byte[] payload) {
|
||||
byte[] packet = new byte[payload.length + 4];
|
||||
packet[0] = type;
|
||||
byte[] temp = toBytes(id);
|
||||
if (temp.length != 3) throw new RuntimeException("Incorrect ID length: " + temp.length);
|
||||
System.arraycopy(temp, 0, packet, 1, 3);
|
||||
System.arraycopy(payload, 0, packet, 4, payload.length);
|
||||
return packet;
|
||||
}
|
||||
|
||||
private static final String toString(byte data[]) {
|
||||
try {
|
||||
return new String(data, "ISO-8859-1");
|
||||
} catch (UnsupportedEncodingException uee) {
|
||||
throw new RuntimeException("WTF! iso-8859-1 isn't supported?");
|
||||
}
|
||||
}
|
||||
|
||||
private static final byte[] toBytes(String str) {
|
||||
try {
|
||||
return str.getBytes("ISO-8859-1");
|
||||
} catch (UnsupportedEncodingException uee) {
|
||||
throw new RuntimeException("WTF! iso-8859-1 isn't supported?");
|
||||
}
|
||||
}
|
||||
public void init(I2PAppContext context, I2PSession session, Properties opts, String name);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Iterator;
|
||||
import java.util.Properties;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.I2PException;
|
||||
import net.i2p.client.I2PClient;
|
||||
import net.i2p.client.I2PClientFactory;
|
||||
@@ -23,6 +25,10 @@ import net.i2p.util.Log;
|
||||
public class I2PSocketManagerFactory {
|
||||
private final static Log _log = new Log(I2PSocketManagerFactory.class);
|
||||
|
||||
public static final String PROP_MANAGER = "i2p.streaming.manager";
|
||||
//public static final String DEFAULT_MANAGER = "net.i2p.client.streaming.I2PSocketManagerImpl";
|
||||
public static final String DEFAULT_MANAGER = "net.i2p.client.streaming.I2PSocketManagerFull";
|
||||
|
||||
/**
|
||||
* Create a socket manager using a brand new destination connected to the
|
||||
* I2CP router on the local machine on the default port (7654).
|
||||
@@ -76,23 +82,74 @@ public class I2PSocketManagerFactory {
|
||||
public static I2PSocketManager createManager(InputStream myPrivateKeyStream, String i2cpHost, int i2cpPort,
|
||||
Properties opts) {
|
||||
I2PClient client = I2PClientFactory.createClient();
|
||||
opts.setProperty(I2PClient.PROP_RELIABILITY, I2PClient.PROP_RELIABILITY_GUARANTEED);
|
||||
if (opts == null)
|
||||
opts = new Properties();
|
||||
for (Iterator iter = System.getProperties().keySet().iterator(); iter.hasNext(); ) {
|
||||
String name = (String)iter.next();
|
||||
if (!opts.containsKey(name))
|
||||
opts.setProperty(name, System.getProperty(name));
|
||||
}
|
||||
boolean oldLib = DEFAULT_MANAGER.equals(opts.getProperty(PROP_MANAGER, DEFAULT_MANAGER));
|
||||
if (oldLib && false) {
|
||||
// for the old streaming lib
|
||||
opts.setProperty(I2PClient.PROP_RELIABILITY, I2PClient.PROP_RELIABILITY_GUARANTEED);
|
||||
//opts.setProperty("tunnels.depthInbound", "0");
|
||||
} else {
|
||||
// for new streaming lib:
|
||||
opts.setProperty(I2PClient.PROP_RELIABILITY, I2PClient.PROP_RELIABILITY_BEST_EFFORT);
|
||||
//p.setProperty("tunnels.depthInbound", "0");
|
||||
}
|
||||
|
||||
opts.setProperty(I2PClient.PROP_TCP_HOST, i2cpHost);
|
||||
opts.setProperty(I2PClient.PROP_TCP_PORT, "" + i2cpPort);
|
||||
|
||||
try {
|
||||
I2PSession session = client.createSession(myPrivateKeyStream, opts);
|
||||
session.connect();
|
||||
return createManager(session);
|
||||
I2PSocketManager sockMgr = createManager(session, opts, "manager");
|
||||
if (sockMgr != null)
|
||||
sockMgr.setDefaultOptions(sockMgr.buildOptions(opts));
|
||||
return sockMgr;
|
||||
} catch (I2PSessionException ise) {
|
||||
_log.error("Error creating session for socket manager", ise);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static I2PSocketManager createManager(I2PSession session) {
|
||||
I2PSocketManager mgr = new I2PSocketManager();
|
||||
mgr.setSession(session);
|
||||
mgr.setDefaultOptions(new I2PSocketOptions());
|
||||
return mgr;
|
||||
private static I2PSocketManager createManager(I2PSession session, Properties opts, String name) {
|
||||
if (false) {
|
||||
I2PSocketManagerImpl mgr = new I2PSocketManagerImpl();
|
||||
mgr.setSession(session);
|
||||
//mgr.setDefaultOptions(new I2PSocketOptions());
|
||||
return mgr;
|
||||
} else {
|
||||
String classname = opts.getProperty(PROP_MANAGER, DEFAULT_MANAGER);
|
||||
if (classname != null) {
|
||||
try {
|
||||
Class cls = Class.forName(classname);
|
||||
Object obj = cls.newInstance();
|
||||
if (obj instanceof I2PSocketManager) {
|
||||
I2PSocketManager mgr = (I2PSocketManager)obj;
|
||||
I2PAppContext context = I2PAppContext.getGlobalContext();
|
||||
mgr.init(context, session, opts, name);
|
||||
return mgr;
|
||||
} else {
|
||||
throw new IllegalStateException("Invalid manager class [" + classname + "]");
|
||||
}
|
||||
} catch (ClassNotFoundException cnfe) {
|
||||
_log.error("Error loading " + classname, cnfe);
|
||||
throw new IllegalStateException("Invalid manager class [" + classname + "] - not found");
|
||||
} catch (InstantiationException ie) {
|
||||
_log.error("Error loading " + classname, ie);
|
||||
throw new IllegalStateException("Invalid manager class [" + classname + "] - unable to instantiate");
|
||||
} catch (IllegalAccessException iae) {
|
||||
_log.error("Error loading " + classname, iae);
|
||||
throw new IllegalStateException("Invalid manager class [" + classname + "] - illegal access");
|
||||
}
|
||||
} else {
|
||||
throw new IllegalStateException("No manager class specified");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,763 @@
|
||||
/*
|
||||
* licensed under BSD license...
|
||||
* (if you know the proper clause for that, add it ...)
|
||||
*/
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.NoRouteToHostException;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.I2PException;
|
||||
import net.i2p.client.I2PSession;
|
||||
import net.i2p.client.I2PSessionException;
|
||||
import net.i2p.client.I2PSessionListener;
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.data.DataFormatException;
|
||||
import net.i2p.data.Destination;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
|
||||
/**
|
||||
* Centralize the coordination and multiplexing of the local client's streaming.
|
||||
* There should be one I2PSocketManager for each I2PSession, and if an application
|
||||
* is sending and receiving data through the streaming library using an
|
||||
* I2PSocketManager, it should not attempt to call I2PSession's setSessionListener
|
||||
* or receive any messages with its .receiveMessage
|
||||
*
|
||||
*/
|
||||
class I2PSocketManagerImpl implements I2PSocketManager, I2PSessionListener {
|
||||
private I2PAppContext _context;
|
||||
private Log _log;
|
||||
private I2PSession _session;
|
||||
private I2PServerSocketImpl _serverSocket = null;
|
||||
private Object lock = new Object(); // for locking socket lists
|
||||
private HashMap _outSockets;
|
||||
private HashMap _inSockets;
|
||||
private I2PSocketOptions _defaultOptions;
|
||||
private long _acceptTimeout;
|
||||
private String _name;
|
||||
private static int __managerId = 0;
|
||||
|
||||
public static final short ACK = 0x51;
|
||||
public static final short CLOSE_OUT = 0x52;
|
||||
public static final short DATA_OUT = 0x50;
|
||||
public static final short SYN = 0xA1;
|
||||
public static final short CLOSE_IN = 0xA2;
|
||||
public static final short DATA_IN = 0xA0;
|
||||
public static final short CHAFF = 0xFF;
|
||||
|
||||
/**
|
||||
* How long to wait for the client app to accept() before sending back CLOSE?
|
||||
* This includes the time waiting in the queue. Currently set to 5 seconds.
|
||||
*/
|
||||
private static final long ACCEPT_TIMEOUT_DEFAULT = 5*1000;
|
||||
|
||||
public I2PSocketManagerImpl() {
|
||||
this("SocketManager " + (++__managerId));
|
||||
}
|
||||
public I2PSocketManagerImpl(String name) {
|
||||
init(I2PAppContext.getGlobalContext(), null, null, name);
|
||||
}
|
||||
|
||||
public void init(I2PAppContext context, I2PSession session, Properties opts, String name) {
|
||||
_name = name;
|
||||
_context = context;
|
||||
_log = _context.logManager().getLog(I2PSocketManager.class);
|
||||
_inSockets = new HashMap(16);
|
||||
_outSockets = new HashMap(16);
|
||||
_acceptTimeout = ACCEPT_TIMEOUT_DEFAULT;
|
||||
setSession(session);
|
||||
setDefaultOptions(buildOptions(opts));
|
||||
_context.statManager().createRateStat("streaming.lifetime", "How long before the socket is closed?", "streaming", new long[] { 10*60*1000, 60*60*1000, 24*60*60*1000 });
|
||||
_context.statManager().createRateStat("streaming.sent", "How many bytes are sent in the stream?", "streaming", new long[] { 10*60*1000, 60*60*1000, 24*60*60*1000 });
|
||||
_context.statManager().createRateStat("streaming.received", "How many bytes are received in the stream?", "streaming", new long[] { 10*60*1000, 60*60*1000, 24*60*60*1000 });
|
||||
_context.statManager().createRateStat("streaming.transferBalance", "How many streams send more than they receive (positive means more sent, negative means more received)?", "streaming", new long[] { 10*60*1000, 60*60*1000, 24*60*60*1000 });
|
||||
_context.statManager().createRateStat("streaming.synNoAck", "How many times have we sent a SYN but not received an ACK?", "streaming", new long[] { 10*60*1000, 60*60*1000, 24*60*60*1000 });
|
||||
_context.statManager().createRateStat("streaming.ackSendFailed", "How many times have we tried to send an ACK to a SYN and failed?", "streaming", new long[] { 10*60*1000, 60*60*1000, 24*60*60*1000 });
|
||||
_context.statManager().createRateStat("streaming.nackSent", "How many times have we refused a SYN with a NACK?", "streaming", new long[] { 10*60*1000, 60*60*1000, 24*60*60*1000 });
|
||||
_context.statManager().createRateStat("streaming.nackReceived", "How many times have we received a NACK to our SYN?", "streaming", new long[] { 10*60*1000, 60*60*1000, 24*60*60*1000 });
|
||||
}
|
||||
|
||||
public I2PSession getSession() {
|
||||
return _session;
|
||||
}
|
||||
|
||||
public void setSession(I2PSession session) {
|
||||
_session = session;
|
||||
if (session != null) session.setSessionListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* How long should we wait for the client to .accept() a socket before
|
||||
* sending back a NACK/Close?
|
||||
*
|
||||
* @param ms milliseconds to wait, maximum
|
||||
*/
|
||||
public void setAcceptTimeout(long ms) { _acceptTimeout = ms; }
|
||||
public long getAcceptTimeout() { return _acceptTimeout; }
|
||||
|
||||
public void disconnected(I2PSession session) {
|
||||
_log.info(getName() + ": Disconnected from the session");
|
||||
destroySocketManager();
|
||||
}
|
||||
|
||||
public void errorOccurred(I2PSession session, String message, Throwable error) {
|
||||
_log.error(getName() + ": Error occurred: [" + message + "]", error);
|
||||
}
|
||||
|
||||
public void messageAvailable(I2PSession session, int msgId, long size) {
|
||||
try {
|
||||
I2PSocketImpl s;
|
||||
byte msg[] = session.receiveMessage(msgId);
|
||||
if (msg.length == 1 && msg[0] == -1) {
|
||||
_log.debug(getName() + ": Ping received");
|
||||
return;
|
||||
}
|
||||
if (msg.length < 4) {
|
||||
_log.warn(getName() + ": ==== packet too short ====");
|
||||
return;
|
||||
}
|
||||
int type = msg[0] & 0xff;
|
||||
String id = toString(new byte[] { msg[1], msg[2], msg[3]});
|
||||
byte[] payload = new byte[msg.length - 4];
|
||||
System.arraycopy(msg, 4, payload, 0, payload.length);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(getName() + ": Message read: type = [" + Integer.toHexString(type)
|
||||
+ "] id = [" + getReadableForm(id)
|
||||
+ "] payload length: [" + payload.length + "]");
|
||||
switch (type) {
|
||||
case ACK:
|
||||
ackAvailable(id, payload);
|
||||
return;
|
||||
case CLOSE_OUT:
|
||||
disconnectAvailable(id, payload);
|
||||
return;
|
||||
case DATA_OUT:
|
||||
sendOutgoingAvailable(id, payload);
|
||||
return;
|
||||
case SYN:
|
||||
synIncomingAvailable(id, payload, session);
|
||||
return;
|
||||
case CLOSE_IN:
|
||||
disconnectIncoming(id, payload);
|
||||
return;
|
||||
case DATA_IN:
|
||||
sendIncoming(id, payload);
|
||||
case CHAFF:
|
||||
// ignore
|
||||
return;
|
||||
default:
|
||||
handleUnknown(type, id, payload);
|
||||
return;
|
||||
}
|
||||
} catch (I2PException ise) {
|
||||
_log.warn(getName() + ": Error processing", ise);
|
||||
} catch (IllegalStateException ise) {
|
||||
_log.debug(getName() + ": Error processing", ise);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We've received an ACK packet (hopefully, in response to a SYN that we
|
||||
* recently sent out). Notify the associated I2PSocket that we now have
|
||||
* the remote stream ID (which should get things going, since the handshake
|
||||
* is complete).
|
||||
*
|
||||
*/
|
||||
private void ackAvailable(String id, byte payload[]) {
|
||||
long begin = _context.clock().now();
|
||||
I2PSocketImpl s = null;
|
||||
synchronized (lock) {
|
||||
s = (I2PSocketImpl) _outSockets.get(id);
|
||||
}
|
||||
|
||||
if (s == null) {
|
||||
_log.warn(getName() + ": No socket responsible for ACK packet for id " + getReadableForm(id));
|
||||
return;
|
||||
}
|
||||
|
||||
long socketRetrieved = _context.clock().now();
|
||||
|
||||
String remoteId = null;
|
||||
remoteId = s.getRemoteID(false);
|
||||
|
||||
if ( (payload.length == 3) && (remoteId == null) ) {
|
||||
String newID = toString(payload);
|
||||
long beforeSetRemId = _context.clock().now();
|
||||
s.setRemoteID(newID);
|
||||
if (_log.shouldLog(Log.DEBUG)) {
|
||||
_log.debug(getName() + ": ackAvailable - socket retrieval took "
|
||||
+ (socketRetrieved-begin) + "ms, getRemoteId took "
|
||||
+ (beforeSetRemId-socketRetrieved) + "ms, setRemoteId took "
|
||||
+ (_context.clock().now()-beforeSetRemId) + "ms");
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
// (payload.length != 3 || getRemoteId != null)
|
||||
if (_log.shouldLog(Log.WARN)) {
|
||||
if (payload.length != 3)
|
||||
_log.warn(getName() + ": Ack packet had " + payload.length + " bytes");
|
||||
else
|
||||
_log.warn(getName() + ": Remote ID already exists? " + remoteId);
|
||||
}
|
||||
if (_log.shouldLog(Log.DEBUG)) {
|
||||
_log.debug(getName() + ": invalid ack - socket retrieval took "
|
||||
+ (socketRetrieved-begin) + "ms, overall took "
|
||||
+ (_context.clock().now()-begin) + "ms");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We received a disconnect packet, telling us to tear down the specified
|
||||
* stream.
|
||||
*/
|
||||
private void disconnectAvailable(String id, byte payload[]) {
|
||||
I2PSocketImpl s = null;
|
||||
synchronized (lock) {
|
||||
s = (I2PSocketImpl) _outSockets.get(id);
|
||||
}
|
||||
|
||||
_log.debug(getName() + ": *Disconnect outgoing for socket " + s + " on id "
|
||||
+ getReadableForm(id));
|
||||
try {
|
||||
if (s != null) {
|
||||
if (payload.length > 0) {
|
||||
_log.debug(getName() + ": Disconnect packet had "
|
||||
+ payload.length + " bytes");
|
||||
}
|
||||
if (s.getRemoteID(false) == null) {
|
||||
s.setRemoteID(null); // Just to wake up socket
|
||||
return;
|
||||
}
|
||||
s.internalClose();
|
||||
synchronized (lock) {
|
||||
_outSockets.remove(id);
|
||||
}
|
||||
}
|
||||
return;
|
||||
} catch (Exception t) {
|
||||
_log.warn(getName() + ": Ignoring error on disconnect for socket " + s, t);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We've received data on a stream we created - toss the data onto
|
||||
* the socket for handling.
|
||||
*
|
||||
* @throws IllegalStateException if the socket isn't open or isn't known
|
||||
*/
|
||||
private void sendOutgoingAvailable(String id, byte payload[]) throws IllegalStateException {
|
||||
I2PSocketImpl s = null;
|
||||
synchronized (lock) {
|
||||
s = (I2PSocketImpl) _outSockets.get(id);
|
||||
}
|
||||
|
||||
// packet send outgoing
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(getName() + ": *Packet send outgoing [" + payload.length + "] for socket "
|
||||
+ s + " on id " + getReadableForm(id));
|
||||
if (s != null) {
|
||||
s.queueData(payload);
|
||||
return;
|
||||
} else {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn(getName() + ": Null socket with data available");
|
||||
throw new IllegalStateException("Null socket with data available");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We've received a SYN packet (a request for a new stream). If the client has
|
||||
* said they want incoming sockets (by retrieving the serverSocket), the stream
|
||||
* will be ACKed, but if they have not, they'll be NACKed)
|
||||
*
|
||||
* @throws DataFormatException if the destination in the SYN was invalid
|
||||
* @throws I2PSessionException if there was an I2P error sending the ACK or NACK
|
||||
*/
|
||||
private void synIncomingAvailable(String id, byte payload[], I2PSession session)
|
||||
throws DataFormatException, I2PSessionException {
|
||||
Destination d = new Destination();
|
||||
d.fromByteArray(payload);
|
||||
|
||||
I2PSocketImpl s = null;
|
||||
boolean acceptConnections = (_serverSocket != null);
|
||||
String newLocalID = null;
|
||||
synchronized (lock) {
|
||||
newLocalID = makeID(_inSockets);
|
||||
if (acceptConnections) {
|
||||
s = new I2PSocketImpl(d, this, false, newLocalID);
|
||||
s.setRemoteID(id);
|
||||
}
|
||||
}
|
||||
_log.debug(getName() + ": *Syn! for socket " + s + " on id " + getReadableForm(newLocalID)
|
||||
+ " from " + d.calculateHash().toBase64().substring(0,6));
|
||||
|
||||
if (!acceptConnections) {
|
||||
// The app did not instantiate an I2PServerSocket
|
||||
byte[] packet = makePacket((byte) CLOSE_OUT, id, toBytes(newLocalID));
|
||||
boolean replySentOk = false;
|
||||
synchronized (_session) {
|
||||
replySentOk = _session.sendMessage(d, packet);
|
||||
}
|
||||
if (!replySentOk) {
|
||||
_log.warn(getName() + ": Error sending close to " + d.calculateHash().toBase64()
|
||||
+ " in response to a new con message",
|
||||
new Exception("Failed creation"));
|
||||
}
|
||||
_context.statManager().addRateData("streaming.nackSent", 1, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_serverSocket.addWaitForAccept(s, _acceptTimeout)) {
|
||||
_inSockets.put(newLocalID, s);
|
||||
byte[] packet = makePacket((byte) ACK, id, toBytes(newLocalID));
|
||||
boolean replySentOk = false;
|
||||
replySentOk = _session.sendMessage(d, packet);
|
||||
if (!replySentOk) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn(getName() + ": Error sending reply to " + d.calculateHash().toBase64()
|
||||
+ " in response to a new con message for socket " + s,
|
||||
new Exception("Failed creation"));
|
||||
s.internalClose();
|
||||
_context.statManager().addRateData("streaming.ackSendFailed", 1, 1);
|
||||
}
|
||||
} else {
|
||||
// timed out or serverSocket closed
|
||||
byte[] packet = toBytes(" " + id);
|
||||
packet[0] = CLOSE_OUT;
|
||||
boolean nackSent = session.sendMessage(d, packet);
|
||||
if (!nackSent) {
|
||||
_log.warn(getName() + ": Error sending NACK for session creation for socket " + s);
|
||||
}
|
||||
s.internalClose();
|
||||
_context.statManager().addRateData("streaming,nackSent", 1, 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* We've received a disconnect for a socket we didn't initiate, so kill
|
||||
* the socket.
|
||||
*
|
||||
*/
|
||||
private void disconnectIncoming(String id, byte payload[]) {
|
||||
I2PSocketImpl s = null;
|
||||
synchronized (lock) {
|
||||
s = (I2PSocketImpl) _inSockets.get(id);
|
||||
if (payload.length == 0 && s != null) {
|
||||
_inSockets.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
_log.debug(getName() + ": *Disconnect incoming for socket " + s);
|
||||
|
||||
try {
|
||||
if (payload.length == 0 && s != null) {
|
||||
s.internalClose();
|
||||
return;
|
||||
} else {
|
||||
if ( (payload.length > 0) && (_log.shouldLog(Log.ERROR)) )
|
||||
_log.warn(getName() + ": Disconnect packet had " + payload.length + " bytes");
|
||||
if (s != null)
|
||||
s.internalClose();
|
||||
return;
|
||||
}
|
||||
} catch (Exception t) {
|
||||
_log.warn(getName() + ": Ignoring error on disconnect", t);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We've received data on a stream we received - toss the data onto
|
||||
* the socket for handling.
|
||||
*
|
||||
* @throws IllegalStateException if the socket isn't open or isn't known
|
||||
*/
|
||||
private void sendIncoming(String id, byte payload[]) throws IllegalStateException {
|
||||
I2PSocketImpl s = null;
|
||||
synchronized (lock) {
|
||||
s = (I2PSocketImpl) _inSockets.get(id);
|
||||
}
|
||||
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(getName() + ": *Packet send incoming [" + payload.length + "] for socket " + s);
|
||||
|
||||
if (s != null) {
|
||||
s.queueData(payload);
|
||||
return;
|
||||
} else {
|
||||
_log.info(getName() + ": Null socket with data available");
|
||||
throw new IllegalStateException("Null socket with data available");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unknown packet. moo.
|
||||
*
|
||||
*/
|
||||
private void handleUnknown(int type, String id, byte payload[]) {
|
||||
_log.error(getName() + ": \n\n=============== Unknown packet! " + "============"
|
||||
+ "\nType: " + (int) type
|
||||
+ "\nID: " + getReadableForm(id)
|
||||
+ "\nBase64'ed Data: " + Base64.encode(payload)
|
||||
+ "\n\n\n");
|
||||
if (id != null) {
|
||||
synchronized (lock) {
|
||||
_inSockets.remove(id);
|
||||
_outSockets.remove(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void reportAbuse(I2PSession session, int severity) {
|
||||
_log.error(getName() + ": Abuse reported [" + severity + "]");
|
||||
}
|
||||
|
||||
public void setDefaultOptions(I2PSocketOptions options) {
|
||||
_defaultOptions = options;
|
||||
}
|
||||
|
||||
public I2PSocketOptions getDefaultOptions() {
|
||||
return _defaultOptions;
|
||||
}
|
||||
|
||||
public I2PSocketOptions buildOptions() { return buildOptions(null); }
|
||||
public I2PSocketOptions buildOptions(Properties opts) {
|
||||
return new I2PSocketOptionsImpl(opts);
|
||||
}
|
||||
|
||||
public I2PServerSocket getServerSocket() {
|
||||
if (_serverSocket == null) {
|
||||
_serverSocket = new I2PServerSocketImpl(this);
|
||||
}
|
||||
return _serverSocket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new connected socket (block until the socket is created)
|
||||
*
|
||||
* @param peer Destination to connect to
|
||||
* @param options I2P socket options to be used for connecting
|
||||
*
|
||||
* @throws ConnectException if the peer refuses the connection
|
||||
* @throws NoRouteToHostException if the peer is not found or not reachable
|
||||
* @throws InterruptedIOException if the connection timeouts
|
||||
* @throws I2PException if there is some other I2P-related problem
|
||||
*/
|
||||
public I2PSocket connect(Destination peer, I2PSocketOptions options)
|
||||
throws I2PException, ConnectException,
|
||||
NoRouteToHostException, InterruptedIOException {
|
||||
String localID, lcID;
|
||||
I2PSocketImpl s;
|
||||
synchronized (lock) {
|
||||
localID = makeID(_outSockets);
|
||||
lcID = getReadableForm(localID);
|
||||
s = new I2PSocketImpl(peer, this, true, localID);
|
||||
_outSockets.put(localID, s);
|
||||
}
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(getName() + ": connect(" + peer.calculateHash().toBase64().substring(0,6)
|
||||
+ ", ...): localID = " + lcID);
|
||||
|
||||
try {
|
||||
ByteArrayOutputStream pubkey = new ByteArrayOutputStream();
|
||||
_session.getMyDestination().writeBytes(pubkey);
|
||||
String remoteID;
|
||||
byte[] packet = makePacket((byte) SYN, localID, pubkey.toByteArray());
|
||||
boolean sent = false;
|
||||
sent = _session.sendMessage(peer, packet);
|
||||
if (!sent) {
|
||||
_log.info(getName() + ": Unable to send & receive ack for SYN packet for socket "
|
||||
+ s + " with localID = " + lcID);
|
||||
synchronized (lock) {
|
||||
_outSockets.remove(s.getLocalID());
|
||||
}
|
||||
_context.statManager().addRateData("streaming.synNoAck", 1, 1);
|
||||
throw new I2PException("Error sending through I2P network");
|
||||
} else {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(getName() + ": syn sent ok to "
|
||||
+ peer.calculateHash().toBase64().substring(0,6)
|
||||
+ " with localID = " + lcID);
|
||||
}
|
||||
if (options != null)
|
||||
remoteID = s.getRemoteID(true, options.getConnectTimeout());
|
||||
else
|
||||
remoteID = s.getRemoteID(true, getDefaultOptions().getConnectTimeout());
|
||||
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(getName() + ": remoteID received from "
|
||||
+ peer.calculateHash().toBase64().substring(0,6)
|
||||
+ ": " + getReadableForm(remoteID)
|
||||
+ " with localID = " + lcID);
|
||||
|
||||
if (remoteID == null) {
|
||||
_context.statManager().addRateData("streaming.nackReceived", 1, 1);
|
||||
throw new ConnectException("Connection refused by peer for socket " + s);
|
||||
}
|
||||
if ("".equals(remoteID)) {
|
||||
_context.statManager().addRateData("streaming.synNoAck", 1, 1);
|
||||
throw new NoRouteToHostException("Unable to reach peer for socket " + s);
|
||||
}
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(getName() + ": TIMING: s given out for remoteID "
|
||||
+ getReadableForm(remoteID) + " for socket " + s);
|
||||
|
||||
return s;
|
||||
} catch (InterruptedIOException ioe) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn(getName() + ": Timeout waiting for ack from syn for id "
|
||||
+ lcID + " to " + peer.calculateHash().toBase64().substring(0,6)
|
||||
+ " for socket " + s, ioe);
|
||||
synchronized (lock) {
|
||||
_outSockets.remove(s.getLocalID());
|
||||
}
|
||||
s.internalClose();
|
||||
_context.statManager().addRateData("streaming.synNoAck", 1, 1);
|
||||
throw new InterruptedIOException("Timeout waiting for ack");
|
||||
} catch (ConnectException ex) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(getName() + ": Connection error waiting for ack from syn for id "
|
||||
+ lcID + " to " + peer.calculateHash().toBase64().substring(0,6)
|
||||
+ " for socket " + s, ex);
|
||||
s.internalClose();
|
||||
throw ex;
|
||||
} catch (NoRouteToHostException ex) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(getName() + ": No route to host waiting for ack from syn for id "
|
||||
+ lcID + " to " + peer.calculateHash().toBase64().substring(0,6)
|
||||
+ " for socket " + s, ex);
|
||||
s.internalClose();
|
||||
throw ex;
|
||||
} catch (IOException ex) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn(getName() + ": Error sending syn on id "
|
||||
+ lcID + " to " + peer.calculateHash().toBase64().substring(0,6)
|
||||
+ " for socket " + s, ex);
|
||||
synchronized (lock) {
|
||||
_outSockets.remove(s.getLocalID());
|
||||
}
|
||||
s.internalClose();
|
||||
throw new I2PException("Unhandled IOException occurred");
|
||||
} catch (I2PException ex) {
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info(getName() + ": Error sending syn on id "
|
||||
+ lcID + " to " + peer.calculateHash().toBase64().substring(0,6)
|
||||
+ " for socket " + s, ex);
|
||||
synchronized (lock) {
|
||||
_outSockets.remove(s.getLocalID());
|
||||
}
|
||||
s.internalClose();
|
||||
throw ex;
|
||||
} catch (Exception e) {
|
||||
s.internalClose();
|
||||
_log.warn(getName() + ": Unhandled error connecting on "
|
||||
+ lcID + " to " + peer.calculateHash().toBase64().substring(0,6)
|
||||
+ " for socket " + s, e);
|
||||
throw new ConnectException("Unhandled error connecting: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new connected socket (block until the socket is created)
|
||||
*
|
||||
* @param peer Destination to connect to
|
||||
*
|
||||
* @throws ConnectException if the peer refuses the connection
|
||||
* @throws NoRouteToHostException if the peer is not found or not reachable
|
||||
* @throws InterruptedIOException if the connection timeouts
|
||||
* @throws I2PException if there is some other I2P-related problem
|
||||
*/
|
||||
public I2PSocket connect(Destination peer) throws I2PException, ConnectException,
|
||||
NoRouteToHostException, InterruptedIOException {
|
||||
return connect(peer, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the socket manager, freeing all the associated resources. This
|
||||
* method will block untill all the managed sockets are closed.
|
||||
*
|
||||
*/
|
||||
public void destroySocketManager() {
|
||||
if (_serverSocket != null) {
|
||||
_serverSocket.close();
|
||||
_serverSocket = null;
|
||||
}
|
||||
|
||||
synchronized (lock) {
|
||||
Iterator iter;
|
||||
String id = null;
|
||||
I2PSocketImpl sock;
|
||||
|
||||
iter = _inSockets.keySet().iterator();
|
||||
while (iter.hasNext()) {
|
||||
id = (String)iter.next();
|
||||
sock = (I2PSocketImpl)_inSockets.get(id);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(getName() + ": Closing inSocket \""
|
||||
+ getReadableForm(sock.getLocalID()) + "\"");
|
||||
sock.internalClose();
|
||||
}
|
||||
|
||||
iter = _outSockets.keySet().iterator();
|
||||
while (iter.hasNext()) {
|
||||
id = (String)iter.next();
|
||||
sock = (I2PSocketImpl)_outSockets.get(id);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(getName() + ": Closing outSocket \""
|
||||
+ getReadableForm(sock.getLocalID()) + "\"");
|
||||
sock.internalClose();
|
||||
}
|
||||
}
|
||||
|
||||
_log.debug(getName() + ": Waiting for all open sockets to really close...");
|
||||
synchronized (lock) {
|
||||
while ((_inSockets.size() != 0) || (_outSockets.size() != 0)) {
|
||||
try {
|
||||
lock.wait();
|
||||
} catch (InterruptedException e) {}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
_log.debug(getName() + ": Destroying I2P session...");
|
||||
_session.destroySession();
|
||||
_log.debug(getName() + ": I2P session destroyed");
|
||||
} catch (I2PSessionException e) {
|
||||
_log.warn(getName() + ": Error destroying I2P session", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a set of currently connected I2PSockets, either initiated locally or remotely.
|
||||
*
|
||||
*/
|
||||
public Set listSockets() {
|
||||
Set sockets = new HashSet(8);
|
||||
synchronized (lock) {
|
||||
sockets.addAll(_inSockets.values());
|
||||
sockets.addAll(_outSockets.values());
|
||||
}
|
||||
return sockets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ping the specified peer, returning true if they replied to the ping within
|
||||
* the timeout specified, false otherwise. This call blocks.
|
||||
*
|
||||
*/
|
||||
public boolean ping(Destination peer, long timeoutMs) {
|
||||
try {
|
||||
return _session.sendMessage(peer, new byte[] { (byte) CHAFF});
|
||||
} catch (I2PException ex) {
|
||||
_log.warn(getName() + ": I2PException:", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void removeSocket(I2PSocketImpl sock) {
|
||||
String localId = sock.getLocalID();
|
||||
boolean removed = false;
|
||||
synchronized (lock) {
|
||||
removed = (null != _inSockets.remove(localId));
|
||||
removed = removed || (null != _outSockets.remove(localId));
|
||||
lock.notify();
|
||||
}
|
||||
|
||||
long now = _context.clock().now();
|
||||
long lifetime = now - sock.getCreatedOn();
|
||||
long timeSinceClose = now - sock.getClosedOn();
|
||||
long sent = sock.getBytesSent();
|
||||
long recv = sock.getBytesReceived();
|
||||
|
||||
if (_log.shouldLog(Log.DEBUG)) {
|
||||
_log.debug(getName() + ": Removing socket \"" + getReadableForm(localId) + "\" [" + sock
|
||||
+ ", send: " + sent + ", recv: " + recv
|
||||
+ ", lifetime: " + lifetime + "ms, time since close: " + timeSinceClose
|
||||
+ " removed? " + removed + ")]",
|
||||
new Exception("removeSocket called"));
|
||||
}
|
||||
|
||||
_context.statManager().addRateData("streaming.lifetime", lifetime, lifetime);
|
||||
_context.statManager().addRateData("streaming.sent", sent, lifetime);
|
||||
_context.statManager().addRateData("streaming.received", recv, lifetime);
|
||||
|
||||
if (sent > recv) {
|
||||
_context.statManager().addRateData("streaming.transferBalance", 1, lifetime);
|
||||
} else if (recv > sent) {
|
||||
_context.statManager().addRateData("streaming.transferBalance", -1, lifetime);
|
||||
} else {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
public String getName() { return _name; }
|
||||
public void setName(String name) { _name = name; }
|
||||
|
||||
public static String getReadableForm(String id) {
|
||||
if (id == null) return "(null)";
|
||||
if (id.length() != 3) return "Bogus";
|
||||
return Base64.encode(toBytes(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new part the connection ID that is locally unique
|
||||
*
|
||||
* @param uniqueIn map of already known local IDs so we don't collide. WARNING - NOT THREADSAFE!
|
||||
*/
|
||||
private static String makeID(HashMap uniqueIn) {
|
||||
String newID;
|
||||
do {
|
||||
int id = (int) (Math.random() * 16777215 + 1);
|
||||
byte[] nid = new byte[3];
|
||||
nid[0] = (byte) (id / 65536);
|
||||
nid[1] = (byte) ((id / 256) % 256);
|
||||
nid[2] = (byte) (id % 256);
|
||||
newID = toString(nid);
|
||||
} while (uniqueIn.get(newID) != null);
|
||||
return newID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new packet of the given type for the specified connection containing
|
||||
* the given payload
|
||||
*/
|
||||
public static byte[] makePacket(byte type, String id, byte[] payload) {
|
||||
byte[] packet = new byte[payload.length + 4];
|
||||
packet[0] = type;
|
||||
byte[] temp = toBytes(id);
|
||||
if (temp.length != 3) throw new RuntimeException("Incorrect ID length: " + temp.length);
|
||||
System.arraycopy(temp, 0, packet, 1, 3);
|
||||
System.arraycopy(payload, 0, packet, 4, payload.length);
|
||||
return packet;
|
||||
}
|
||||
|
||||
private static final String toString(byte data[]) {
|
||||
try {
|
||||
return new String(data, "ISO-8859-1");
|
||||
} catch (UnsupportedEncodingException uee) {
|
||||
throw new RuntimeException("WTF! iso-8859-1 isn't supported?");
|
||||
}
|
||||
}
|
||||
|
||||
private static final byte[] toBytes(String str) {
|
||||
try {
|
||||
return str.getBytes("ISO-8859-1");
|
||||
} catch (UnsupportedEncodingException uee) {
|
||||
throw new RuntimeException("WTF! iso-8859-1 isn't supported?");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +1,43 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* Define the configuration for streaming and verifying data on the socket.
|
||||
*
|
||||
*/
|
||||
public class I2PSocketOptions {
|
||||
private long _connectTimeout;
|
||||
private long _readTimeout;
|
||||
private long _writeTimeout;
|
||||
private int _maxBufferSize;
|
||||
|
||||
public static final int DEFAULT_BUFFER_SIZE = 1024*64;
|
||||
public static final int DEFAULT_WRITE_TIMEOUT = 60*1000;
|
||||
public interface I2PSocketOptions {
|
||||
public static final String PROP_BUFFER_SIZE = "i2p.streaming.bufferSize";
|
||||
public static final String PROP_CONNECT_TIMEOUT = "i2p.streaming.connectTimeout";
|
||||
public static final String PROP_READ_TIMEOUT = "i2p.streaming.readTimeout";
|
||||
public static final String PROP_WRITE_TIMEOUT = "i2p.streaming.writeTimeout";
|
||||
|
||||
public I2PSocketOptions() {
|
||||
_connectTimeout = -1;
|
||||
_readTimeout = -1;
|
||||
_writeTimeout = DEFAULT_WRITE_TIMEOUT;
|
||||
_maxBufferSize = DEFAULT_BUFFER_SIZE;
|
||||
}
|
||||
|
||||
/**
|
||||
* How long we will wait for the ACK from a SYN, in milliseconds.
|
||||
*
|
||||
* @return milliseconds to wait, or -1 if we will wait indefinitely
|
||||
*/
|
||||
public long getConnectTimeout() {
|
||||
return _connectTimeout;
|
||||
}
|
||||
public long getConnectTimeout();
|
||||
|
||||
/**
|
||||
* Define how long we will wait for the ACK from a SYN, in milliseconds.
|
||||
*
|
||||
*/
|
||||
public void setConnectTimeout(long ms) {
|
||||
_connectTimeout = ms;
|
||||
}
|
||||
public void setConnectTimeout(long ms);
|
||||
|
||||
/**
|
||||
* What is the longest we'll block on the input stream while waiting
|
||||
* for more data. If this value is exceeded, the read() throws
|
||||
* InterruptedIOException
|
||||
*/
|
||||
public long getReadTimeout() {
|
||||
return _readTimeout;
|
||||
}
|
||||
public long getReadTimeout();
|
||||
|
||||
/**
|
||||
* What is the longest we'll block on the input stream while waiting
|
||||
* for more data. If this value is exceeded, the read() throws
|
||||
* InterruptedIOException
|
||||
*/
|
||||
public void setReadTimeout(long ms) {
|
||||
_readTimeout = ms;
|
||||
}
|
||||
public void setReadTimeout(long ms);
|
||||
|
||||
/**
|
||||
* How much data will we accept that hasn't been written out yet. After
|
||||
@@ -63,9 +47,7 @@ public class I2PSocketOptions {
|
||||
*
|
||||
* @return buffer size limit, in bytes
|
||||
*/
|
||||
public int getMaxBufferSize() {
|
||||
return _maxBufferSize;
|
||||
}
|
||||
public int getMaxBufferSize();
|
||||
|
||||
/**
|
||||
* How much data will we accept that hasn't been written out yet. After
|
||||
@@ -74,9 +56,7 @@ public class I2PSocketOptions {
|
||||
* less than or equal to zero, there is no limit (warning: can eat ram)
|
||||
*
|
||||
*/
|
||||
public void setMaxBufferSize(int numBytes) {
|
||||
_maxBufferSize = numBytes;
|
||||
}
|
||||
public void setMaxBufferSize(int numBytes);
|
||||
|
||||
/**
|
||||
* What is the longest we'll block on the output stream while waiting
|
||||
@@ -84,9 +64,7 @@ public class I2PSocketOptions {
|
||||
* InterruptedIOException. If this is less than or equal to zero, there
|
||||
* is no timeout.
|
||||
*/
|
||||
public long getWriteTimeout() {
|
||||
return _writeTimeout;
|
||||
}
|
||||
public long getWriteTimeout();
|
||||
|
||||
/**
|
||||
* What is the longest we'll block on the output stream while waiting
|
||||
@@ -94,7 +72,5 @@ public class I2PSocketOptions {
|
||||
* InterruptedIOException. If this is less than or equal to zero, there
|
||||
* is no timeout.
|
||||
*/
|
||||
public void setWriteTimeout(long ms) {
|
||||
_writeTimeout = ms;
|
||||
}
|
||||
public void setWriteTimeout(long ms);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* Define the configuration for streaming and verifying data on the socket.
|
||||
*
|
||||
*/
|
||||
class I2PSocketOptionsImpl implements I2PSocketOptions {
|
||||
private long _connectTimeout;
|
||||
private long _readTimeout;
|
||||
private long _writeTimeout;
|
||||
private int _maxBufferSize;
|
||||
|
||||
public static final int DEFAULT_BUFFER_SIZE = 1024*64;
|
||||
public static final int DEFAULT_WRITE_TIMEOUT = 60*1000;
|
||||
public static final int DEFAULT_CONNECT_TIMEOUT = 60*1000;
|
||||
|
||||
public I2PSocketOptionsImpl() {
|
||||
this(System.getProperties());
|
||||
}
|
||||
|
||||
public I2PSocketOptionsImpl(I2PSocketOptions opts) {
|
||||
this(System.getProperties());
|
||||
if (opts != null) {
|
||||
_connectTimeout = opts.getConnectTimeout();
|
||||
_readTimeout = opts.getReadTimeout();
|
||||
_writeTimeout = opts.getWriteTimeout();
|
||||
_maxBufferSize = opts.getMaxBufferSize();
|
||||
}
|
||||
}
|
||||
|
||||
public I2PSocketOptionsImpl(Properties opts) {
|
||||
init(opts);
|
||||
}
|
||||
|
||||
protected void init(Properties opts) {
|
||||
_maxBufferSize = getInt(opts, PROP_BUFFER_SIZE, DEFAULT_BUFFER_SIZE);
|
||||
_connectTimeout = getInt(opts, PROP_CONNECT_TIMEOUT, DEFAULT_CONNECT_TIMEOUT);
|
||||
_readTimeout = getInt(opts, PROP_READ_TIMEOUT, -1);
|
||||
_writeTimeout = getInt(opts, PROP_WRITE_TIMEOUT, DEFAULT_WRITE_TIMEOUT);
|
||||
}
|
||||
|
||||
protected int getInt(Properties opts, String name, int defaultVal) {
|
||||
if (opts == null) return defaultVal;
|
||||
String val = opts.getProperty(name);
|
||||
if (val == null) {
|
||||
return defaultVal;
|
||||
} else {
|
||||
try {
|
||||
return Integer.parseInt(val);
|
||||
} catch (NumberFormatException nfe) {
|
||||
return defaultVal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* How long we will wait for the ACK from a SYN, in milliseconds.
|
||||
*
|
||||
* @return milliseconds to wait, or -1 if we will wait indefinitely
|
||||
*/
|
||||
public long getConnectTimeout() {
|
||||
return _connectTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define how long we will wait for the ACK from a SYN, in milliseconds.
|
||||
*
|
||||
*/
|
||||
public void setConnectTimeout(long ms) {
|
||||
_connectTimeout = ms;
|
||||
}
|
||||
|
||||
/**
|
||||
* What is the longest we'll block on the input stream while waiting
|
||||
* for more data. If this value is exceeded, the read() throws
|
||||
* InterruptedIOException
|
||||
*/
|
||||
public long getReadTimeout() {
|
||||
return _readTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* What is the longest we'll block on the input stream while waiting
|
||||
* for more data. If this value is exceeded, the read() throws
|
||||
* InterruptedIOException
|
||||
*/
|
||||
public void setReadTimeout(long ms) {
|
||||
_readTimeout = ms;
|
||||
}
|
||||
|
||||
/**
|
||||
* How much data will we accept that hasn't been written out yet. After
|
||||
* this amount has been exceeded, subsequent .write calls will block until
|
||||
* either some data is removed or the connection is closed. If this is
|
||||
* less than or equal to zero, there is no limit (warning: can eat ram)
|
||||
*
|
||||
* @return buffer size limit, in bytes
|
||||
*/
|
||||
public int getMaxBufferSize() {
|
||||
return _maxBufferSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* How much data will we accept that hasn't been written out yet. After
|
||||
* this amount has been exceeded, subsequent .write calls will block until
|
||||
* either some data is removed or the connection is closed. If this is
|
||||
* less than or equal to zero, there is no limit (warning: can eat ram)
|
||||
*
|
||||
*/
|
||||
public void setMaxBufferSize(int numBytes) {
|
||||
_maxBufferSize = numBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* What is the longest we'll block on the output stream while waiting
|
||||
* for the data to flush. If this value is exceeded, the write() throws
|
||||
* InterruptedIOException. If this is less than or equal to zero, there
|
||||
* is no timeout.
|
||||
*/
|
||||
public long getWriteTimeout() {
|
||||
return _writeTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* What is the longest we'll block on the output stream while waiting
|
||||
* for the data to flush. If this value is exceeded, the write() throws
|
||||
* InterruptedIOException. If this is less than or equal to zero, there
|
||||
* is no timeout.
|
||||
*/
|
||||
public void setWriteTimeout(long ms) {
|
||||
_writeTimeout = ms;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import java.io.OutputStream;
|
||||
import java.net.ConnectException;
|
||||
import java.net.NoRouteToHostException;
|
||||
|
||||
import java.util.Properties;
|
||||
import java.util.Random;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
@@ -26,6 +27,8 @@ public class StreamSinkClient {
|
||||
private int _sendSize;
|
||||
private int _writeDelay;
|
||||
private String _peerDestFile;
|
||||
private String _i2cpHost;
|
||||
private int _i2cpPort;
|
||||
|
||||
|
||||
/**
|
||||
@@ -35,6 +38,11 @@ public class StreamSinkClient {
|
||||
* @param serverDestFile file containing the StreamSinkServer's binary Destination
|
||||
*/
|
||||
public StreamSinkClient(int sendSize, int writeDelayMs, String serverDestFile) {
|
||||
this(null, -1, sendSize, writeDelayMs, serverDestFile);
|
||||
}
|
||||
public StreamSinkClient(String i2cpHost, int i2cpPort, int sendSize, int writeDelayMs, String serverDestFile) {
|
||||
_i2cpHost = i2cpHost;
|
||||
_i2cpPort = i2cpPort;
|
||||
_sendSize = sendSize;
|
||||
_writeDelay = writeDelayMs;
|
||||
_peerDestFile = serverDestFile;
|
||||
@@ -46,7 +54,11 @@ public class StreamSinkClient {
|
||||
*
|
||||
*/
|
||||
public void runClient() {
|
||||
I2PSocketManager mgr = I2PSocketManagerFactory.createManager();
|
||||
I2PSocketManager mgr = null;
|
||||
if (_i2cpHost != null)
|
||||
mgr = I2PSocketManagerFactory.createManager(_i2cpHost, _i2cpPort, new Properties());
|
||||
else
|
||||
mgr = I2PSocketManagerFactory.createManager();
|
||||
Destination peer = null;
|
||||
FileInputStream fis = null;
|
||||
try {
|
||||
@@ -81,9 +93,9 @@ public class StreamSinkClient {
|
||||
try { Thread.sleep(_writeDelay); } catch (InterruptedException ie) {}
|
||||
}
|
||||
}
|
||||
sock.close();
|
||||
long afterSending = System.currentTimeMillis();
|
||||
System.out.println("Sent " + _sendSize + "KB in " + (afterSending-beforeSending) + "ms");
|
||||
sock.close();
|
||||
} catch (InterruptedIOException iie) {
|
||||
_log.error("Timeout connecting to the peer", iie);
|
||||
return;
|
||||
@@ -103,7 +115,7 @@ public class StreamSinkClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire up the client. <code>Usage: StreamSinkClient sendSizeKB writeDelayMs serverDestFile</code> <br />
|
||||
* Fire up the client. <code>Usage: StreamSinkClient [i2cpHost i2cpPort] sendSizeKB writeDelayMs serverDestFile</code> <br />
|
||||
* <ul>
|
||||
* <li><b>sendSizeKB</b>: how many KB to send</li>
|
||||
* <li><b>writeDelayMs</b>: how long to wait between each .write (0 for no delay)</li>
|
||||
@@ -111,25 +123,40 @@ public class StreamSinkClient {
|
||||
* </ul>
|
||||
*/
|
||||
public static void main(String args[]) {
|
||||
if (args.length != 3) {
|
||||
System.out.println("Usage: StreamSinkClient sendSizeKB writeDelayMs serverDestFile");
|
||||
} else {
|
||||
int sendSizeKB = -1;
|
||||
int writeDelayMs = -1;
|
||||
try {
|
||||
sendSizeKB = Integer.parseInt(args[0]);
|
||||
} catch (NumberFormatException nfe) {
|
||||
System.err.println("Send size invalid [" + args[0] + "]");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
writeDelayMs = Integer.parseInt(args[1]);
|
||||
} catch (NumberFormatException nfe) {
|
||||
System.err.println("Write delay ms invalid [" + args[1] + "]");
|
||||
return;
|
||||
}
|
||||
StreamSinkClient client = new StreamSinkClient(sendSizeKB, writeDelayMs, args[2]);
|
||||
client.runClient();
|
||||
StreamSinkClient client = null;
|
||||
int sendSizeKB = -1;
|
||||
int writeDelayMs = -1;
|
||||
|
||||
switch (args.length) {
|
||||
case 3:
|
||||
try {
|
||||
sendSizeKB = Integer.parseInt(args[0]);
|
||||
} catch (NumberFormatException nfe) {
|
||||
System.err.println("Send size invalid [" + args[0] + "]");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
writeDelayMs = Integer.parseInt(args[1]);
|
||||
} catch (NumberFormatException nfe) {
|
||||
System.err.println("Write delay ms invalid [" + args[1] + "]");
|
||||
return;
|
||||
}
|
||||
client = new StreamSinkClient(sendSizeKB, writeDelayMs, args[2]);
|
||||
break;
|
||||
case 5:
|
||||
try {
|
||||
int port = Integer.parseInt(args[1]);
|
||||
sendSizeKB = Integer.parseInt(args[2]);
|
||||
writeDelayMs = Integer.parseInt(args[3]);
|
||||
client = new StreamSinkClient(args[0], port, sendSizeKB, writeDelayMs, args[4]);
|
||||
} catch (NumberFormatException nfe) {
|
||||
System.err.println("arg error");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
System.out.println("Usage: StreamSinkClient [i2cpHost i2cpPort] sendSizeKB writeDelayMs serverDestFile");
|
||||
}
|
||||
if (client != null)
|
||||
client.runClient();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import java.net.ConnectException;
|
||||
import java.net.NoRouteToHostException;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.I2PException;
|
||||
import net.i2p.data.Destination;
|
||||
import net.i2p.data.DataFormatException;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Simple streaming lib test app that connects to a given destination and sends
|
||||
* the contents of a file, then disconnects. See the {@link #main}
|
||||
*
|
||||
*/
|
||||
public class StreamSinkSend {
|
||||
private Log _log;
|
||||
private String _sendFile;
|
||||
private int _writeDelay;
|
||||
private String _peerDestFile;
|
||||
|
||||
/**
|
||||
* Build the client but don't fire it up.
|
||||
* @param filename file to send
|
||||
* @param writeDelayMs how long to wait between each .write (0 for no delay)
|
||||
* @param serverDestFile file containing the StreamSinkServer's binary Destination
|
||||
*/
|
||||
public StreamSinkSend(String filename, int writeDelayMs, String serverDestFile) {
|
||||
_sendFile = filename;
|
||||
_writeDelay = writeDelayMs;
|
||||
_peerDestFile = serverDestFile;
|
||||
_log = I2PAppContext.getGlobalContext().logManager().getLog(StreamSinkClient.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually connect and run the client - this call blocks until completion.
|
||||
*
|
||||
*/
|
||||
public void runClient() {
|
||||
I2PSocketManager mgr = I2PSocketManagerFactory.createManager();
|
||||
Destination peer = null;
|
||||
FileInputStream fis = null;
|
||||
try {
|
||||
fis = new FileInputStream(_peerDestFile);
|
||||
peer = new Destination();
|
||||
peer.readBytes(fis);
|
||||
} catch (IOException ioe) {
|
||||
_log.error("Error finding the peer destination to contact in " + _peerDestFile, ioe);
|
||||
return;
|
||||
} catch (DataFormatException dfe) {
|
||||
_log.error("Peer destination is not valid in " + _peerDestFile, dfe);
|
||||
return;
|
||||
} finally {
|
||||
if (fis != null) try { fis.close(); } catch (IOException ioe) {}
|
||||
}
|
||||
|
||||
|
||||
System.out.println("Send " + _sendFile + " to " + peer.calculateHash().toBase64());
|
||||
|
||||
try {
|
||||
I2PSocket sock = mgr.connect(peer);
|
||||
byte buf[] = new byte[32*1024];
|
||||
OutputStream out = sock.getOutputStream();
|
||||
long beforeSending = System.currentTimeMillis();
|
||||
fis = new FileInputStream(_sendFile);
|
||||
long size = 0;
|
||||
while (true) {
|
||||
int read = fis.read(buf);
|
||||
if (read < 0)
|
||||
break;
|
||||
out.write(buf, 0, read);
|
||||
size += read;
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Wrote " + read);
|
||||
if (_writeDelay > 0) {
|
||||
try { Thread.sleep(_writeDelay); } catch (InterruptedException ie) {}
|
||||
}
|
||||
}
|
||||
fis.close();
|
||||
sock.close();
|
||||
long afterSending = System.currentTimeMillis();
|
||||
System.out.println("Sent " + (size / 1024) + "KB in " + (afterSending-beforeSending) + "ms");
|
||||
} catch (InterruptedIOException iie) {
|
||||
_log.error("Timeout connecting to the peer", iie);
|
||||
return;
|
||||
} catch (NoRouteToHostException nrthe) {
|
||||
_log.error("Unable to connect to the peer", nrthe);
|
||||
return;
|
||||
} catch (ConnectException ce) {
|
||||
_log.error("Connection already dropped", ce);
|
||||
return;
|
||||
} catch (I2PException ie) {
|
||||
_log.error("Error connecting to the peer", ie);
|
||||
return;
|
||||
} catch (IOException ioe) {
|
||||
_log.error("IO error sending", ioe);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire up the client. <code>Usage: StreamSinkClient sendFile writeDelayMs serverDestFile</code> <br />
|
||||
* <ul>
|
||||
* <li><b>sendFile</b>: filename to send</li>
|
||||
* <li><b>writeDelayMs</b>: how long to wait between each .write (0 for no delay)</li>
|
||||
* <li><b>serverDestFile</b>: file containing the StreamSinkServer's binary Destination</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static void main(String args[]) {
|
||||
if (args.length != 3) {
|
||||
System.out.println("Usage: StreamSinkClient sendFile writeDelayMs serverDestFile");
|
||||
} else {
|
||||
int writeDelayMs = -1;
|
||||
try {
|
||||
writeDelayMs = Integer.parseInt(args[1]);
|
||||
} catch (NumberFormatException nfe) {
|
||||
System.err.println("Write delay ms invalid [" + args[1] + "]");
|
||||
return;
|
||||
}
|
||||
StreamSinkSend client = new StreamSinkSend(args[0], writeDelayMs, args[2]);
|
||||
client.runClient();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import java.net.ConnectException;
|
||||
import java.util.Properties;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.I2PException;
|
||||
@@ -23,6 +24,8 @@ public class StreamSinkServer {
|
||||
private Log _log;
|
||||
private String _sinkDir;
|
||||
private String _destFile;
|
||||
private String _i2cpHost;
|
||||
private int _i2cpPort;
|
||||
|
||||
/**
|
||||
* Create but do not start the streaming server.
|
||||
@@ -31,8 +34,13 @@ public class StreamSinkServer {
|
||||
* @param ourDestFile filename to write our binary destination to
|
||||
*/
|
||||
public StreamSinkServer(String sinkDir, String ourDestFile) {
|
||||
this(sinkDir, ourDestFile, null, -1);
|
||||
}
|
||||
public StreamSinkServer(String sinkDir, String ourDestFile, String i2cpHost, int i2cpPort) {
|
||||
_sinkDir = sinkDir;
|
||||
_destFile = ourDestFile;
|
||||
_i2cpHost = i2cpHost;
|
||||
_i2cpPort = i2cpPort;
|
||||
_log = I2PAppContext.getGlobalContext().logManager().getLog(StreamSinkServer.class);
|
||||
}
|
||||
|
||||
@@ -42,7 +50,11 @@ public class StreamSinkServer {
|
||||
*
|
||||
*/
|
||||
public void runServer() {
|
||||
I2PSocketManager mgr = I2PSocketManagerFactory.createManager();
|
||||
I2PSocketManager mgr = null;
|
||||
if (_i2cpHost != null)
|
||||
mgr = I2PSocketManagerFactory.createManager(_i2cpHost, _i2cpPort, new Properties());
|
||||
else
|
||||
mgr = I2PSocketManagerFactory.createManager();
|
||||
Destination dest = mgr.getSession().getMyDestination();
|
||||
System.out.println("Listening for connections on: " + dest.calculateHash().toBase64());
|
||||
FileOutputStream fos = null;
|
||||
@@ -95,6 +107,7 @@ public class StreamSinkServer {
|
||||
sink.mkdirs();
|
||||
File cur = File.createTempFile("clientSink", ".dat", sink);
|
||||
_fos = new FileOutputStream(cur);
|
||||
System.out.println("Writing to " + cur.getAbsolutePath());
|
||||
} catch (IOException ioe) {
|
||||
_log.error("Error creating sink", ioe);
|
||||
_fos = null;
|
||||
@@ -105,31 +118,53 @@ public class StreamSinkServer {
|
||||
try {
|
||||
InputStream in = _sock.getInputStream();
|
||||
byte buf[] = new byte[4096];
|
||||
long written = 0;
|
||||
int read = 0;
|
||||
while ( (read = in.read(buf)) != -1) {
|
||||
_fos.write(buf, 0, read);
|
||||
written += read;
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("read and wrote " + read);
|
||||
}
|
||||
_log.error("Got EOF from client socket [written=" + written + "]");
|
||||
} catch (IOException ioe) {
|
||||
_log.error("Error writing the sink", ioe);
|
||||
} finally {
|
||||
if (_fos != null) try { _fos.close(); } catch (IOException ioe) {}
|
||||
if (_sock != null) try { _sock.close(); } catch (IOException ioe) {}
|
||||
_log.error("Client socket closed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire up the streaming server. <code>Usage: StreamSinkServer sinkDir ourDestFile</code><br />
|
||||
* Fire up the streaming server. <code>Usage: StreamSinkServer [i2cpHost i2cpPort] sinkDir ourDestFile</code><br />
|
||||
* <ul>
|
||||
* <li><b>sinkDir</b>: Directory to store received files in</li>
|
||||
* <li><b>ourDestFile</b>: filename to write our binary destination to</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static void main(String args[]) {
|
||||
if (args.length != 2) {
|
||||
System.out.println("Usage: StreamSinkServer sinkDir ourDestFile");
|
||||
} else {
|
||||
StreamSinkServer server = new StreamSinkServer(args[0], args[1]);
|
||||
server.runServer();
|
||||
StreamSinkServer server = null;
|
||||
switch (args.length) {
|
||||
case 0:
|
||||
server = new StreamSinkServer("dataDir", "server.key", "localhost", 10001);
|
||||
break;
|
||||
case 2:
|
||||
server = new StreamSinkServer(args[0], args[1]);
|
||||
break;
|
||||
case 4:
|
||||
try {
|
||||
int port = Integer.parseInt(args[1]);
|
||||
server = new StreamSinkServer(args[2], args[3], args[0], port);
|
||||
} catch (NumberFormatException nfe) {
|
||||
System.out.println("Usage: StreamSinkServer [i2cpHost i2cpPort] sinkDir ourDestFile");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
System.out.println("Usage: StreamSinkServer [i2cpHost i2cpPort] sinkDir ourDestFile");
|
||||
}
|
||||
if (server != null)
|
||||
server.runServer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,11 +206,11 @@ public class NetMonitor {
|
||||
}
|
||||
|
||||
/** drop all the old summary data */
|
||||
public void coallesceData() {
|
||||
public void coalesceData() {
|
||||
synchronized (_peerSummaries) {
|
||||
for (Iterator iter = _peerSummaries.values().iterator(); iter.hasNext(); ) {
|
||||
PeerSummary summary = (PeerSummary)iter.next();
|
||||
summary.coallesceData(_summaryDurationHours * 60*60*1000);
|
||||
summary.coalesceData(_summaryDurationHours * 60*60*1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ class NetMonitorRunner implements Runnable {
|
||||
long nextExport = now + _monitor.getExportDelay() * 1000;
|
||||
while (_monitor.isRunning()) {
|
||||
now = Clock.getInstance().now();
|
||||
_monitor.coallesceData();
|
||||
_monitor.coalesceData();
|
||||
if (now >= nextHarvest) {
|
||||
runHarvest();
|
||||
nextHarvest = now + _monitor.getHarvestDelay() * 1000;
|
||||
|
||||
@@ -22,7 +22,7 @@ public class PeerSummary {
|
||||
/** statName to a List of PeerStat elements (sorted by sample date, earliest first) */
|
||||
private Map _stats;
|
||||
/** lock on this when accessing stat data */
|
||||
private Object _coallesceLock = new Object();
|
||||
private Object _coalesceLock = new Object();
|
||||
|
||||
public PeerSummary(String peer) {
|
||||
_peer = peer;
|
||||
@@ -38,7 +38,7 @@ public class PeerSummary {
|
||||
* @param val actual data harvested
|
||||
*/
|
||||
public void addData(String stat, String description, String valueDescriptions[], long when, double val[]) {
|
||||
synchronized (_coallesceLock) {
|
||||
synchronized (_coalesceLock) {
|
||||
TreeMap stats = locked_getData(stat);
|
||||
stats.put(new Long(when), new PeerStat(stat, description, valueDescriptions, when, val));
|
||||
}
|
||||
@@ -53,7 +53,7 @@ public class PeerSummary {
|
||||
* @param val actual data harvested
|
||||
*/
|
||||
public void addData(String stat, String description, String valueDescriptions[], long when, long val[]) {
|
||||
synchronized (_coallesceLock) {
|
||||
synchronized (_coalesceLock) {
|
||||
TreeMap stats = locked_getData(stat);
|
||||
stats.put(new Long(when), new PeerStat(stat, description, valueDescriptions, when, val));
|
||||
}
|
||||
@@ -68,7 +68,7 @@ public class PeerSummary {
|
||||
*
|
||||
*/
|
||||
public List getData(String statName) {
|
||||
synchronized (_coallesceLock) {
|
||||
synchronized (_coalesceLock) {
|
||||
return new ArrayList(((TreeMap)_stats.get(statName)).values());
|
||||
}
|
||||
}
|
||||
@@ -78,21 +78,21 @@ public class PeerSummary {
|
||||
*
|
||||
*/
|
||||
public Set getStatNames() {
|
||||
synchronized (_coallesceLock) {
|
||||
synchronized (_coalesceLock) {
|
||||
return new HashSet(_stats.keySet());
|
||||
}
|
||||
}
|
||||
|
||||
/** drop old data points */
|
||||
public void coallesceData(long summaryDurationMs) {
|
||||
public void coalesceData(long summaryDurationMs) {
|
||||
long earliest = Clock.getInstance().now() - summaryDurationMs;
|
||||
synchronized (_coallesceLock) {
|
||||
locked_coallesce(earliest);
|
||||
synchronized (_coalesceLock) {
|
||||
locked_coalesce(earliest);
|
||||
}
|
||||
}
|
||||
|
||||
/** go through all the stats and remove ones from before the given date */
|
||||
private void locked_coallesce(long earliestSampleDate) {
|
||||
private void locked_coalesce(long earliestSampleDate) {
|
||||
if (true) return;
|
||||
for (Iterator iter = _stats.keySet().iterator(); iter.hasNext(); ) {
|
||||
String statName = (String)iter.next();
|
||||
@@ -116,4 +116,4 @@ public class PeerSummary {
|
||||
_stats.put(statName, new TreeMap());
|
||||
return (TreeMap)_stats.get(statName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ class PeerSummaryReader {
|
||||
}
|
||||
if (summary == null)
|
||||
return;
|
||||
summary.coallesceData(monitor.getSummaryDurationHours() * 60*60*1000);
|
||||
summary.coalesceData(monitor.getSummaryDurationHours() * 60*60*1000);
|
||||
monitor.addSummary(summary);
|
||||
}
|
||||
|
||||
@@ -103,4 +103,4 @@ class PeerSummaryReader {
|
||||
return _fmt.parse(when).getTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<pathelement location="../../../core/java/build/i2p.jar" />
|
||||
<pathelement location="../../../router/java/build/router.jar" />
|
||||
<pathelement location="../../jetty/jettylib/org.mortbay.jetty-jdk1.2.jar" />
|
||||
<pathelement location="../../jetty/jettylib/javax.servlet.jar" />
|
||||
<pathelement location="../../systray/java/build/systray.jar" />
|
||||
<pathelement location="../../systray/java/lib/systray4j.jar" />
|
||||
<pathelement location="../../../installer/lib/wrapper/win32/wrapper.jar" /> <!-- we dont care if we're not on win32 -->
|
||||
@@ -54,6 +55,7 @@
|
||||
<pathelement location="../../jetty/jettylib/javax.servlet.jar" />
|
||||
<pathelement location="../../jetty/jettylib/ant.jar" />
|
||||
<pathelement location="../../systray/java/build/obj" />
|
||||
<pathelement location="../../systray/java/lib/systray4j.jar" /> <!-- some javacs resolve recursively... -->
|
||||
<pathelement location="../../../installer/lib/wrapper/win32/wrapper.jar" /> <!-- we dont care if we're not on win32 -->
|
||||
<pathelement location="build/routerconsole.jar" />
|
||||
</classpath>
|
||||
|
||||
@@ -11,6 +11,7 @@ public class ConfigClientsHandler extends FormHandler {
|
||||
private String _numClients;
|
||||
private String _numTunnels;
|
||||
private String _numHops;
|
||||
private String _numHopsOutbound;
|
||||
private boolean _shouldSave;
|
||||
|
||||
public void ConfigNetHandler() {
|
||||
@@ -36,6 +37,9 @@ public class ConfigClientsHandler extends FormHandler {
|
||||
public void setTunneldepth(String num) {
|
||||
_numHops = (num != null ? num.trim() : null);
|
||||
}
|
||||
public void setTunneldepthoutbound(String num) {
|
||||
_numHopsOutbound = (num != null ? num.trim() : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* The user made changes to the network config and wants to save them, so
|
||||
@@ -63,6 +67,12 @@ public class ConfigClientsHandler extends FormHandler {
|
||||
saveRequired = true;
|
||||
}
|
||||
|
||||
if ( (_numHopsOutbound != null) && (_numHopsOutbound.length() > 0) ) {
|
||||
_context.router().setConfigSetting(ClientTunnelSettings.PROP_DEPTH_OUTBOUND, _numHopsOutbound);
|
||||
addFormNotice("Updating default outbound tunnel length to " + _numHopsOutbound);
|
||||
saveRequired = true;
|
||||
}
|
||||
|
||||
if (saveRequired) {
|
||||
boolean saved = _context.router().saveConfig();
|
||||
if (saved)
|
||||
|
||||
@@ -114,4 +114,31 @@ public class ConfigClientsHelper {
|
||||
buf.append("</select>\n");
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
public String getTunnelDepthOutboundSelectBox() {
|
||||
int count = ClientTunnelSettings.DEFAULT_DEPTH_OUTBOUND;
|
||||
String val = _context.router().getConfigSetting(ClientTunnelSettings.PROP_DEPTH_OUTBOUND);
|
||||
if (val != null) {
|
||||
try {
|
||||
count = Integer.parseInt(val);
|
||||
} catch (NumberFormatException nfe) {
|
||||
// ignore, use default from above
|
||||
}
|
||||
}
|
||||
StringBuffer buf = new StringBuffer(1024);
|
||||
buf.append("<select name=\"tunneldepthoutbound\">\n");
|
||||
for (int i = 0; i < 4; i++) {
|
||||
buf.append("<option value=\"").append(i).append("\" ");
|
||||
if (count == i)
|
||||
buf.append("selected=\"true\" ");
|
||||
buf.append(">").append(i).append("</option>\n");
|
||||
}
|
||||
if (count >= 4) {
|
||||
buf.append("<option value=\"").append(count);
|
||||
buf.append("\" selected>").append(count);
|
||||
buf.append("</option>\n");
|
||||
}
|
||||
buf.append("</select>\n");
|
||||
return buf.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,7 +217,10 @@ public class ConfigNetHandler extends FormHandler {
|
||||
}
|
||||
if ( (_port != null) && (_port.length() > 0) ) {
|
||||
String oldPort = _context.router().getConfigSetting(ConfigNetHelper.PROP_I2NP_TCP_PORT);
|
||||
if ( (oldPort == null) || (!oldPort.equalsIgnoreCase(_port)) ) {
|
||||
if ( (oldPort == null) && (_port.equals("8887")) ) {
|
||||
// still on default.. noop
|
||||
} else if ( (oldPort == null) || (!oldPort.equalsIgnoreCase(_port)) ) {
|
||||
// its not the default OR it has changed
|
||||
_context.router().setConfigSetting(ConfigNetHelper.PROP_I2NP_TCP_PORT, _port);
|
||||
addFormNotice("Updating TCP port from " + oldPort + " to " + _port);
|
||||
restartRequired = true;
|
||||
|
||||
@@ -120,15 +120,12 @@ public class ConfigNetHelper {
|
||||
private static String getBurstFactor(int numSeconds, String name) {
|
||||
StringBuffer buf = new StringBuffer(256);
|
||||
buf.append("<select name=\"").append(name).append("\">\n");
|
||||
for (int i = 1; i < 10; i++) {
|
||||
for (int i = 10; i <= 60; i += 10) {
|
||||
buf.append("<option value=\"").append(i).append("\" ");
|
||||
if ( (i == numSeconds) || (i == 10) )
|
||||
if ( (i == numSeconds) || (i == 60) )
|
||||
buf.append("selected ");
|
||||
buf.append(">");
|
||||
if (i == 1)
|
||||
buf.append("1 second (no burst)</option>\n");
|
||||
else
|
||||
buf.append(i).append(" seconds</option>\n");
|
||||
buf.append(i).append(" seconds</option>\n");
|
||||
}
|
||||
buf.append("</select>\n");
|
||||
return buf.toString();
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
package net.i2p.router.web;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
import java.util.Properties;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.router.ClientTunnelSettings;
|
||||
import net.i2p.router.Router;
|
||||
import net.i2p.apps.systray.SysTray;
|
||||
import net.i2p.apps.systray.UrlLauncher;
|
||||
import org.tanukisoftware.wrapper.WrapperManager;
|
||||
|
||||
/**
|
||||
@@ -86,6 +93,12 @@ public class ConfigServiceHandler extends FormHandler {
|
||||
} catch (Throwable t) {
|
||||
addFormError("Warning: unable to contact the systray manager - " + t.getMessage());
|
||||
}
|
||||
} else if ("View console on startup".equals(_action)) {
|
||||
browseOnStartup(true);
|
||||
addFormNotice("Console is to be shown on startup");
|
||||
} else if ("Do not view console on startup".equals(_action)) {
|
||||
browseOnStartup(false);
|
||||
addFormNotice("Console is not to be shown on startup");
|
||||
} else {
|
||||
addFormNotice("Blah blah blah. whatever. I'm not going to " + _action);
|
||||
}
|
||||
@@ -107,4 +120,81 @@ public class ConfigServiceHandler extends FormHandler {
|
||||
addFormError("Warning: unable to remove the service - " + ioe.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private final static String NL = System.getProperty("line.separator");
|
||||
private void browseOnStartup(boolean shouldLaunchBrowser) {
|
||||
File f = new File("clients.config");
|
||||
Properties p = new Properties();
|
||||
try {
|
||||
DataHelper.loadProps(p, f);
|
||||
|
||||
int i = 0;
|
||||
int launchIndex = -1;
|
||||
while (true) {
|
||||
String className = p.getProperty("clientApp." + i + ".main");
|
||||
if (className == null) break;
|
||||
if (UrlLauncher.class.getName().equals(className)) {
|
||||
launchIndex = i;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
if ((launchIndex >= 0) && shouldLaunchBrowser)
|
||||
return;
|
||||
if ((launchIndex < 0) && !shouldLaunchBrowser)
|
||||
return;
|
||||
|
||||
if (shouldLaunchBrowser) {
|
||||
p.setProperty("clientApp." + i + ".main", UrlLauncher.class.getName());
|
||||
p.setProperty("clientApp." + i + ".name", "BrowserLauncher");
|
||||
p.setProperty("clientApp." + i + ".args", "http://localhost:7657/index.jsp");
|
||||
p.setProperty("clientApp." + i + ".delay", "5");
|
||||
} else {
|
||||
p.remove("clientApp." + launchIndex + ".main");
|
||||
p.remove("clientApp." + launchIndex + ".name");
|
||||
p.remove("clientApp." + launchIndex + ".args");
|
||||
p.remove("clientApp." + launchIndex + ".onBoot");
|
||||
p.remove("clientApp." + launchIndex + ".delay");
|
||||
|
||||
i = launchIndex + 1;
|
||||
while (true) {
|
||||
String main = p.getProperty("clientApp." + i + ".main");
|
||||
String name = p.getProperty("clientApp." + i + ".name");
|
||||
String args = p.getProperty("clientApp." + i + ".args");
|
||||
String boot = p.getProperty("clientApp." + i + ".onBoot");
|
||||
String delay= p.getProperty("clientApp." + i + ".delay");
|
||||
|
||||
if (main == null) break;
|
||||
|
||||
p.setProperty("clientApp." + (i-1) + ".main", main);
|
||||
p.setProperty("clientApp." + (i-1) + ".name", name);
|
||||
p.setProperty("clientApp." + (i-1) + ".args", args);
|
||||
if (boot != null)
|
||||
p.setProperty("clientApp." + (i-1) + ".onBoot", boot);
|
||||
if (delay != null)
|
||||
p.setProperty("clientApp." + (i-1) + ".delay", delay);
|
||||
|
||||
p.remove("clientApp." + i + ".main");
|
||||
p.remove("clientApp." + i + ".name");
|
||||
p.remove("clientApp." + i + ".args");
|
||||
p.remove("clientApp." + i + ".onBoot");
|
||||
p.remove("clientApp." + i + ".delay");
|
||||
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
TreeMap sorted = new TreeMap(p);
|
||||
FileWriter out = new FileWriter(f);
|
||||
for (Iterator iter = sorted.keySet().iterator(); iter.hasNext(); ) {
|
||||
String name = (String)iter.next();
|
||||
String val = (String)sorted.get(name);
|
||||
out.write(name + "=" + val + NL);
|
||||
}
|
||||
out.close();
|
||||
} catch (IOException ioe) {
|
||||
addFormError("Error updating the client config");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import net.i2p.util.FileUtil;
|
||||
public class ContentHelper {
|
||||
private String _page;
|
||||
private int _maxLines;
|
||||
private boolean _startAtBeginning;
|
||||
private RouterContext _context;
|
||||
/**
|
||||
* Configure this bean to query a particular router context
|
||||
@@ -28,6 +29,10 @@ public class ContentHelper {
|
||||
public ContentHelper() {}
|
||||
|
||||
public void setPage(String page) { _page = page; }
|
||||
public void setStartAtBeginning(String moo) {
|
||||
_startAtBeginning = Boolean.valueOf(""+moo).booleanValue();
|
||||
}
|
||||
|
||||
public void setMaxLines(String lines) {
|
||||
if (lines != null) {
|
||||
try {
|
||||
@@ -40,14 +45,14 @@ public class ContentHelper {
|
||||
}
|
||||
}
|
||||
public String getContent() {
|
||||
String str = FileUtil.readTextFile(_page, _maxLines);
|
||||
String str = FileUtil.readTextFile(_page, _maxLines, _startAtBeginning);
|
||||
if (str == null)
|
||||
return "";
|
||||
else
|
||||
return str;
|
||||
}
|
||||
public String getTextContent() {
|
||||
String str = FileUtil.readTextFile(_page, _maxLines);
|
||||
String str = FileUtil.readTextFile(_page, _maxLines, _startAtBeginning);
|
||||
if (str == null)
|
||||
return "";
|
||||
else
|
||||
|
||||
@@ -42,7 +42,7 @@ public class LogsHelper {
|
||||
}
|
||||
|
||||
public String getServiceLogs() {
|
||||
String str = FileUtil.readTextFile("wrapper.log", 500);
|
||||
String str = FileUtil.readTextFile("wrapper.log", 500, false);
|
||||
if (str == null)
|
||||
return "";
|
||||
else
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package net.i2p.router.web;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.util.I2PThread;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Handler to deal with reseed requests. This reseed from the URL
|
||||
* http://dev.i2p.net/i2pdb/ unless the java env property "i2p.reseedURL" is
|
||||
* set. It always writes to ./netDb/, so don't mess with that.
|
||||
*
|
||||
*/
|
||||
public class ReseedHandler {
|
||||
private static ReseedRunner _reseedRunner = new ReseedRunner();
|
||||
|
||||
public void setReseedNonce(String nonce) {
|
||||
if (nonce == null) return;
|
||||
if (nonce.equals(System.getProperty("net.i2p.router.web.ReseedHandler.nonce")) ||
|
||||
nonce.equals(System.getProperty("net.i2p.router.web.ReseedHandler.noncePrev"))) {
|
||||
synchronized (_reseedRunner) {
|
||||
if (_reseedRunner.isRunning()) {
|
||||
return;
|
||||
} else {
|
||||
System.setProperty("net.i2p.router.web.ReseedHandler.reseedInProgress", "true");
|
||||
I2PThread reseed = new I2PThread(_reseedRunner, "Reseed");
|
||||
reseed.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class ReseedRunner implements Runnable {
|
||||
private boolean _isRunning;
|
||||
public ReseedRunner() { _isRunning = false; }
|
||||
public boolean isRunning() { return _isRunning; }
|
||||
public void run() {
|
||||
_isRunning = true;
|
||||
reseed();
|
||||
System.setProperty("net.i2p.router.web.ReseedHandler.reseedInProgress", "false");
|
||||
_isRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static final String DEFAULT_SEED_URL = "http://dev.i2p.net/i2pdb/";
|
||||
/**
|
||||
* Reseed has been requested, so lets go ahead and do it. Fetch all of
|
||||
* the routerInfo-*.dat files from the specified URL (or the default) and
|
||||
* save them into this router's netDb dir.
|
||||
*
|
||||
*/
|
||||
private static void reseed() {
|
||||
String seedURL = System.getProperty("i2p.reseedURL", DEFAULT_SEED_URL);
|
||||
if ( (seedURL == null) || (seedURL.trim().length() <= 0) )
|
||||
seedURL = DEFAULT_SEED_URL;
|
||||
try {
|
||||
URL dir = new URL(seedURL);
|
||||
String content = new String(readURL(dir));
|
||||
Set urls = new HashSet();
|
||||
int cur = 0;
|
||||
while (true) {
|
||||
int start = content.indexOf("href=\"routerInfo-", cur);
|
||||
if (start < 0)
|
||||
break;
|
||||
|
||||
int end = content.indexOf(".dat\">", start);
|
||||
String name = content.substring(start+"href=\"routerInfo-".length(), end);
|
||||
urls.add(name);
|
||||
cur = end + 1;
|
||||
}
|
||||
|
||||
int fetched = 0;
|
||||
int errors = 0;
|
||||
for (Iterator iter = urls.iterator(); iter.hasNext(); ) {
|
||||
try {
|
||||
fetchSeed(seedURL, (String)iter.next());
|
||||
fetched++;
|
||||
} catch (Exception e) {
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
I2PAppContext.getGlobalContext().logManager().getLog(ReseedHandler.class).error("Error reseeding", t);
|
||||
}
|
||||
}
|
||||
|
||||
private static void fetchSeed(String seedURL, String peer) throws Exception {
|
||||
URL url = new URL(seedURL + (seedURL.endsWith("/") ? "" : "/") + "routerInfo-" + peer + ".dat");
|
||||
|
||||
byte data[] = readURL(url);
|
||||
writeSeed(peer, data);
|
||||
}
|
||||
|
||||
private static byte[] readURL(URL url) throws Exception {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
|
||||
URLConnection con = url.openConnection();
|
||||
InputStream in = con.getInputStream();
|
||||
byte buf[] = new byte[1024];
|
||||
while (true) {
|
||||
int read = in.read(buf);
|
||||
if (read < 0)
|
||||
break;
|
||||
baos.write(buf, 0, read);
|
||||
}
|
||||
in.close();
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
private static void writeSeed(String name, byte data[]) throws Exception {
|
||||
String dirName = "netDb"; // _context.getProperty("router.networkDatabase.dbDir", "netDb");
|
||||
File netDbDir = new File(dirName);
|
||||
if (!netDbDir.exists()) {
|
||||
boolean ok = netDbDir.mkdirs();
|
||||
}
|
||||
FileOutputStream fos = new FileOutputStream(new File(netDbDir, "routerInfo-" + name + ".dat"));
|
||||
fos.write(data);
|
||||
fos.close();
|
||||
}
|
||||
}
|
||||
@@ -28,44 +28,58 @@
|
||||
<input type="hidden" name="nonce" value="<%=System.getProperty("net.i2p.router.web.ConfigNetHandler.nonce")%>" />
|
||||
<input type="hidden" name="action" value="blah" />
|
||||
|
||||
<b>External hostname/IP address:</b>
|
||||
<input name="hostname" type="text" size="32" value="<jsp:getProperty name="nethelper" property="hostname" />" />
|
||||
<input type="submit" name="guesshost" value="Guess" /><br />
|
||||
<b>Externally reachable TCP port:</b>
|
||||
TCP port:
|
||||
<input name="port" type="text" size="4" value="<jsp:getProperty name="nethelper" property="port" />" /> <br />
|
||||
<i>The hostname/IP address and TCP port must be reachable from the outside world. If
|
||||
you are behind a firewall or NAT, this means you must poke a hole for this port. If
|
||||
you are using DHCP and do not have a static IP address, you should either use a service like
|
||||
<a href="http://dyndns.org/">dyndns</a> or leave the hostname blank. If you leave it blank,
|
||||
your router will autodetect the 'correct' IP address by asking a peer (and unconditionally
|
||||
believing them if the address is routable and you don't have any established connections yet).
|
||||
The "guess" functionality makes an HTTP request
|
||||
to <a href="http://www.whatismyip.com/">www.whatismyip.com</a>.</i>
|
||||
<hr />
|
||||
<b>Enable internal time synchronization?</b> <input type="checkbox" <jsp:getProperty name="nethelper" property="enableTimeSyncChecked" /> name="enabletimesync" /><br />
|
||||
<i>If disabled, your machine <b>must</b> be NTP synchronized - your clock must always
|
||||
be within a few seconds of "correct".</i>
|
||||
<b>You must poke a hole in your firewall or NAT (if applicable) so that you can receive inbound TCP
|
||||
connections on it.</b> Nothing will work if you don't. Sorry. We know how to make it so
|
||||
this restriction won't be necessary, but its later on in the
|
||||
<a href="http://www.i2p.net/roadmap">roadmap</a> and we only have so many coder-hours (but if you want
|
||||
to help, please <a href="http://www.i2p.net/getinvolved">get involved!</a>)
|
||||
<hr />
|
||||
|
||||
<b>Bandwidth limiter</b><br />
|
||||
<b>Inbound rate</b>:
|
||||
Inbound rate:
|
||||
<input name="inboundrate" type="text" size="2" value="<jsp:getProperty name="nethelper" property="inboundRate" />" /> KBytes per second<br />
|
||||
<b>Inbound burst duration:</b>
|
||||
Inbound burst duration:
|
||||
<jsp:getProperty name="nethelper" property="inboundBurstFactorBox" /><br />
|
||||
<b>Outbound rate:</b>
|
||||
Outbound rate:
|
||||
<input name="outboundrate" type="text" size="2" value="<jsp:getProperty name="nethelper" property="outboundRate" />" /> KBytes per second<br />
|
||||
<b>Outbound burst duration:</b>
|
||||
Outbound burst duration:
|
||||
<jsp:getProperty name="nethelper" property="outboundBurstFactorBox" /><br />
|
||||
<i>A negative rate means there is no limit</i><br />
|
||||
<hr />
|
||||
<b>Reseed</b> (from <input name="reseedfrom" type="text" size="40" value="http://dev.i2p.net/i2pdb/" />):
|
||||
<input type="submit" name="reseed" value="now" /><br />
|
||||
<i>May take some time to download the peer references</i>
|
||||
Enable internal time synchronization? <input type="checkbox" <jsp:getProperty name="nethelper" property="enableTimeSyncChecked" /> name="enabletimesync" /><br />
|
||||
<i>If disabled, your machine <b>must</b> be NTP synchronized - your clock must always
|
||||
be within a few seconds of "correct". You will need to be able to send outbound UDP
|
||||
packets on port 123 to one of the pool.ntp.org machines (or some other SNTP server).</i>
|
||||
<hr />
|
||||
<input type="submit" name="save" value="Save changes" /> <input type="reset" value="Cancel" /><br />
|
||||
<i>Changing the hostname or TCP port will force a 'soft restart' - dropping your connections
|
||||
and clients as if the router was stopped and restarted. <b>Please be patient</b> - it may take
|
||||
<i>Changing the TCP port will force a 'soft restart' - dropping your connections and clients as
|
||||
if the router was stopped and restarted. <b>Please be patient</b> - it may take
|
||||
a few seconds to complete.</i>
|
||||
</form>
|
||||
<hr />
|
||||
<b>Advanced network config:</b>
|
||||
<p>
|
||||
There are two other network settings, but no one reads this text so there's no reason
|
||||
to tell you about them. In case you actually do read this, here's the deal: by default,
|
||||
I2P will attempt to guess your IP address by having whomever it talks to tell it what
|
||||
address they think you are. If and only if you have no working TCP connections and you
|
||||
have not overridden the IP address, your router will believe them. If that doesn't sound
|
||||
ok to you, thats fine - go to the <a href="/configadvanced.jsp">advanced config</a> page
|
||||
and add "i2np.tcp.hostname=yourHostname", then go to the
|
||||
<a href="/configservice.jsp">service</a> page and do a graceful restart. We used to make
|
||||
people enter a hostname/IP address on this page, but too many people got it wrong ;)</p>
|
||||
|
||||
<p>The other advanced network option has to do with reseeding - you should never need to
|
||||
reseed your router as long as you can find at least one other peer on the network. However,
|
||||
when you do need to reseed, a link will show up on the left hand side which will
|
||||
fetch all of the routerInfo-* files from http://dev.i2p.net/i2pdb/. That URL is just an
|
||||
apache folder pointing at the netDb/ directory of a router - anyone can run one, and you can
|
||||
configure your router to seed off an alternate URL by adding the java environmental property
|
||||
"i2p.reseedURL=someURL" (e.g. java -Di2p.reseedURL=http://dev.i2p.net/i2pdb/ ...). You can
|
||||
also do it manually by getting routerInfo-*.dat files from someone (a friend, someone on IRC,
|
||||
whatever) and saving them to your netDb/ directory.</p>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -29,10 +29,12 @@
|
||||
<input type="hidden" name="nonce" value="<%=System.getProperty("net.i2p.router.web.ConfigAdvancedHandler.nonce")%>" />
|
||||
<input type="hidden" name="action" value="blah" />
|
||||
<textarea rows="20" cols="100" name="config"><jsp:getProperty name="advancedhelper" property="settings" /></textarea><br />
|
||||
<input type="submit" name="shouldsave" value="Apply" /> <input type="reset" value="Cancel" /> <br />
|
||||
<input type="submit" name="shouldsave" value="Apply" /> <input type="reset" value="Cancel" /><!-- <br />
|
||||
<b>Force restart:</b> <input type="checkbox" name="restart" value="force" /> <i>(specify this
|
||||
if the changes made above require the router to reset itself - e.g. you are updating TCP ports
|
||||
or hostnames, etc)</i>
|
||||
or hostnames, etc)</i>-->
|
||||
If you are changing any of the I2NP settings, you should go to the
|
||||
<a href="configservice.jsp">service config</a> page and do a graceful restart after saving.
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
<jsp:getProperty name="clientshelper" property="tunnelCountSelectBox" /><br />
|
||||
<b>Default number of hops per tunnel:</b>
|
||||
<jsp:getProperty name="clientshelper" property="tunnelDepthSelectBox" /><br />
|
||||
<b>Hops per outbound tunnel:</b>
|
||||
<jsp:getProperty name="clientshelper" property="tunnelDepthOutboundSelectBox" /><br />
|
||||
<hr />
|
||||
<input type="submit" name="shouldsave" value="Save changes" /> <input type="reset" value="Cancel" />
|
||||
</form>
|
||||
|
||||
@@ -67,6 +67,14 @@
|
||||
please select the following option and review the thread dumped to
|
||||
<a href="logs.jsp#servicelogs">wrapper.log</a>.</p>
|
||||
<input type="submit" name="action" value="Dump threads" />
|
||||
|
||||
<h4>Launch browser on router startup?</h4>
|
||||
<p>I2P's main configuration interface is this web console, so for your convenience
|
||||
I2P can launch a web browser pointing at
|
||||
<a href="http://localhost:7657/index.jsp">http://localhost:7657/index.jsp</a> whenever
|
||||
the router starts up.</p>
|
||||
<input type="submit" name="action" value="View console on startup" />
|
||||
<input type="submit" name="action" value="Do not view console on startup" />
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
BIN
apps/routerconsole/jsp/favicon.ico
Normal file
BIN
apps/routerconsole/jsp/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
@@ -37,7 +37,9 @@ by their binary code license. This product includes software developed by the A
|
||||
lets you tunnel normal TCP/IP traffic over I2P (such as the eepproxy and the irc proxy).</p>
|
||||
|
||||
<p>The router by default also includes human's public domain <a href="http://www.i2p.net/sam">SAM</a> bridge,
|
||||
which other client applications (such as aum's <a href="http://stasher.i2p/">stasher</a>) can use. For
|
||||
which other client applications (such the <a href="http://duck.i2p/i2p-bt/">bittorrent port</a>) can use.
|
||||
There is also an optimized library for doing large number calculations - jbigi - which in turn uses the
|
||||
LGPL licensed <a href="http://swox.com/gmp/">GMP</a> library, tuned for various PC architectures. For
|
||||
details on other applications available, as well as their licenses, please see the
|
||||
<a href="http://www.i2p.net/licenses">license policy</a>. Source for the I2P code and most bundled
|
||||
client applications can be found on our <a href="http://www.i2p.net/download">download page</a>, and is
|
||||
@@ -47,7 +49,14 @@ in <a href="http://www.i2p.net/cvs">cvs</a>.</p>
|
||||
<jsp:useBean class="net.i2p.router.web.ContentHelper" id="contenthelper" scope="request" />
|
||||
<jsp:setProperty name="contenthelper" property="page" value="history.txt" />
|
||||
<jsp:setProperty name="contenthelper" property="maxLines" value="500" />
|
||||
<jsp:setProperty name="contenthelper" property="startAtBeginning" value="true" />
|
||||
<jsp:getProperty name="contenthelper" property="textContent" />
|
||||
|
||||
<p>
|
||||
A more complete list of updates can be found
|
||||
<a href="http://dev.i2p.net/cgi-bin/cvsweb.cgi/i2p/history.txt?rev=HEAD">online</a>
|
||||
(<a href="http://dev.i2p/cgi-bin/cvsweb.cgi/i2p/history.txt?rev=HEAD">anonymously</a>)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
<jsp:useBean class="net.i2p.router.web.SummaryHelper" id="helper" scope="request" />
|
||||
<jsp:setProperty name="helper" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
|
||||
|
||||
<jsp:useBean class="net.i2p.router.web.ReseedHandler" id="reseed" scope="request" />
|
||||
<jsp:setProperty name="reseed" property="*" />
|
||||
|
||||
<div class="routersummary">
|
||||
<u><b>General</b></u><br />
|
||||
<b>Ident:</b> <jsp:getProperty name="helper" property="ident" /><br />
|
||||
@@ -16,8 +19,27 @@
|
||||
<b>High capacity:</b> <jsp:getProperty name="helper" property="highCapacityPeers" /><br />
|
||||
<b>Well integrated:</b> <jsp:getProperty name="helper" property="wellIntegratedPeers" /><br />
|
||||
<b>Failing:</b> <jsp:getProperty name="helper" property="failingPeers" /><br />
|
||||
<b>Shitlisted:</b> <jsp:getProperty name="helper" property="shitlistedPeers" /><br />
|
||||
<hr />
|
||||
<b>Shitlisted:</b> <jsp:getProperty name="helper" property="shitlistedPeers" /><br /><%
|
||||
if (helper.getActivePeers() <= 0) {
|
||||
%><b><a href="config.jsp">check your NAT/firewall</a></b><br /><%
|
||||
}
|
||||
if (helper.getActiveProfiles() <= 4) { // 4 is the min fallback
|
||||
if ("true".equals(System.getProperty("net.i2p.router.web.ReseedHandler.reseedInProgress", "false"))) {
|
||||
out.print(" <i>reseeding</i>");
|
||||
} else {
|
||||
long nonce = new java.util.Random().nextLong();
|
||||
String prev = System.getProperty("net.i2p.router.web.ReseedHandler.nonce");
|
||||
if (prev != null) System.setProperty("net.i2p.router.web.ReseedHandler.noncePrev", prev);
|
||||
System.setProperty("net.i2p.router.web.ReseedHandler.nonce", nonce+"");
|
||||
String uri = request.getRequestURI();
|
||||
if (uri.indexOf('?') > 0)
|
||||
uri = uri + "&reseedNonce=" + nonce;
|
||||
else
|
||||
uri = uri + "?reseedNonce=" + nonce;
|
||||
out.print(" <a href=\"" + uri + "\">reseed</a>");
|
||||
}
|
||||
}
|
||||
%><hr />
|
||||
|
||||
<u><b>Bandwidth in/out</b></u><br />
|
||||
<b>1m:</b> <jsp:getProperty name="helper" property="inboundMinuteKBps" />/<jsp:getProperty name="helper" property="outboundMinuteKBps" />KBps<br />
|
||||
|
||||
@@ -2,6 +2,9 @@ I need to do these things:
|
||||
|
||||
* SAM raw support
|
||||
* Write an instruction manual
|
||||
* Make dest a dynamic string
|
||||
* Change SAM parser to use a hashmap
|
||||
* Switch to GNU Autoconf (?)
|
||||
|
||||
Anyone can help with these things:
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
/* vi:set ts=4: */
|
||||
|
||||
v1.30
|
||||
* Improved WIRETAP to do more logging
|
||||
* Added "pinger.sh" shell script example for using i2p-ping
|
||||
* Added SAM_BAD_STYLE error
|
||||
* Added exit values for i2p-ping from xolo
|
||||
|
||||
v1.25 2004-07-31
|
||||
* Created I2P-Ping, a new example program (it's a clone of I2Ping). Works
|
||||
|
||||
60
apps/sam/c/examples/i2p-ping/pinger.sh
Normal file
60
apps/sam/c/examples/i2p-ping/pinger.sh
Normal file
@@ -0,0 +1,60 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Copyright (c) 2004, Matthew P. Cashdollar <mpc@innographx.com>
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# * Neither the name of the author nor the names of any contributors
|
||||
# may be used to endorse or promote products derived from this software
|
||||
# without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
||||
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
||||
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
|
||||
# OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
# This script I use to ping all the hosts in the I2P hosts.txt file. You should
|
||||
# edit your hosts.txt and remove all the comments and blanks lines before
|
||||
# running this.
|
||||
#
|
||||
# You can set this up to run daily via cron. It takes a very long time to run
|
||||
# so I recommend starting it in the middle of the night.
|
||||
|
||||
OUTPUT=/tmp/index.html
|
||||
FINALOUT=$HOME/www/index.html
|
||||
|
||||
echo "<html>" > $OUTPUT
|
||||
echo "<head>" >> $OUTPUT
|
||||
echo "<title>I2P Weather Report</title>" >> $OUTPUT
|
||||
echo "</head>" >> $OUTPUT
|
||||
echo "<body>" >> $OUTPUT
|
||||
echo "<h1>I2P Weather Report</h1>" >> $OUTPUT
|
||||
echo "Date: " >> $OUTPUT
|
||||
date -u >> $OUTPUT
|
||||
echo "(refreshed daily)" >> $OUTPUT
|
||||
echo "<p>" >> $OUTPUT
|
||||
echo "If your site isn't listed, that means the ping failed (I2P error)" >> $OUTPUT
|
||||
echo "<p>" >> $OUTPUT
|
||||
echo "<pre>" >> $OUTPUT
|
||||
cut -d"=" -f1 $HOME/i2p/hosts.txt | tr "\n" " " | xargs $HOME/bin/i2p-ping -q -c 1 | grep -v "I2P error" >> $OUTPUT
|
||||
echo "</pre>" >> $OUTPUT
|
||||
echo "<p>" >> $OUTPUT
|
||||
echo "<i>Disclaimer: This only indicates accessibility from my router, not the network in general.</i>" >> $OUTPUT
|
||||
echo "</body>" >> $OUTPUT
|
||||
echo "</html>" >> $OUTPUT
|
||||
cp $OUTPUT $FINALOUT
|
||||
@@ -35,6 +35,7 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/*
|
||||
|
||||
@@ -85,8 +85,12 @@ void (*sam_statusback)(sam_sess_t *session, sam_sid_t stream_id,
|
||||
bool sam_close(sam_sess_t *session)
|
||||
{
|
||||
assert(session != NULL);
|
||||
if (!session->connected)
|
||||
if (!session->connected) {
|
||||
#if SAM_WIRETAP
|
||||
SAMLOGS("Connection already closed - sam_close() skipped");
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef WINSOCK
|
||||
if (closesocket(session->sock) == SOCKET_ERROR) {
|
||||
@@ -95,13 +99,19 @@ bool sam_close(sam_sess_t *session)
|
||||
return false;
|
||||
}
|
||||
session->connected = false;
|
||||
if (sam_winsock_cleanup() == SAM_OK)
|
||||
if (sam_winsock_cleanup() == SAM_OK) {
|
||||
#if SAM_WIRETAP
|
||||
SAMLOGS("Connection closed safely");
|
||||
#endif
|
||||
return true;
|
||||
else
|
||||
} else
|
||||
return false;
|
||||
#else
|
||||
if (close(session->sock) == 0) {
|
||||
session->connected = false;
|
||||
#if SAM_WIRETAP
|
||||
SAMLOGS("Connection closed safely");
|
||||
#endif
|
||||
return true;
|
||||
} else {
|
||||
SAMLOG("Failed closing the SAM connection (%s)", strerror(errno));
|
||||
@@ -122,8 +132,7 @@ bool sam_close(sam_sess_t *session)
|
||||
* tunneldepth - length of the I2P tunnels created by this program (longer is
|
||||
* more anonymous, but slower)
|
||||
*
|
||||
* Returns: True on success, false on failure. If true, `session' will be ready
|
||||
* for use.
|
||||
* Returns: SAM error code. If SAM_OK, `session' will be ready for use.
|
||||
*/
|
||||
samerr_t sam_connect(sam_sess_t *session, const char *samhost, uint16_t samport,
|
||||
const char *destname, sam_conn_t style, uint_t tunneldepth)
|
||||
@@ -188,7 +197,7 @@ samerr_t sam_connect(sam_sess_t *session, const char *samhost, uint16_t samport,
|
||||
* data - the data we're sending
|
||||
* size - the size of the data
|
||||
*
|
||||
* Returns: true on success, false on failure
|
||||
* Returns: SAM_OK on success
|
||||
*/
|
||||
samerr_t sam_dgram_send(sam_sess_t *session, const sam_pubkey_t dest,
|
||||
const void *data, size_t size)
|
||||
@@ -589,7 +598,7 @@ static ssize_t sam_read1(sam_sess_t *session, char *buf, size_t n)
|
||||
if (*p == '\n') { /* end of SAM response */
|
||||
*p = '\0';
|
||||
#if SAM_WIRETAP
|
||||
printf("<<<< %s\n", buf);
|
||||
printf("*RR* %s\n", buf);
|
||||
#endif
|
||||
return n - nleft;
|
||||
}
|
||||
@@ -654,7 +663,16 @@ static ssize_t sam_read2(sam_sess_t *session, void *buf, size_t n)
|
||||
p += nread;
|
||||
}
|
||||
#if SAM_WIRETAP
|
||||
printf("<<<< (read2() %d bytes)\n", n);
|
||||
p = buf;
|
||||
printf("*RR* ");
|
||||
for (size_t x = 0; x < n; x++) {
|
||||
if (isprint(((uchar_t*)p)[x]))
|
||||
printf("%c,", ((uchar_t*)p)[x]);
|
||||
else
|
||||
printf("%03d,", ((uint8_t*)p)[x]);
|
||||
}
|
||||
printf("\n");
|
||||
printf("*RR* (read2() read %d bytes)\n", n);
|
||||
#endif
|
||||
assert(nleft == 0);/* <---\ */
|
||||
return n - nleft; /* should be equal to initial n */
|
||||
@@ -680,7 +698,7 @@ static bool sam_readable(sam_sess_t *session)
|
||||
FD_ZERO(&rset);
|
||||
FD_SET(session->sock, &rset);
|
||||
tv.tv_sec = 0;
|
||||
tv.tv_usec = 10;
|
||||
tv.tv_usec = 0;
|
||||
rc = select(session->sock + 1, &rset, NULL, NULL, &tv);
|
||||
if (rc == 0)
|
||||
return false;
|
||||
@@ -791,39 +809,6 @@ void sam_sendq_flush(sam_sess_t *session, sam_sid_t stream_id,
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Allocates memory for the session and sets its default values
|
||||
*
|
||||
* session - pointer to a previously allocated sam_sess_t to initalise, or NULL
|
||||
* if you want memory to be allocated by this function
|
||||
*/
|
||||
sam_sess_t *sam_session_init(sam_sess_t *session)
|
||||
{
|
||||
if (session == NULL) {
|
||||
session = malloc(sizeof(sam_sess_t));
|
||||
if (session == NULL) {
|
||||
SAMLOGS("Out of memory");
|
||||
abort();
|
||||
}
|
||||
}
|
||||
session->connected = false;
|
||||
session->prev_id = 0;
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/*
|
||||
* Frees memory used by the session and sets the pointer to NULL
|
||||
*
|
||||
* session - pointer to a pointer to a sam_sess_t
|
||||
*/
|
||||
void sam_session_free(sam_sess_t **session)
|
||||
{
|
||||
assert(*session != NULL);
|
||||
free(*session);
|
||||
*session = NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* Sends the second SAM handshake command and checks the reply
|
||||
*
|
||||
@@ -880,6 +865,39 @@ static samerr_t sam_session_create(sam_sess_t *session, const char *destname,
|
||||
return SAM_UNKNOWN;
|
||||
}
|
||||
|
||||
/*
|
||||
* Allocates memory for the session and sets its default values
|
||||
*
|
||||
* session - pointer to a previously allocated sam_sess_t to initalise, or NULL
|
||||
* if you want memory to be allocated by this function
|
||||
*/
|
||||
sam_sess_t *sam_session_init(sam_sess_t *session)
|
||||
{
|
||||
if (session == NULL) {
|
||||
session = malloc(sizeof(sam_sess_t));
|
||||
if (session == NULL) {
|
||||
SAMLOGS("Out of memory");
|
||||
abort();
|
||||
}
|
||||
}
|
||||
session->connected = false;
|
||||
session->prev_id = 0;
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/*
|
||||
* Frees memory used by the session and sets the pointer to NULL
|
||||
*
|
||||
* session - pointer to a pointer to a sam_sess_t
|
||||
*/
|
||||
void sam_session_free(sam_sess_t **session)
|
||||
{
|
||||
assert(*session != NULL);
|
||||
free(*session);
|
||||
*session = NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* Connects to a remote host and returns a connected descriptor
|
||||
*
|
||||
@@ -1120,7 +1138,7 @@ const char *sam_strerror(samerr_t code)
|
||||
case SAM_BAD_STYLE: /* Style must be stream, datagram, or raw */
|
||||
return "Bad connection style";
|
||||
case SAM_BAD_VERSION: /* sam_hello() had an unexpected reply */
|
||||
return "Bad SAM version";
|
||||
return "Not a SAM port, or bad SAM version";
|
||||
case SAM_CALLBACKS_UNSET: /* Some callbacks are still set to NULL */
|
||||
return "Callbacks unset";
|
||||
case SAM_SOCKET_ERROR: /* TCP/IP connection to the SAM host:port
|
||||
@@ -1325,6 +1343,7 @@ static ssize_t sam_write(sam_sess_t *session, const void *buf, size_t n)
|
||||
}
|
||||
#if SAM_WIRETAP
|
||||
const uchar_t *cp = buf;
|
||||
printf("*WW* ");
|
||||
for (size_t x = 0; x < n; x++) {
|
||||
if (isprint(cp[x]))
|
||||
printf("%c,", cp[x]);
|
||||
@@ -1356,7 +1375,7 @@ static ssize_t sam_write(sam_sess_t *session, const void *buf, size_t n)
|
||||
p += nwritten;
|
||||
}
|
||||
#if SAM_WIRETAP
|
||||
printf(">>>> (write() %d bytes)\n", n);
|
||||
printf("*WW* (write() wrote %d bytes)\n", n);
|
||||
#endif
|
||||
|
||||
return n;
|
||||
|
||||
@@ -46,7 +46,7 @@ public class SAMBridge implements Runnable {
|
||||
* app designated destination name to the base64 of the I2P formatted
|
||||
* destination keys (Destination+PrivateKey+SigningPrivateKey)
|
||||
*/
|
||||
private Map nameToPrivKeys = Collections.synchronizedMap(new HashMap(8));
|
||||
private Map nameToPrivKeys;
|
||||
|
||||
private boolean acceptConnections = true;
|
||||
|
||||
@@ -65,6 +65,7 @@ public class SAMBridge implements Runnable {
|
||||
*/
|
||||
public SAMBridge(String listenHost, int listenPort, Properties i2cpProps, String persistFile) {
|
||||
persistFilename = persistFile;
|
||||
nameToPrivKeys = new HashMap(8);
|
||||
loadKeys();
|
||||
try {
|
||||
if ( (listenHost != null) && !("0.0.0.0".equals(listenHost)) ) {
|
||||
@@ -93,16 +94,18 @@ public class SAMBridge implements Runnable {
|
||||
* @return null if the name does not exist, or if it is improperly formatted
|
||||
*/
|
||||
public Destination getDestination(String name) {
|
||||
String val = (String)nameToPrivKeys.get(name);
|
||||
if (val == null) return null;
|
||||
try {
|
||||
Destination d = new Destination();
|
||||
d.fromBase64(val);
|
||||
return d;
|
||||
} catch (DataFormatException dfe) {
|
||||
_log.error("Error retrieving the destination from " + name, dfe);
|
||||
nameToPrivKeys.remove(name);
|
||||
return null;
|
||||
synchronized (nameToPrivKeys) {
|
||||
String val = (String)nameToPrivKeys.get(name);
|
||||
if (val == null) return null;
|
||||
try {
|
||||
Destination d = new Destination();
|
||||
d.fromBase64(val);
|
||||
return d;
|
||||
} catch (DataFormatException dfe) {
|
||||
_log.error("Error retrieving the destination from " + name, dfe);
|
||||
nameToPrivKeys.remove(name);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,9 +117,11 @@ public class SAMBridge implements Runnable {
|
||||
* @return null if the name does not exist, else the stream
|
||||
*/
|
||||
public String getKeystream(String name) {
|
||||
String val = (String)nameToPrivKeys.get(name);
|
||||
if (val == null) return null;
|
||||
return val;
|
||||
synchronized (nameToPrivKeys) {
|
||||
String val = (String)nameToPrivKeys.get(name);
|
||||
if (val == null) return null;
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,7 +129,9 @@ public class SAMBridge implements Runnable {
|
||||
*
|
||||
*/
|
||||
public void addKeystream(String name, String stream) {
|
||||
nameToPrivKeys.put(name, stream);
|
||||
synchronized (nameToPrivKeys) {
|
||||
nameToPrivKeys.put(name, stream);
|
||||
}
|
||||
storeKeys();
|
||||
}
|
||||
|
||||
@@ -132,49 +139,52 @@ public class SAMBridge implements Runnable {
|
||||
* Load up the keys from the persistFilename
|
||||
*
|
||||
*/
|
||||
private synchronized void loadKeys() {
|
||||
Map keys = new HashMap(16);
|
||||
FileInputStream in = null;
|
||||
try {
|
||||
in = new FileInputStream(persistFilename);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
|
||||
String line = null;
|
||||
while ( (line = reader.readLine()) != null) {
|
||||
int eq = line.indexOf('=');
|
||||
String name = line.substring(0, eq);
|
||||
String privKeys = line.substring(eq+1);
|
||||
keys.put(name, privKeys);
|
||||
private void loadKeys() {
|
||||
synchronized (nameToPrivKeys) {
|
||||
nameToPrivKeys.clear();
|
||||
FileInputStream in = null;
|
||||
try {
|
||||
in = new FileInputStream(persistFilename);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
|
||||
String line = null;
|
||||
while ( (line = reader.readLine()) != null) {
|
||||
int eq = line.indexOf('=');
|
||||
String name = line.substring(0, eq);
|
||||
String privKeys = line.substring(eq+1);
|
||||
nameToPrivKeys.put(name, privKeys);
|
||||
}
|
||||
} catch (FileNotFoundException fnfe) {
|
||||
_log.warn("Key file does not exist at " + persistFilename);
|
||||
} catch (IOException ioe) {
|
||||
_log.error("Unable to read the keys from " + persistFilename, ioe);
|
||||
} finally {
|
||||
if (in != null) try { in.close(); } catch (IOException ioe) {}
|
||||
}
|
||||
} catch (FileNotFoundException fnfe) {
|
||||
_log.warn("Key file does not exist at " + persistFilename);
|
||||
} catch (IOException ioe) {
|
||||
_log.error("Unable to read the keys from " + persistFilename, ioe);
|
||||
} finally {
|
||||
if (in != null) try { in.close(); } catch (IOException ioe) {}
|
||||
}
|
||||
nameToPrivKeys = Collections.synchronizedMap(keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the current keys to disk in the location specified on creation
|
||||
*
|
||||
*/
|
||||
private synchronized void storeKeys() {
|
||||
FileOutputStream out = null;
|
||||
try {
|
||||
out = new FileOutputStream(persistFilename);
|
||||
for (Iterator iter = nameToPrivKeys.keySet().iterator(); iter.hasNext(); ) {
|
||||
String name = (String)iter.next();
|
||||
String privKeys = (String)nameToPrivKeys.get(name);
|
||||
out.write(name.getBytes());
|
||||
out.write('=');
|
||||
out.write(privKeys.getBytes());
|
||||
out.write('\n');
|
||||
private void storeKeys() {
|
||||
synchronized (nameToPrivKeys) {
|
||||
FileOutputStream out = null;
|
||||
try {
|
||||
out = new FileOutputStream(persistFilename);
|
||||
for (Iterator iter = nameToPrivKeys.keySet().iterator(); iter.hasNext(); ) {
|
||||
String name = (String)iter.next();
|
||||
String privKeys = (String)nameToPrivKeys.get(name);
|
||||
out.write(name.getBytes());
|
||||
out.write('=');
|
||||
out.write(privKeys.getBytes());
|
||||
out.write('\n');
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
_log.error("Error writing out the SAM keys to " + persistFilename, ioe);
|
||||
} finally {
|
||||
if (out != null) try { out.close(); } catch (IOException ioe) {}
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
_log.error("Error writing out the SAM keys to " + persistFilename, ioe);
|
||||
} finally {
|
||||
if (out != null) try { out.close(); } catch (IOException ioe) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,6 +250,7 @@ public class SAMBridge implements Runnable {
|
||||
}
|
||||
|
||||
public void run() {
|
||||
if (serverSocket == null) return;
|
||||
try {
|
||||
while (acceptConnections) {
|
||||
Socket s = serverSocket.accept();
|
||||
@@ -270,8 +281,11 @@ public class SAMBridge implements Runnable {
|
||||
if (_log.shouldLog(Log.ERROR))
|
||||
_log.error("SAM Error sending error reply", ioe);
|
||||
}
|
||||
s.close();
|
||||
}
|
||||
try { s.close(); } catch (IOException ioe) {}
|
||||
} catch (Exception ee) {
|
||||
try { s.close(); } catch (IOException ioe) {}
|
||||
_log.log(Log.CRIT, "Unexpected error handling SAM connection", ee);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (_log.shouldLog(Log.ERROR))
|
||||
@@ -280,7 +294,8 @@ public class SAMBridge implements Runnable {
|
||||
try {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Shutting down, closing server socket");
|
||||
serverSocket.close();
|
||||
if (serverSocket != null)
|
||||
serverSocket.close();
|
||||
} catch (IOException e) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,14 @@ public abstract class SAMHandler implements Runnable {
|
||||
socketOS.flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If you're crazy enough to write to the raw socket, grab the write lock
|
||||
* with getWriteLock(), synchronize against it, and write to the getOut()
|
||||
*
|
||||
*/
|
||||
protected Object getWriteLock() { return socketWLock; }
|
||||
protected OutputStream getOut() { return socketOS; }
|
||||
|
||||
/**
|
||||
* Write a string to the handler's socket. This method must
|
||||
|
||||
@@ -152,6 +152,8 @@ public class SAMHandlerFactory {
|
||||
|
||||
/* Get the major protocol version from a string */
|
||||
private static int getMajor(String ver) {
|
||||
if ( (ver == null) || (ver.indexOf('.') < 0) )
|
||||
return -1;
|
||||
try {
|
||||
String major = ver.substring(0, ver.indexOf("."));
|
||||
return Integer.parseInt(major);
|
||||
@@ -164,6 +166,8 @@ public class SAMHandlerFactory {
|
||||
|
||||
/* Get the minor protocol version from a string */
|
||||
private static int getMinor(String ver) {
|
||||
if ( (ver == null) || (ver.indexOf('.') < 0) )
|
||||
return -1;
|
||||
try {
|
||||
String major = ver.substring(ver.indexOf(".") + 1);
|
||||
return Integer.parseInt(major);
|
||||
|
||||
@@ -180,9 +180,9 @@ public class SAMStreamSession {
|
||||
Destination d = new Destination();
|
||||
d.fromBase64(dest);
|
||||
|
||||
// FIXME: we should config I2PSocketOptions here
|
||||
I2PSocketOptions opts = new I2PSocketOptions();
|
||||
opts.setConnectTimeout(60 * 1000);
|
||||
I2PSocketOptions opts = socketMgr.buildOptions(props);
|
||||
if (props.getProperty(I2PSocketOptions.PROP_CONNECT_TIMEOUT) == null)
|
||||
opts.setConnectTimeout(60 * 1000);
|
||||
|
||||
_log.debug("Connecting new I2PSocket...");
|
||||
I2PSocket i2ps = socketMgr.connect(d, opts);
|
||||
|
||||
@@ -111,9 +111,10 @@ public class SAMUtils {
|
||||
*/
|
||||
public static Properties parseParams(StringTokenizer tok) throws SAMException {
|
||||
int pos, nprops = 0, ntoks = tok.countTokens();
|
||||
String token, param, value;
|
||||
String token, param;
|
||||
Properties props = new Properties();
|
||||
|
||||
StringBuffer value = new StringBuffer();
|
||||
for (int i = 0; i < ntoks; ++i) {
|
||||
token = tok.nextToken();
|
||||
|
||||
@@ -123,9 +124,16 @@ public class SAMUtils {
|
||||
throw new SAMException("Bad formatting for param [" + token + "]");
|
||||
}
|
||||
param = token.substring(0, pos);
|
||||
value = token.substring(pos + 1);
|
||||
value.append(token.substring(pos+1));
|
||||
if (value.charAt(0) == '"') {
|
||||
while ( (i < ntoks) && (value.lastIndexOf("\"") <= 0) ) {
|
||||
value.append(' ').append(tok.nextToken());
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
props.setProperty(param, value);
|
||||
props.setProperty(param, value.toString());
|
||||
value.setLength(0);
|
||||
nprops += 1;
|
||||
}
|
||||
|
||||
@@ -157,4 +165,19 @@ public class SAMUtils {
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
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\"");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
private static void test(String props) throws Exception {
|
||||
StringTokenizer tok = new StringTokenizer(props);
|
||||
Properties p = parseParams(tok);
|
||||
System.out.println(p);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.NoRouteToHostException;
|
||||
@@ -107,7 +108,7 @@ public class SAMv1Handler extends SAMHandler implements SAMRawReceiver, SAMDatag
|
||||
|
||||
msg = buf.toString("ISO-8859-1").trim();
|
||||
if (_log.shouldLog(Log.DEBUG)) {
|
||||
_log.debug("New message received: " + msg);
|
||||
_log.debug("New message received: [" + msg + "]");
|
||||
}
|
||||
buf.reset();
|
||||
|
||||
@@ -685,9 +686,12 @@ public class SAMv1Handler extends SAMHandler implements SAMRawReceiver, SAMDatag
|
||||
|
||||
ByteArrayOutputStream msg = new ByteArrayOutputStream();
|
||||
|
||||
msg.write(("RAW RECEIVED SIZE=" + data.length
|
||||
+ "\n").getBytes("ISO-8859-1"));
|
||||
String msgText = "RAW RECEIVED SIZE=" + data.length + "\n";
|
||||
msg.write(msgText.getBytes("ISO-8859-1"));
|
||||
msg.write(data);
|
||||
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("sending to client: " + msgText);
|
||||
|
||||
writeBytes(msg.toByteArray());
|
||||
}
|
||||
@@ -716,9 +720,12 @@ public class SAMv1Handler extends SAMHandler implements SAMRawReceiver, SAMDatag
|
||||
|
||||
ByteArrayOutputStream msg = new ByteArrayOutputStream();
|
||||
|
||||
msg.write(("DATAGRAM RECEIVED DESTINATION=" + sender.toBase64()
|
||||
+ " SIZE=" + data.length
|
||||
+ "\n").getBytes("ISO-8859-1"));
|
||||
String msgText = "DATAGRAM RECEIVED DESTINATION=" + sender.toBase64()
|
||||
+ " SIZE=" + data.length + "\n";
|
||||
msg.write(msgText.getBytes("ISO-8859-1"));
|
||||
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("sending to client: " + msgText);
|
||||
msg.write(data);
|
||||
|
||||
writeBytes(msg.toByteArray());
|
||||
@@ -759,13 +766,24 @@ public class SAMv1Handler extends SAMHandler implements SAMRawReceiver, SAMDatag
|
||||
throw new NullPointerException("BUG! STREAM session is null!");
|
||||
}
|
||||
|
||||
ByteArrayOutputStream msg = new ByteArrayOutputStream();
|
||||
|
||||
msg.write(("STREAM RECEIVED ID=" + id
|
||||
+" SIZE=" + len + "\n").getBytes("ISO-8859-1"));
|
||||
msg.write(data, 0, len);
|
||||
|
||||
writeBytes(msg.toByteArray());
|
||||
String msgText = "STREAM RECEIVED ID=" + id +" SIZE=" + len + "\n";
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("sending to client: " + msgText);
|
||||
|
||||
byte prefix[] = msgText.getBytes("ISO-8859-1");
|
||||
|
||||
// dont waste so much memory
|
||||
//ByteArrayOutputStream msg = new ByteArrayOutputStream();
|
||||
//msg.write(msgText.getBytes("ISO-8859-1"));
|
||||
//msg.write(data, 0, len);
|
||||
// writeBytes(msg.toByteArray());
|
||||
Object writeLock = getWriteLock();
|
||||
OutputStream out = getOut();
|
||||
synchronized (writeLock) {
|
||||
out.write(prefix);
|
||||
out.write(data, 0, len);
|
||||
out.flush();
|
||||
}
|
||||
}
|
||||
|
||||
public void notifyStreamDisconnection(int id, String result, String msg) throws IOException {
|
||||
|
||||
@@ -33,7 +33,11 @@ public class TestStreamTransfer {
|
||||
|
||||
private static void runTest(String samHost, int samPort, String conOptions) {
|
||||
startAlice(samHost, samPort, conOptions);
|
||||
testBob(samHost, samPort, conOptions);
|
||||
for (int i = 0; i < 20; i++) {
|
||||
testBob("bob" + i, samHost, samPort, conOptions);
|
||||
if (i % 2 == 1)
|
||||
try { Thread.sleep(10*1000); } catch (InterruptedException ie) {}
|
||||
}
|
||||
}
|
||||
|
||||
private static void startAlice(String host, int port, String conOptions) {
|
||||
@@ -95,11 +99,13 @@ public class TestStreamTransfer {
|
||||
try { _out.close(); } catch (IOException ioe) {}
|
||||
try { _s.close(); } catch (IOException ioe) {}
|
||||
_streams.clear();
|
||||
_dead = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
private void doRun() throws IOException, SAMException {
|
||||
String line = _reader.readLine();
|
||||
_log.debug("Read: " + line);
|
||||
StringTokenizer tok = new StringTokenizer(line);
|
||||
String maj = tok.nextToken();
|
||||
String min = tok.nextToken();
|
||||
@@ -146,12 +152,15 @@ public class TestStreamTransfer {
|
||||
}
|
||||
_log.info("Received from the stream " + id + ": [" + new String(payload) + "]");
|
||||
try { Thread.sleep(5*1000); } catch (InterruptedException ie) {}
|
||||
/*
|
||||
// now echo it back
|
||||
String reply = "STREAM SEND ID=" + id +
|
||||
" SIZE=" + payloadSize +
|
||||
"\n" + payload;
|
||||
"\n" + new String(payload);
|
||||
_out.write(reply.getBytes());
|
||||
_out.flush();
|
||||
_log.info("Reply sent back [" + new String(reply.getBytes()) + "]");
|
||||
*/
|
||||
} else {
|
||||
_log.error("Received unsupported type [" + maj + "/"+ min + "]");
|
||||
return;
|
||||
@@ -159,8 +168,27 @@ public class TestStreamTransfer {
|
||||
}
|
||||
}
|
||||
|
||||
private static void testBob(String host, int port, String conOptions) {
|
||||
_log.info("\n\nTesting Bob\n\n\n");
|
||||
private static void testBob(String sessionName, String host, int port, String conOptions) {
|
||||
I2PThread t = new I2PThread(new TestBob(sessionName, host, port, conOptions), sessionName);
|
||||
t.start();
|
||||
}
|
||||
private static class TestBob implements Runnable {
|
||||
private String _sessionName;
|
||||
private String _host;
|
||||
private int _port;
|
||||
private String _opts;
|
||||
public TestBob(String name, String host, int port, String opts) {
|
||||
_sessionName = name;
|
||||
_host = host;
|
||||
_port = port;
|
||||
_opts = opts;
|
||||
}
|
||||
public void run() {
|
||||
doTestBob(_sessionName, _host, _port, _opts);
|
||||
}
|
||||
}
|
||||
private static void doTestBob(String sessionName, String host, int port, String conOptions) {
|
||||
_log.info("\n\nTesting " + sessionName + "\n\n\n");
|
||||
try {
|
||||
Socket s = new Socket(host, port);
|
||||
OutputStream out = s.getOutputStream();
|
||||
@@ -168,32 +196,37 @@ public class TestStreamTransfer {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(s.getInputStream()));
|
||||
String line = reader.readLine();
|
||||
_log.debug("line read for valid version: " + line);
|
||||
String req = "SESSION CREATE STYLE=STREAM DESTINATION=Bob " + conOptions + "\n";
|
||||
String req = "SESSION CREATE STYLE=STREAM DESTINATION=" + sessionName + " " + conOptions + "\n";
|
||||
out.write(req.getBytes());
|
||||
line = reader.readLine();
|
||||
_log.info("Response to creating the session with destination Bob: " + line);
|
||||
_log.info("Response to creating the session with destination "+ sessionName+": " + line);
|
||||
req = "STREAM CONNECT ID=42 DESTINATION=" + _alice + "\n";
|
||||
out.write(req.getBytes());
|
||||
line = reader.readLine();
|
||||
_log.info("Response to the stream connect from Bob to Alice: " + line);
|
||||
_log.info("Response to the stream connect from "+sessionName+" to Alice: " + line);
|
||||
StringTokenizer tok = new StringTokenizer(line);
|
||||
String maj = tok.nextToken();
|
||||
String min = tok.nextToken();
|
||||
Properties props = SAMUtils.parseParams(tok);
|
||||
_log.info("props = " + props);
|
||||
String result = props.getProperty("RESULT");
|
||||
if (!("OK".equals(result))) {
|
||||
_log.error("Unable to connect!");
|
||||
_dead = true;
|
||||
//_dead = true;
|
||||
return;
|
||||
}
|
||||
try { Thread.sleep(5*1000) ; } catch (InterruptedException ie) {}
|
||||
req = "STREAM SEND ID=42 SIZE=10\nBlahBlah!!";
|
||||
_log.info("Sending data");
|
||||
out.write(req.getBytes());
|
||||
out.flush();
|
||||
try { Thread.sleep(20*1000); } catch (InterruptedException ie) {}
|
||||
_log.info("Sending close");
|
||||
req = "STREAM CLOSE ID=42\n";
|
||||
out.write(req.getBytes());
|
||||
try { Thread.sleep(3*1000); } catch (InterruptedException ie) {}
|
||||
_dead = true;
|
||||
out.flush();
|
||||
try { Thread.sleep(30*1000); } catch (InterruptedException ie) {}
|
||||
//_dead = true;
|
||||
s.close();
|
||||
} catch (Exception e) {
|
||||
_log.error("Error testing for valid version", e);
|
||||
@@ -203,7 +236,7 @@ public class TestStreamTransfer {
|
||||
public static void main(String args[]) {
|
||||
// "i2cp.tcp.host=www.i2p.net i2cp.tcp.port=7765 tunnels.inboundDepth=0";
|
||||
// "i2cp.tcp.host=localhost i2cp.tcp.port=7654 tunnels.inboundDepth=0";
|
||||
String conOptions = "i2cp.tcp.host=www.i2p.net i2cp.tcp.port=7765 tunnels.inboundDepth=0";
|
||||
String conOptions = "i2cp.tcp.host=localhost i2cp.tcp.port=10001 tunnels.inboundDepth=0";
|
||||
if (args.length > 0) {
|
||||
conOptions = "";
|
||||
for (int i = 0; i < args.length; i++)
|
||||
@@ -215,8 +248,8 @@ public class TestStreamTransfer {
|
||||
} catch (Throwable t) {
|
||||
_log.error("Error running test", t);
|
||||
}
|
||||
try { Thread.sleep(5*1000); } catch (InterruptedException ie) {}
|
||||
System.exit(0);
|
||||
//try { Thread.sleep(5*1000); } catch (InterruptedException ie) {}
|
||||
//System.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,9 +33,8 @@ def urlopen(url, eepaddr=eepaddr):
|
||||
eepaddr = eepaddr.rstrip('/')
|
||||
|
||||
proxy = urllib2.ProxyHandler( \
|
||||
{'http': 'http://anonymous:passwd@' + eepaddr})
|
||||
opener = urllib2.build_opener(proxy, \
|
||||
urllib2.HTTPBasicAuthHandler(), urllib2.HTTPHandler)
|
||||
{'http': 'http://' + eepaddr})
|
||||
opener = urllib2.build_opener(proxy, urllib2.HTTPHandler)
|
||||
return opener.open(url)
|
||||
|
||||
def urlget(url, eepaddr=eepaddr):
|
||||
|
||||
@@ -40,7 +40,7 @@ class Poll:
|
||||
del self.fds[self._hash(fd)]
|
||||
def poll(self, timeout=None):
|
||||
readlist, writelist, errlist = [], [], []
|
||||
for F, mask in self.fds:
|
||||
for F, mask in self.fds.values():
|
||||
if mask & POLLIN: readlist += [F]
|
||||
if mask & POLLOUT: writelist += [F]
|
||||
if mask & POLLERR: errlist += [F]
|
||||
|
||||
42
apps/streaming/java/build.xml
Normal file
42
apps/streaming/java/build.xml
Normal file
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project basedir="." default="all" name="streaming">
|
||||
<target name="all" depends="clean, build" />
|
||||
<target name="build" depends="builddep, jar" />
|
||||
<target name="builddep">
|
||||
<ant dir="../../ministreaming/java/" target="build" />
|
||||
<!-- ministreaming will build core -->
|
||||
</target>
|
||||
<target name="compile">
|
||||
<mkdir dir="./build" />
|
||||
<mkdir dir="./build/obj" />
|
||||
<javac
|
||||
srcdir="./src:./test"
|
||||
debug="true" deprecation="on" source="1.3" target="1.3"
|
||||
destdir="./build/obj"
|
||||
classpath="../../../core/java/build/i2p.jar:../../ministreaming/java/build/mstreaming.jar" />
|
||||
</target>
|
||||
<target name="jar" depends="builddep, compile">
|
||||
<jar destfile="./build/streaming.jar" basedir="./build/obj" includes="**/*.class" />
|
||||
</target>
|
||||
<target name="javadoc">
|
||||
<mkdir dir="./build" />
|
||||
<mkdir dir="./build/javadoc" />
|
||||
<javadoc
|
||||
sourcepath="./src:../../../core/java/src:../../ministreaming/java/src" destdir="./build/javadoc"
|
||||
packagenames="*"
|
||||
use="true"
|
||||
splitindex="true"
|
||||
windowtitle="Streaming" />
|
||||
</target>
|
||||
<target name="clean">
|
||||
<delete dir="./build" />
|
||||
</target>
|
||||
<target name="cleandep" depends="clean">
|
||||
<!-- ministreaming will clean core -->
|
||||
<ant dir="../../ministreaming/java/" target="distclean" />
|
||||
</target>
|
||||
<target name="distclean" depends="clean">
|
||||
<!-- ministreaming will clean core -->
|
||||
<ant dir="../../ministreaming/java/" target="distclean" />
|
||||
</target>
|
||||
</project>
|
||||
800
apps/streaming/java/src/net/i2p/client/streaming/Connection.java
Normal file
800
apps/streaming/java/src/net/i2p/client/streaming/Connection.java
Normal file
@@ -0,0 +1,800 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.client.I2PSession;
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.data.Destination;
|
||||
import net.i2p.data.SessionTag;
|
||||
import net.i2p.util.Log;
|
||||
import net.i2p.util.SimpleTimer;
|
||||
|
||||
/**
|
||||
* Maintain the state controlling a streaming connection between two
|
||||
* destinations.
|
||||
*
|
||||
*/
|
||||
public class Connection {
|
||||
private I2PAppContext _context;
|
||||
private Log _log;
|
||||
private ConnectionManager _connectionManager;
|
||||
private Destination _remotePeer;
|
||||
private byte _sendStreamId[];
|
||||
private byte _receiveStreamId[];
|
||||
private long _lastSendTime;
|
||||
private long _lastSendId;
|
||||
private boolean _resetReceived;
|
||||
private boolean _connected;
|
||||
private MessageInputStream _inputStream;
|
||||
private MessageOutputStream _outputStream;
|
||||
private SchedulerChooser _chooser;
|
||||
private long _nextSendTime;
|
||||
private long _ackedPackets;
|
||||
private long _createdOn;
|
||||
private long _closeSentOn;
|
||||
private long _closeReceivedOn;
|
||||
private int _unackedPacketsReceived;
|
||||
private long _congestionWindowEnd;
|
||||
private long _highestAckedThrough;
|
||||
/** Packet ID (Long) to PacketLocal for sent but unacked packets */
|
||||
private Map _outboundPackets;
|
||||
private PacketQueue _outboundQueue;
|
||||
private ConnectionPacketHandler _handler;
|
||||
private ConnectionOptions _options;
|
||||
private ConnectionDataReceiver _receiver;
|
||||
private I2PSocketFull _socket;
|
||||
/** set to an error cause if the connection could not be established */
|
||||
private String _connectionError;
|
||||
private boolean _disconnectScheduled;
|
||||
private long _lastReceivedOn;
|
||||
private ActivityTimer _activityTimer;
|
||||
/** window size when we last saw congestion */
|
||||
private int _lastCongestionSeenAt;
|
||||
private boolean _ackSinceCongestion;
|
||||
/** Notify this on connection (or connection failure) */
|
||||
private Object _connectLock;
|
||||
/** how many messages have been resent and not yet ACKed? */
|
||||
private int _activeResends;
|
||||
|
||||
private long _lifetimeBytesSent;
|
||||
private long _lifetimeBytesReceived;
|
||||
private long _lifetimeDupMessageSent;
|
||||
private long _lifetimeDupMessageReceived;
|
||||
|
||||
public static final long MAX_RESEND_DELAY = 60*1000;
|
||||
public static final long MIN_RESEND_DELAY = 20*1000;
|
||||
|
||||
/** wait up to 5 minutes after disconnection so we can ack/close packets */
|
||||
public static int DISCONNECT_TIMEOUT = 5*60*1000;
|
||||
|
||||
/** lets be sane- no more than 32 packets in the air in each dir */
|
||||
public static final int MAX_WINDOW_SIZE = 32;
|
||||
|
||||
public Connection(I2PAppContext ctx, ConnectionManager manager, SchedulerChooser chooser, PacketQueue queue, ConnectionPacketHandler handler) {
|
||||
this(ctx, manager, chooser, queue, handler, null);
|
||||
}
|
||||
public Connection(I2PAppContext ctx, ConnectionManager manager, SchedulerChooser chooser, PacketQueue queue, ConnectionPacketHandler handler, ConnectionOptions opts) {
|
||||
_context = ctx;
|
||||
_log = ctx.logManager().getLog(Connection.class);
|
||||
_receiver = new ConnectionDataReceiver(ctx, this);
|
||||
_inputStream = new MessageInputStream(ctx);
|
||||
_outputStream = new MessageOutputStream(ctx, _receiver);
|
||||
_chooser = chooser;
|
||||
_outboundPackets = new TreeMap();
|
||||
_outboundQueue = queue;
|
||||
_handler = handler;
|
||||
_options = (opts != null ? opts : new ConnectionOptions());
|
||||
_lastSendId = -1;
|
||||
_nextSendTime = -1;
|
||||
_ackedPackets = 0;
|
||||
_createdOn = ctx.clock().now();
|
||||
_closeSentOn = -1;
|
||||
_closeReceivedOn = -1;
|
||||
_unackedPacketsReceived = 0;
|
||||
_congestionWindowEnd = 0;
|
||||
_highestAckedThrough = -1;
|
||||
_lastCongestionSeenAt = MAX_WINDOW_SIZE;
|
||||
_connectionManager = manager;
|
||||
_resetReceived = false;
|
||||
_connected = true;
|
||||
_disconnectScheduled = false;
|
||||
_lastReceivedOn = -1;
|
||||
_activityTimer = new ActivityTimer();
|
||||
_ackSinceCongestion = true;
|
||||
_connectLock = new Object();
|
||||
_activeResends = 0;
|
||||
_context.statManager().createRateStat("stream.con.windowSizeAtCongestion", "How large was our send window when we send a dup?", "Stream", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
|
||||
}
|
||||
|
||||
public long getNextOutboundPacketNum() {
|
||||
synchronized (this) {
|
||||
return ++_lastSendId;
|
||||
}
|
||||
}
|
||||
|
||||
void closeReceived() {
|
||||
setCloseReceivedOn(_context.clock().now());
|
||||
_inputStream.closeReceived();
|
||||
synchronized (_connectLock) { _connectLock.notifyAll(); }
|
||||
}
|
||||
|
||||
/**
|
||||
* Block until there is an open outbound packet slot or the write timeout
|
||||
* expires.
|
||||
*
|
||||
* @return true if the packet should be sent
|
||||
*/
|
||||
boolean packetSendChoke(long timeoutMs) {
|
||||
if (false) return true;
|
||||
long writeExpire = timeoutMs;
|
||||
while (true) {
|
||||
long timeLeft = writeExpire - _context.clock().now();
|
||||
synchronized (_outboundPackets) {
|
||||
if (_outboundPackets.size() >= _options.getWindowSize()) {
|
||||
if (writeExpire > 0) {
|
||||
if (timeLeft <= 0) {
|
||||
_log.error("Outbound window is full of " + _outboundPackets.size()
|
||||
+ " and we've waited too long (" + writeExpire + "ms)");
|
||||
return false;
|
||||
}
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Outbound window is full (" + _outboundPackets.size() + "/" + _options.getWindowSize() + "), waiting " + timeLeft);
|
||||
try { _outboundPackets.wait(timeLeft); } catch (InterruptedException ie) {}
|
||||
} else {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Outbound window is full (" + _outboundPackets.size() + "), waiting indefinitely");
|
||||
try { _outboundPackets.wait(); } catch (InterruptedException ie) {}
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ackImmediately() {
|
||||
_receiver.send(null, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush any data that we can
|
||||
*/
|
||||
void sendAvailable() {
|
||||
// this grabs the data, builds a packet, and queues it up via sendPacket
|
||||
try {
|
||||
_outputStream.flushAvailable(_receiver, false);
|
||||
} catch (IOException ioe) {
|
||||
if (_log.shouldLog(Log.ERROR))
|
||||
_log.error("Error flushing available", ioe);
|
||||
}
|
||||
}
|
||||
|
||||
void sendPacket(PacketLocal packet) {
|
||||
if (packet == null) return;
|
||||
|
||||
setNextSendTime(-1);
|
||||
_unackedPacketsReceived = 0;
|
||||
if (_options.getRequireFullySigned()) {
|
||||
packet.setFlag(Packet.FLAG_SIGNATURE_INCLUDED);
|
||||
packet.setFlag(Packet.FLAG_SIGNATURE_REQUESTED);
|
||||
}
|
||||
|
||||
boolean ackOnly = false;
|
||||
|
||||
if ( (packet.getSequenceNum() == 0) && (!packet.isFlagSet(Packet.FLAG_SYNCHRONIZE)) ) {
|
||||
ackOnly = true;
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("No resend for " + packet);
|
||||
} else {
|
||||
int remaining = 0;
|
||||
synchronized (_outboundPackets) {
|
||||
_outboundPackets.put(new Long(packet.getSequenceNum()), packet);
|
||||
remaining = _options.getWindowSize() - _outboundPackets.size() ;
|
||||
_outboundPackets.notifyAll();
|
||||
}
|
||||
if (remaining < 0)
|
||||
remaining = 0;
|
||||
if (packet.isFlagSet(Packet.FLAG_CLOSE) || (remaining < 2)) {
|
||||
packet.setOptionalDelay(0);
|
||||
} else {
|
||||
int delay = _options.getRTT() / 2;
|
||||
packet.setOptionalDelay(delay);
|
||||
_log.debug("Requesting ack delay of " + delay + "ms for packet " + packet);
|
||||
}
|
||||
packet.setFlag(Packet.FLAG_DELAY_REQUESTED);
|
||||
|
||||
long timeout = (_options.getRTT() < MIN_RESEND_DELAY ? MIN_RESEND_DELAY : _options.getRTT());
|
||||
if (timeout > MAX_RESEND_DELAY)
|
||||
timeout = MAX_RESEND_DELAY;
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Resend in " + timeout + " for " + packet);
|
||||
|
||||
SimpleTimer.getInstance().addEvent(new ResendPacketEvent(packet), timeout);
|
||||
}
|
||||
|
||||
_lastSendTime = _context.clock().now();
|
||||
_outboundQueue.enqueue(packet);
|
||||
resetActivityTimer();
|
||||
|
||||
if (ackOnly) {
|
||||
// ACK only, don't schedule this packet for retries
|
||||
// however, if we are running low on sessionTags we want to send
|
||||
// something that will get a reply so that we can deliver some new tags -
|
||||
// ACKs don't get ACKed, but pings do.
|
||||
if ( (packet.getTagsSent() != null) && (packet.getTagsSent().size() > 0) ) {
|
||||
_log.warn("Sending a ping since the ACK we just sent has " + packet.getTagsSent().size() + " tags");
|
||||
_connectionManager.ping(_remotePeer, _options.getRTT()*2, false, packet.getKeyUsed(), packet.getTagsSent(), new PingNotifier());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PingNotifier implements ConnectionManager.PingNotifier {
|
||||
private long _startedPingOn;
|
||||
public PingNotifier() {
|
||||
_startedPingOn = _context.clock().now();
|
||||
}
|
||||
public void pingComplete(boolean ok) {
|
||||
long time = _context.clock().now()-_startedPingOn;
|
||||
if (ok)
|
||||
_options.updateRTT((int)time);
|
||||
else
|
||||
_options.updateRTT((int)time*2);
|
||||
}
|
||||
}
|
||||
|
||||
List ackPackets(long ackThrough, long nacks[]) {
|
||||
if (nacks == null) {
|
||||
_highestAckedThrough = ackThrough;
|
||||
} else {
|
||||
long lowest = -1;
|
||||
for (int i = 0; i < nacks.length; i++) {
|
||||
if ( (lowest < 0) || (nacks[i] < lowest) )
|
||||
lowest = nacks[i];
|
||||
}
|
||||
if (lowest - 1 > _highestAckedThrough)
|
||||
_highestAckedThrough = lowest - 1;
|
||||
}
|
||||
|
||||
List acked = null;
|
||||
synchronized (_outboundPackets) {
|
||||
for (Iterator iter = _outboundPackets.keySet().iterator(); iter.hasNext(); ) {
|
||||
Long id = (Long)iter.next();
|
||||
if (id.longValue() <= ackThrough) {
|
||||
boolean nacked = false;
|
||||
if (nacks != null) {
|
||||
// linear search since its probably really tiny
|
||||
for (int i = 0; i < nacks.length; i++) {
|
||||
if (nacks[i] == id.longValue()) {
|
||||
nacked = true;
|
||||
break; // NACKed
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!nacked) { // aka ACKed
|
||||
if (acked == null)
|
||||
acked = new ArrayList(1);
|
||||
PacketLocal ackedPacket = (PacketLocal)_outboundPackets.get(id);
|
||||
ackedPacket.ackReceived();
|
||||
acked.add(ackedPacket);
|
||||
}
|
||||
} else {
|
||||
break; // _outboundPackets is ordered
|
||||
}
|
||||
}
|
||||
if (acked != null) {
|
||||
for (int i = 0; i < acked.size(); i++) {
|
||||
PacketLocal p = (PacketLocal)acked.get(i);
|
||||
_outboundPackets.remove(new Long(p.getSequenceNum()));
|
||||
_ackedPackets++;
|
||||
if (p.getNumSends() > 1) {
|
||||
_activeResends--;
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Active resend of " + p + " successful, # active left: " + _activeResends);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( (_outboundPackets.size() <= 0) && (_activeResends != 0) ) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("All outbound packets acked, clearing " + _activeResends);
|
||||
_activeResends = 0;
|
||||
}
|
||||
_outboundPackets.notifyAll();
|
||||
}
|
||||
if ((acked != null) && (acked.size() > 0) )
|
||||
_ackSinceCongestion = true;
|
||||
return acked;
|
||||
}
|
||||
|
||||
void eventOccurred() {
|
||||
_chooser.getScheduler(this).eventOccurred(this);
|
||||
}
|
||||
|
||||
void resetReceived() {
|
||||
_resetReceived = true;
|
||||
_outputStream.streamErrorOccurred(new IOException("Reset received"));
|
||||
_inputStream.streamErrorOccurred(new IOException("Reset received"));
|
||||
_connectionError = "Connection reset";
|
||||
synchronized (_connectLock) { _connectLock.notifyAll(); }
|
||||
}
|
||||
public boolean getResetReceived() { return _resetReceived; }
|
||||
|
||||
public boolean getIsConnected() { return _connected; }
|
||||
|
||||
void disconnect(boolean cleanDisconnect) {
|
||||
disconnect(cleanDisconnect, true);
|
||||
}
|
||||
void disconnect(boolean cleanDisconnect, boolean removeFromConMgr) {
|
||||
if (!_connected) return;
|
||||
_connected = false;
|
||||
synchronized (_connectLock) { _connectLock.notifyAll(); }
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Disconnecting " + toString(), new Exception("discon"));
|
||||
|
||||
if (cleanDisconnect) {
|
||||
// send close packets and schedule stuff...
|
||||
_outputStream.closeInternal();
|
||||
_inputStream.close();
|
||||
} else {
|
||||
doClose();
|
||||
boolean tagsCancelled = false;
|
||||
synchronized (_outboundPackets) {
|
||||
for (Iterator iter = _outboundPackets.values().iterator(); iter.hasNext(); ) {
|
||||
PacketLocal pl = (PacketLocal)iter.next();
|
||||
if ( (pl.getTagsSent() != null) && (pl.getTagsSent().size() > 0) )
|
||||
tagsCancelled = true;
|
||||
pl.cancelled();
|
||||
}
|
||||
_outboundPackets.clear();
|
||||
_outboundPackets.notifyAll();
|
||||
}
|
||||
if (tagsCancelled)
|
||||
_context.sessionKeyManager().failTags(_remotePeer.getPublicKey());
|
||||
}
|
||||
if (removeFromConMgr) {
|
||||
if (!_disconnectScheduled) {
|
||||
_disconnectScheduled = true;
|
||||
SimpleTimer.getInstance().addEvent(new DisconnectEvent(), DISCONNECT_TIMEOUT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void disconnectComplete() {
|
||||
_connected = false;
|
||||
if (_socket != null)
|
||||
_socket.destroy();
|
||||
_socket = null;
|
||||
_inputStream = null;
|
||||
if (_outputStream != null)
|
||||
_outputStream.destroy();
|
||||
_outputStream = null;
|
||||
_outboundQueue = null;
|
||||
_handler = null;
|
||||
if (_receiver != null)
|
||||
_receiver.destroy();
|
||||
_receiver = null;
|
||||
if (_activityTimer != null)
|
||||
SimpleTimer.getInstance().addEvent(_activityTimer, 1);
|
||||
_activityTimer = null;
|
||||
|
||||
if (!_disconnectScheduled) {
|
||||
_disconnectScheduled = true;
|
||||
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("Connection disconnect complete from dead, drop the con "
|
||||
+ toString());
|
||||
_connectionManager.removeConnection(this);
|
||||
}
|
||||
|
||||
boolean tagsCancelled = false;
|
||||
synchronized (_outboundPackets) {
|
||||
for (Iterator iter = _outboundPackets.values().iterator(); iter.hasNext(); ) {
|
||||
PacketLocal pl = (PacketLocal)iter.next();
|
||||
if ( (pl.getTagsSent() != null) && (pl.getTagsSent().size() > 0) )
|
||||
tagsCancelled = true;
|
||||
pl.cancelled();
|
||||
}
|
||||
_outboundPackets.clear();
|
||||
_outboundPackets.notifyAll();
|
||||
}
|
||||
if (tagsCancelled)
|
||||
_context.sessionKeyManager().failTags(_remotePeer.getPublicKey());
|
||||
|
||||
}
|
||||
|
||||
private class DisconnectEvent implements SimpleTimer.TimedEvent {
|
||||
public DisconnectEvent() {
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("Connection disconnect timer initiated: 5 minutes to drop "
|
||||
+ Connection.this.toString());
|
||||
}
|
||||
public void timeReached() {
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("Connection disconnect timer complete, drop the con "
|
||||
+ Connection.this.toString());
|
||||
_connectionManager.removeConnection(Connection.this);
|
||||
}
|
||||
}
|
||||
|
||||
private void doClose() {
|
||||
_outputStream.streamErrorOccurred(new IOException("Hard disconnect"));
|
||||
_inputStream.closeReceived();
|
||||
synchronized (_connectLock) { _connectLock.notifyAll(); }
|
||||
}
|
||||
|
||||
/** who are we talking with */
|
||||
public Destination getRemotePeer() { return _remotePeer; }
|
||||
public void setRemotePeer(Destination peer) { _remotePeer = peer; }
|
||||
|
||||
/** what stream do we send data to the peer on? */
|
||||
public byte[] getSendStreamId() { return _sendStreamId; }
|
||||
public void setSendStreamId(byte[] id) { _sendStreamId = id; }
|
||||
|
||||
/** stream the peer sends data to us on. (may be null) */
|
||||
public byte[] getReceiveStreamId() { return _receiveStreamId; }
|
||||
public void setReceiveStreamId(byte[] id) {
|
||||
_receiveStreamId = id;
|
||||
synchronized (_connectLock) { _connectLock.notifyAll(); }
|
||||
}
|
||||
|
||||
/** when did we last send anything to the peer? */
|
||||
public long getLastSendTime() { return _lastSendTime; }
|
||||
public void setLastSendTime(long when) { _lastSendTime = when; }
|
||||
|
||||
/** what was the last packet Id sent to the peer? */
|
||||
public long getLastSendId() { return _lastSendId; }
|
||||
public void setLastSendId(long id) { _lastSendId = id; }
|
||||
|
||||
public ConnectionOptions getOptions() { return _options; }
|
||||
public void setOptions(ConnectionOptions opts) { _options = opts; }
|
||||
|
||||
public I2PSession getSession() { return _connectionManager.getSession(); }
|
||||
public I2PSocketFull getSocket() { return _socket; }
|
||||
public void setSocket(I2PSocketFull socket) { _socket = socket; }
|
||||
|
||||
public String getConnectionError() { return _connectionError; }
|
||||
public void setConnectionError(String err) { _connectionError = err; }
|
||||
|
||||
public long getLifetime() {
|
||||
if (_closeSentOn <= 0)
|
||||
return _context.clock().now() - _createdOn;
|
||||
else
|
||||
return _closeSentOn - _createdOn;
|
||||
}
|
||||
|
||||
public ConnectionPacketHandler getPacketHandler() { return _handler; }
|
||||
|
||||
public long getLifetimeBytesSent() { return _lifetimeBytesSent; }
|
||||
public long getLifetimeBytesReceived() { return _lifetimeBytesReceived; }
|
||||
public long getLifetimeDupMessagesSent() { return _lifetimeDupMessageSent; }
|
||||
public long getLifetimeDupMessagesReceived() { return _lifetimeDupMessageReceived; }
|
||||
public void incrementBytesSent(int bytes) { _lifetimeBytesSent += bytes; }
|
||||
public void incrementDupMessagesSent(int msgs) { _lifetimeDupMessageSent += msgs; }
|
||||
public void incrementBytesReceived(int bytes) { _lifetimeBytesReceived += bytes; }
|
||||
public void incrementDupMessagesReceived(int msgs) { _lifetimeDupMessageReceived += msgs; }
|
||||
|
||||
/**
|
||||
* Time when the scheduler next want to send a packet, or -1 if
|
||||
* never. This should be set when we want to send on timeout, for
|
||||
* instance, or want to delay an ACK.
|
||||
*/
|
||||
public long getNextSendTime() { return _nextSendTime; }
|
||||
public void setNextSendTime(long when) {
|
||||
if (_nextSendTime >= 0) {
|
||||
if (when < _nextSendTime)
|
||||
_nextSendTime = when;
|
||||
} else {
|
||||
_nextSendTime = when;
|
||||
}
|
||||
|
||||
if (_nextSendTime >= 0) {
|
||||
long max = _context.clock().now() + _options.getSendAckDelay();
|
||||
if (max < _nextSendTime)
|
||||
_nextSendTime = max;
|
||||
}
|
||||
|
||||
if (_log.shouldLog(Log.DEBUG) && false) {
|
||||
if (_nextSendTime <= 0)
|
||||
_log.debug("set next send time to an unknown time", new Exception(toString()));
|
||||
else
|
||||
_log.debug("set next send time to " + (_nextSendTime-_context.clock().now()) + "ms from now", new Exception(toString()));
|
||||
}
|
||||
}
|
||||
|
||||
public long getAckedPackets() { return _ackedPackets; }
|
||||
public long getCreatedOn() { return _createdOn; }
|
||||
public long getCloseSentOn() { return _closeSentOn; }
|
||||
public void setCloseSentOn(long when) { _closeSentOn = when; }
|
||||
public long getCloseReceivedOn() { return _closeReceivedOn; }
|
||||
public void setCloseReceivedOn(long when) { _closeReceivedOn = when; }
|
||||
|
||||
public void incrementUnackedPacketsReceived() { _unackedPacketsReceived++; }
|
||||
public int getUnackedPacketsReceived() { return _unackedPacketsReceived; }
|
||||
public int getUnackedPacketsSent() {
|
||||
synchronized (_outboundPackets) {
|
||||
return _outboundPackets.size();
|
||||
}
|
||||
}
|
||||
|
||||
public long getCongestionWindowEnd() { return _congestionWindowEnd; }
|
||||
public void setCongestionWindowEnd(long endMsg) { _congestionWindowEnd = endMsg; }
|
||||
public long getHighestAckedThrough() { return _highestAckedThrough; }
|
||||
public void setHighestAckedThrough(long msgNum) { _highestAckedThrough = msgNum; }
|
||||
|
||||
public long getLastActivityOn() {
|
||||
return (_lastSendTime > _lastReceivedOn ? _lastSendTime : _lastReceivedOn);
|
||||
}
|
||||
|
||||
public int getLastCongestionSeenAt() { return _lastCongestionSeenAt; }
|
||||
|
||||
void congestionOccurred() {
|
||||
// if we hit congestion and e.g. 5 packets are resent,
|
||||
// dont set the size to (winSize >> 4). only set the
|
||||
if (_ackSinceCongestion) {
|
||||
_lastCongestionSeenAt = _options.getWindowSize();
|
||||
_ackSinceCongestion = false;
|
||||
}
|
||||
}
|
||||
|
||||
void packetReceived() {
|
||||
_lastReceivedOn = _context.clock().now();
|
||||
resetActivityTimer();
|
||||
synchronized (_connectLock) { _connectLock.notifyAll(); }
|
||||
}
|
||||
|
||||
/**
|
||||
* wait until a connection is made or the connection fails within the
|
||||
* timeout period, setting the error accordingly.
|
||||
*/
|
||||
void waitForConnect() {
|
||||
long expiration = _context.clock().now() + _options.getConnectTimeout();
|
||||
while (true) {
|
||||
if (_connected && (_receiveStreamId != null) && (_sendStreamId != null) ) {
|
||||
// w00t
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("waitForConnect(): Connected and we have stream IDs");
|
||||
return;
|
||||
}
|
||||
if (_connectionError != null) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("waitForConnect(): connection error found: " + _connectionError);
|
||||
return;
|
||||
}
|
||||
if (!_connected) {
|
||||
_connectionError = "Connection failed";
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("waitForConnect(): not connected");
|
||||
return;
|
||||
}
|
||||
|
||||
long timeLeft = expiration - _context.clock().now();
|
||||
if ( (timeLeft <= 0) && (_options.getConnectTimeout() > 0) ) {
|
||||
if (_connectionError == null) {
|
||||
_connectionError = "Connection timed out";
|
||||
disconnect(false);
|
||||
}
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("waitForConnect(): timed out: " + _connectionError);
|
||||
return;
|
||||
}
|
||||
if (timeLeft > 60*1000)
|
||||
timeLeft = 60*1000;
|
||||
if (_options.getConnectTimeout() <= 0)
|
||||
timeLeft = 60*1000;
|
||||
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("waitForConnect(): wait " + timeLeft);
|
||||
try {
|
||||
synchronized (_connectLock) {
|
||||
_connectLock.wait(timeLeft);
|
||||
}
|
||||
} catch (InterruptedException ie) {}
|
||||
}
|
||||
}
|
||||
|
||||
private void resetActivityTimer() {
|
||||
if (_options.getInactivityTimeout() <= 0) return;
|
||||
if (_activityTimer == null) return;
|
||||
long howLong = _activityTimer.getTimeLeft();
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Resetting the inactivity timer to " + howLong);
|
||||
// this will get rescheduled, and rescheduled, and rescheduled...
|
||||
SimpleTimer.getInstance().addEvent(_activityTimer, howLong);
|
||||
}
|
||||
|
||||
private class ActivityTimer implements SimpleTimer.TimedEvent {
|
||||
public void timeReached() {
|
||||
// uh, nothing more to do...
|
||||
if (!_connected) return;
|
||||
// we got rescheduled already
|
||||
if (getTimeLeft() > 0) return;
|
||||
// these are either going to time out or cause further rescheduling
|
||||
if (getUnackedPacketsSent() > 0) return;
|
||||
// wtf, this shouldn't have been scheduled
|
||||
if (_options.getInactivityTimeout() <= 0) return;
|
||||
// if one of us can't talk...
|
||||
if ( (_closeSentOn > 0) || (_closeReceivedOn > 0) ) return;
|
||||
|
||||
// bugger it, might as well do the hard work now
|
||||
switch (_options.getInactivityAction()) {
|
||||
case ConnectionOptions.INACTIVITY_ACTION_SEND:
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Sending some data due to inactivity");
|
||||
_receiver.send(null, 0, 0, true);
|
||||
break;
|
||||
case ConnectionOptions.INACTIVITY_ACTION_NOOP:
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Inactivity timer expired, but we aint doin' shit");
|
||||
break;
|
||||
case ConnectionOptions.INACTIVITY_ACTION_DISCONNECT:
|
||||
// fall through
|
||||
default:
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Closing connection due to inactivity");
|
||||
disconnect(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public final long getTimeLeft() {
|
||||
if (getLastActivityOn() > 0)
|
||||
return getLastActivityOn() + _options.getInactivityTimeout() - _context.clock().now();
|
||||
else
|
||||
return _createdOn + _options.getInactivityTimeout() - _context.clock().now();
|
||||
}
|
||||
}
|
||||
|
||||
/** stream that the local peer receives data on */
|
||||
public MessageInputStream getInputStream() { return _inputStream; }
|
||||
/** stream that the local peer sends data to the remote peer on */
|
||||
public MessageOutputStream getOutputStream() { return _outputStream; }
|
||||
|
||||
public String toString() {
|
||||
StringBuffer buf = new StringBuffer(128);
|
||||
buf.append("[Connection ");
|
||||
if (_receiveStreamId != null)
|
||||
buf.append(Base64.encode(_receiveStreamId));
|
||||
else
|
||||
buf.append("unknown");
|
||||
buf.append("<-->");
|
||||
if (_sendStreamId != null)
|
||||
buf.append(Base64.encode(_sendStreamId));
|
||||
else
|
||||
buf.append("unknown");
|
||||
buf.append(" wsize: ").append(_options.getWindowSize());
|
||||
buf.append(" cwin: ").append(_congestionWindowEnd - _highestAckedThrough);
|
||||
buf.append(" rtt: ").append(_options.getRTT());
|
||||
buf.append(" unacked outbound: ");
|
||||
synchronized (_outboundPackets) {
|
||||
buf.append(_outboundPackets.size()).append(" [");
|
||||
for (Iterator iter = _outboundPackets.keySet().iterator(); iter.hasNext(); ) {
|
||||
buf.append(((Long)iter.next()).longValue()).append(" ");
|
||||
}
|
||||
buf.append("] ");
|
||||
}
|
||||
buf.append("unacked inbound? ").append(getUnackedPacketsReceived());
|
||||
if (_inputStream != null) {
|
||||
buf.append(" [high ");
|
||||
buf.append(_inputStream.getHighestBlockId());
|
||||
long nacks[] = _inputStream.getNacks();
|
||||
if (nacks != null)
|
||||
for (int i = 0; i < nacks.length; i++)
|
||||
buf.append(" ").append(nacks[i]);
|
||||
buf.append("]");
|
||||
}
|
||||
buf.append("]");
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Coordinate the resends of a given packet
|
||||
*/
|
||||
private class ResendPacketEvent implements SimpleTimer.TimedEvent {
|
||||
private PacketLocal _packet;
|
||||
private boolean _currentIsActiveResend;
|
||||
public ResendPacketEvent(PacketLocal packet) {
|
||||
_packet = packet;
|
||||
_currentIsActiveResend = false;
|
||||
}
|
||||
|
||||
public void timeReached() {
|
||||
if (_packet.getAckTime() > 0)
|
||||
return;
|
||||
|
||||
if (!_connected) {
|
||||
_packet.cancelled();
|
||||
return;
|
||||
}
|
||||
|
||||
//if (_log.shouldLog(Log.DEBUG))
|
||||
// _log.debug("Resend period reached for " + _packet);
|
||||
boolean resend = false;
|
||||
synchronized (_outboundPackets) {
|
||||
if (_outboundPackets.containsKey(new Long(_packet.getSequenceNum())))
|
||||
resend = true;
|
||||
}
|
||||
if ( (resend) && (_packet.getAckTime() < 0) ) {
|
||||
if ( (_activeResends > 0) && (!_currentIsActiveResend) ) {
|
||||
// we want to resend this packet, but there are already active
|
||||
// resends in the air and we dont want to make a bad situation
|
||||
// worse. wait another second
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Delaying resend of " + _packet + " as there are "
|
||||
+ _activeResends + " active resends already in play");
|
||||
SimpleTimer.getInstance().addEvent(ResendPacketEvent.this, 1000);
|
||||
return;
|
||||
}
|
||||
// revamp various fields, in case we need to ack more, etc
|
||||
_inputStream.updateAcks(_packet);
|
||||
_packet.setOptionalDelay(getOptions().getChoke());
|
||||
_packet.setOptionalMaxSize(getOptions().getMaxMessageSize());
|
||||
_packet.setResendDelay(getOptions().getResendDelay());
|
||||
_packet.setReceiveStreamId(_receiveStreamId);
|
||||
_packet.setSendStreamId(_sendStreamId);
|
||||
|
||||
// shrink the window
|
||||
int newWindowSize = getOptions().getWindowSize();
|
||||
congestionOccurred();
|
||||
_context.statManager().addRateData("stream.con.windowSizeAtCongestion", newWindowSize, _packet.getLifetime());
|
||||
newWindowSize /= 2;
|
||||
if (newWindowSize <= 0)
|
||||
newWindowSize = 1;
|
||||
getOptions().setWindowSize(newWindowSize);
|
||||
|
||||
int numSends = _packet.getNumSends() + 1;
|
||||
|
||||
if (numSends == 2) {
|
||||
// first resend for this packet
|
||||
_activeResends++;
|
||||
_currentIsActiveResend = true;
|
||||
}
|
||||
|
||||
// in case things really suck, the other side may have lost thier
|
||||
// session tags (e.g. they restarted), so jump back to ElGamal.
|
||||
int failTagsAt = _options.getMaxResends() - 1;
|
||||
if ( (newWindowSize == 1) && (numSends == failTagsAt) ) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Optimistically failing tags at resend " + numSends);
|
||||
_context.sessionKeyManager().failTags(_remotePeer.getPublicKey());
|
||||
}
|
||||
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Resend packet " + _packet + " time " + numSends +
|
||||
" activeResends: " + _activeResends +
|
||||
" (wsize "
|
||||
+ newWindowSize + " lifetime "
|
||||
+ (_context.clock().now() - _packet.getCreatedOn()) + "ms)");
|
||||
_outboundQueue.enqueue(_packet);
|
||||
|
||||
if (numSends > _options.getMaxResends()) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Too many resends");
|
||||
_packet.cancelled();
|
||||
disconnect(false);
|
||||
} else {
|
||||
//long timeout = _options.getResendDelay() << numSends;
|
||||
long rtt = _options.getRTT();
|
||||
if (rtt < MIN_RESEND_DELAY)
|
||||
rtt = MIN_RESEND_DELAY;
|
||||
long timeout = rtt << (numSends-1);
|
||||
if ( (timeout > MAX_RESEND_DELAY) || (timeout <= 0) )
|
||||
timeout = MAX_RESEND_DELAY;
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Scheduling resend in " + timeout + "ms for " + _packet);
|
||||
SimpleTimer.getInstance().addEvent(ResendPacketEvent.this, timeout);
|
||||
}
|
||||
} else {
|
||||
//if (_log.shouldLog(Log.DEBUG))
|
||||
// _log.debug("Packet acked before resend (resend="+ resend + "): "
|
||||
// + _packet + " on " + Connection.this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.IOException;
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Receive data from the MessageOutputStream, build a packet,
|
||||
* and send it through a connection. The write calls on this
|
||||
* do NOT block, but they also do not necessary imply immediate
|
||||
* delivery, or even the generation of a new packet. This class
|
||||
* is the only one that builds useful outbound Packet objects.
|
||||
*
|
||||
*/
|
||||
class ConnectionDataReceiver implements MessageOutputStream.DataReceiver {
|
||||
private I2PAppContext _context;
|
||||
private Log _log;
|
||||
private Connection _connection;
|
||||
private MessageOutputStream.WriteStatus _dummyStatus;
|
||||
|
||||
public ConnectionDataReceiver(I2PAppContext ctx, Connection con) {
|
||||
_context = ctx;
|
||||
_log = ctx.logManager().getLog(ConnectionDataReceiver.class);
|
||||
_connection = con;
|
||||
_dummyStatus = new DummyStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send some data through the connection, or if there is no new data, this
|
||||
* may generate a packet with a plain ACK/NACK or CLOSE, or nothing whatsoever
|
||||
* if there's nothing new to send.
|
||||
*
|
||||
* @param buf data to be sent - may be null
|
||||
* @param off offset into the buffer to start writing from
|
||||
* @param size how many bytes of the buffer to write (may be 0)
|
||||
* @return an object to allow optional blocking for data acceptance or
|
||||
* delivery.
|
||||
*/
|
||||
public MessageOutputStream.WriteStatus writeData(byte[] buf, int off, int size) {
|
||||
boolean doSend = true;
|
||||
if ( (size <= 0) && (_connection.getLastSendId() >= 0) ) {
|
||||
if (_connection.getOutputStream().getClosed()) {
|
||||
if (_connection.getCloseSentOn() < 0) {
|
||||
doSend = true;
|
||||
} else {
|
||||
// closed, no new data, and we've already sent a close packet
|
||||
doSend = false;
|
||||
}
|
||||
} else {
|
||||
// no new data, not closed, already synchronized
|
||||
doSend = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (_connection.getUnackedPacketsReceived() > 0)
|
||||
doSend = true;
|
||||
|
||||
if (_log.shouldLog(Log.INFO) && !doSend)
|
||||
_log.info("writeData called: size="+size + " doSend=" + doSend
|
||||
+ " unackedReceived: " + _connection.getUnackedPacketsReceived()
|
||||
+ " con: " + _connection, new Exception("write called by"));
|
||||
|
||||
if (doSend) {
|
||||
PacketLocal packet = send(buf, off, size);
|
||||
return packet;
|
||||
} else {
|
||||
return _dummyStatus;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send some data through the connection, attaching any appropriate flags
|
||||
* onto the packet.
|
||||
*
|
||||
* @param buf data to be sent - may be null
|
||||
* @param off offset into the buffer to start writing from
|
||||
* @param size how many bytes of the buffer to write (may be 0)
|
||||
* @return the packet sent
|
||||
*/
|
||||
public PacketLocal send(byte buf[], int off, int size) {
|
||||
return send(buf, off, size, false);
|
||||
}
|
||||
/**
|
||||
* @param buf data to be sent - may be null
|
||||
* @param off offset into the buffer to start writing from
|
||||
* @param size how many bytes of the buffer to write (may be 0)
|
||||
* @param forceIncrement even if the buffer is empty, increment the packetId
|
||||
* so we get an ACK back
|
||||
* @return the packet sent
|
||||
*/
|
||||
public PacketLocal send(byte buf[], int off, int size, boolean forceIncrement) {
|
||||
PacketLocal packet = buildPacket(buf, off, size, forceIncrement);
|
||||
_connection.sendPacket(packet);
|
||||
return packet;
|
||||
}
|
||||
|
||||
private boolean isAckOnly(int size) {
|
||||
boolean ackOnly = ( (size <= 0) && // no data
|
||||
(_connection.getLastSendId() >= 0) && // not a SYN
|
||||
( (!_connection.getOutputStream().getClosed()) || // not a CLOSE
|
||||
(_connection.getOutputStream().getClosed() &&
|
||||
_connection.getCloseSentOn() > 0) )); // or it is a dup CLOSE
|
||||
return ackOnly;
|
||||
}
|
||||
|
||||
private PacketLocal buildPacket(byte buf[], int off, int size, boolean forceIncrement) {
|
||||
boolean ackOnly = isAckOnly(size);
|
||||
PacketLocal packet = new PacketLocal(_context, _connection.getRemotePeer(), _connection);
|
||||
byte data[] = new byte[size];
|
||||
if (size > 0)
|
||||
System.arraycopy(buf, off, data, 0, size);
|
||||
packet.setPayload(data);
|
||||
if (ackOnly && !forceIncrement)
|
||||
packet.setSequenceNum(0);
|
||||
else
|
||||
packet.setSequenceNum(_connection.getNextOutboundPacketNum());
|
||||
packet.setSendStreamId(_connection.getSendStreamId());
|
||||
packet.setReceiveStreamId(_connection.getReceiveStreamId());
|
||||
|
||||
_connection.getInputStream().updateAcks(packet);
|
||||
packet.setOptionalDelay(_connection.getOptions().getChoke());
|
||||
packet.setOptionalMaxSize(_connection.getOptions().getMaxMessageSize());
|
||||
packet.setResendDelay(_connection.getOptions().getResendDelay());
|
||||
|
||||
if (_connection.getOptions().getProfile() == ConnectionOptions.PROFILE_INTERACTIVE)
|
||||
packet.setFlag(Packet.FLAG_PROFILE_INTERACTIVE, true);
|
||||
else
|
||||
packet.setFlag(Packet.FLAG_PROFILE_INTERACTIVE, false);
|
||||
|
||||
packet.setFlag(Packet.FLAG_SIGNATURE_REQUESTED, _connection.getOptions().getRequireFullySigned());
|
||||
|
||||
if ( (!ackOnly) && (packet.getSequenceNum() <= 0) ) {
|
||||
packet.setFlag(Packet.FLAG_SYNCHRONIZE);
|
||||
packet.setOptionalFrom(_connection.getSession().getMyDestination());
|
||||
}
|
||||
|
||||
// don't set the closed flag if this is a plain ACK and there are outstanding
|
||||
// packets sent, otherwise the other side could receive the CLOSE prematurely,
|
||||
// since this ACK could arrive before the unacked payload message.
|
||||
if (_connection.getOutputStream().getClosed() &&
|
||||
( (size > 0) || (_connection.getUnackedPacketsSent() <= 0) ) ) {
|
||||
packet.setFlag(Packet.FLAG_CLOSE);
|
||||
_connection.setCloseSentOn(_context.clock().now());
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Closed is set for a new packet on " + _connection + ": " + packet);
|
||||
} else {
|
||||
//if (_log.shouldLog(Log.DEBUG))
|
||||
// _log.debug("Closed is not set for a new packet on " + _connection + ": " + packet);
|
||||
}
|
||||
return packet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used if no new packet was sent.
|
||||
*/
|
||||
private static final class DummyStatus implements MessageOutputStream.WriteStatus {
|
||||
public final void waitForAccept(int maxWaitMs) { return; }
|
||||
public final void waitForCompletion(int maxWaitMs) { return; }
|
||||
public final boolean writeAccepted() { return true; }
|
||||
public final boolean writeFailed() { return false; }
|
||||
public final boolean writeSuccessful() { return true; }
|
||||
}
|
||||
|
||||
void destroy() {
|
||||
_connection = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.util.Log;
|
||||
import net.i2p.util.SimpleTimer;
|
||||
|
||||
/**
|
||||
* Receive new connection attempts
|
||||
*/
|
||||
class ConnectionHandler {
|
||||
private I2PAppContext _context;
|
||||
private Log _log;
|
||||
private ConnectionManager _manager;
|
||||
private List _synQueue;
|
||||
private boolean _active;
|
||||
private int _acceptTimeout;
|
||||
|
||||
/** max time after receiveNewSyn() and before the matched accept() */
|
||||
private static final int DEFAULT_ACCEPT_TIMEOUT = 3*1000;
|
||||
|
||||
/** Creates a new instance of ConnectionHandler */
|
||||
public ConnectionHandler(I2PAppContext context, ConnectionManager mgr) {
|
||||
_context = context;
|
||||
_log = context.logManager().getLog(ConnectionHandler.class);
|
||||
_manager = mgr;
|
||||
_synQueue = new ArrayList(5);
|
||||
_active = false;
|
||||
_acceptTimeout = DEFAULT_ACCEPT_TIMEOUT;
|
||||
}
|
||||
|
||||
public void setActive(boolean active) { _active = active; }
|
||||
public boolean getActive() { return _active; }
|
||||
|
||||
public void receiveNewSyn(Packet packet) {
|
||||
if (!_active) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Dropping new SYN request, as we're not listening");
|
||||
sendReset(packet);
|
||||
return;
|
||||
}
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Receive new SYN: " + packet + ": timeout in " + _acceptTimeout);
|
||||
SimpleTimer.getInstance().addEvent(new TimeoutSyn(packet), _acceptTimeout);
|
||||
synchronized (_synQueue) {
|
||||
_synQueue.add(packet);
|
||||
_synQueue.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive an incoming connection (built from a received SYN)
|
||||
*
|
||||
* @param timeoutMs max amount of time to wait for a connection (if less
|
||||
* than 1ms, wait indefinitely)
|
||||
* @return connection received, or null if there was a timeout or the
|
||||
* handler was shut down
|
||||
*/
|
||||
public Connection accept(long timeoutMs) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Accept("+ timeoutMs+") called");
|
||||
|
||||
long expiration = timeoutMs + _context.clock().now();
|
||||
while (true) {
|
||||
if ( (timeoutMs > 0) && (expiration < _context.clock().now()) )
|
||||
return null;
|
||||
if (!_active)
|
||||
return null;
|
||||
|
||||
Packet syn = null;
|
||||
synchronized (_synQueue) {
|
||||
while ( _active && (_synQueue.size() <= 0) ) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Accept("+ timeoutMs+"): active=" + _active + " queue: "
|
||||
+ _synQueue.size());
|
||||
if (timeoutMs <= 0) {
|
||||
try { _synQueue.wait(); } catch (InterruptedException ie) {}
|
||||
} else {
|
||||
long remaining = expiration - _context.clock().now();
|
||||
if (remaining < 0)
|
||||
break;
|
||||
try { _synQueue.wait(remaining); } catch (InterruptedException ie) {}
|
||||
}
|
||||
}
|
||||
if (_active && _synQueue.size() > 0) {
|
||||
syn = (Packet)_synQueue.remove(0);
|
||||
}
|
||||
}
|
||||
|
||||
if (syn != null) {
|
||||
// deal with forged / invalid syn packets
|
||||
Connection con = _manager.receiveConnection(syn);
|
||||
if (con != null)
|
||||
return con;
|
||||
}
|
||||
// keep looping...
|
||||
}
|
||||
}
|
||||
|
||||
private void sendReset(Packet packet) {
|
||||
boolean ok = packet.verifySignature(_context, packet.getOptionalFrom(), null);
|
||||
if (!ok) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Received a spoofed SYN packet: they said they were " + packet.getOptionalFrom());
|
||||
return;
|
||||
}
|
||||
PacketLocal reply = new PacketLocal(_context, packet.getOptionalFrom());
|
||||
reply.setFlag(Packet.FLAG_RESET);
|
||||
reply.setFlag(Packet.FLAG_SIGNATURE_INCLUDED);
|
||||
reply.setAckThrough(packet.getSequenceNum());
|
||||
reply.setSendStreamId(packet.getReceiveStreamId());
|
||||
reply.setReceiveStreamId(null);
|
||||
reply.setOptionalFrom(_manager.getSession().getMyDestination());
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Sending RST: " + reply + " because of " + packet);
|
||||
// this just sends the packet - no retries or whatnot
|
||||
_manager.getPacketQueue().enqueue(reply);
|
||||
}
|
||||
|
||||
private class TimeoutSyn implements SimpleTimer.TimedEvent {
|
||||
private Packet _synPacket;
|
||||
public TimeoutSyn(Packet packet) {
|
||||
_synPacket = packet;
|
||||
}
|
||||
|
||||
public void timeReached() {
|
||||
boolean removed = false;
|
||||
synchronized (_synQueue) {
|
||||
removed = _synQueue.remove(_synPacket);
|
||||
}
|
||||
|
||||
if (removed) {
|
||||
// timeout - send RST
|
||||
sendReset(_synPacket);
|
||||
} else {
|
||||
// handled. noop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.I2PException;
|
||||
import net.i2p.client.I2PSession;
|
||||
import net.i2p.data.ByteArray;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.data.Destination;
|
||||
import net.i2p.data.SessionKey;
|
||||
import net.i2p.util.SimpleTimer;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Coordinate all of the connections for a single local destination.
|
||||
*
|
||||
*
|
||||
*/
|
||||
public class ConnectionManager {
|
||||
private I2PAppContext _context;
|
||||
private Log _log;
|
||||
private I2PSession _session;
|
||||
private MessageHandler _messageHandler;
|
||||
private PacketHandler _packetHandler;
|
||||
private ConnectionHandler _connectionHandler;
|
||||
private PacketQueue _outboundQueue;
|
||||
private SchedulerChooser _schedulerChooser;
|
||||
private ConnectionPacketHandler _conPacketHandler;
|
||||
/** Inbound stream ID (ByteArray) to Connection map */
|
||||
private Map _connectionByInboundId;
|
||||
/** Ping ID (ByteArray) to PingRequest */
|
||||
private Map _pendingPings;
|
||||
private boolean _allowIncoming;
|
||||
private int _maxConcurrentStreams;
|
||||
private volatile int _numWaiting;
|
||||
private Object _connectionLock;
|
||||
|
||||
public ConnectionManager(I2PAppContext context, I2PSession session, int maxConcurrent) {
|
||||
_context = context;
|
||||
_log = context.logManager().getLog(ConnectionManager.class);
|
||||
_connectionByInboundId = new HashMap(32);
|
||||
_pendingPings = new HashMap(4);
|
||||
_connectionLock = new Object();
|
||||
_messageHandler = new MessageHandler(context, this);
|
||||
_packetHandler = new PacketHandler(context, this);
|
||||
_connectionHandler = new ConnectionHandler(context, this);
|
||||
_schedulerChooser = new SchedulerChooser(context);
|
||||
_conPacketHandler = new ConnectionPacketHandler(context);
|
||||
_session = session;
|
||||
session.setSessionListener(_messageHandler);
|
||||
_outboundQueue = new PacketQueue(context, session, this);
|
||||
_allowIncoming = false;
|
||||
_maxConcurrentStreams = maxConcurrent;
|
||||
_numWaiting = 0;
|
||||
_context.statManager().createRateStat("stream.con.lifetimeMessagesSent", "How many messages do we send on a stream?", "Stream", new long[] { 60*60*1000, 24*60*60*1000 });
|
||||
_context.statManager().createRateStat("stream.con.lifetimeMessagesReceived", "How many messages do we receive on a stream?", "Stream", new long[] { 60*60*1000, 24*60*60*1000 });
|
||||
_context.statManager().createRateStat("stream.con.lifetimeBytesSent", "How many bytes do we send on a stream?", "Stream", new long[] { 60*60*1000, 24*60*60*1000 });
|
||||
_context.statManager().createRateStat("stream.con.lifetimeBytesReceived", "How many bytes do we receive on a stream?", "Stream", new long[] { 60*60*1000, 24*60*60*1000 });
|
||||
_context.statManager().createRateStat("stream.con.lifetimeDupMessagesSent", "How many duplicate messages do we send on a stream?", "Stream", new long[] { 60*60*1000, 24*60*60*1000 });
|
||||
_context.statManager().createRateStat("stream.con.lifetimeDupMessagesReceived", "How many duplicate messages do we receive on a stream?", "Stream", new long[] { 60*60*1000, 24*60*60*1000 });
|
||||
_context.statManager().createRateStat("stream.con.lifetimeRTT", "What is the final RTT when a stream closes?", "Stream", new long[] { 60*60*1000, 24*60*60*1000 });
|
||||
_context.statManager().createRateStat("stream.con.lifetimeCongestionSeenAt", "When was the last congestion seen at when a stream closes?", "Stream", new long[] { 60*60*1000, 24*60*60*1000 });
|
||||
_context.statManager().createRateStat("stream.con.lifetimeSendWindowSize", "What is the final send window size when a stream closes?", "Stream", new long[] { 60*60*1000, 24*60*60*1000 });
|
||||
}
|
||||
|
||||
Connection getConnectionByInboundId(byte[] id) {
|
||||
synchronized (_connectionLock) {
|
||||
return (Connection)_connectionByInboundId.get(new ByteArray(id));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* not guaranteed to be unique, but in case we receive more than one packet
|
||||
* on an inbound connection that we havent ack'ed yet...
|
||||
*/
|
||||
Connection getConnectionByOutboundId(byte[] id) {
|
||||
synchronized (_connectionLock) {
|
||||
for (Iterator iter = _connectionByInboundId.values().iterator(); iter.hasNext(); ) {
|
||||
Connection con = (Connection)iter.next();
|
||||
if (DataHelper.eq(con.getSendStreamId(), id))
|
||||
return con;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setAllowIncomingConnections(boolean allow) {
|
||||
_connectionHandler.setActive(allow);
|
||||
}
|
||||
/** should we acceot connections, or just reject everyone? */
|
||||
public boolean getAllowIncomingConnections() {
|
||||
return _connectionHandler.getActive();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new connection based on the SYN packet we received.
|
||||
*
|
||||
* @return created Connection with the packet's data already delivered to
|
||||
* it, or null if the syn's streamId was already taken
|
||||
*/
|
||||
public Connection receiveConnection(Packet synPacket) {
|
||||
Connection con = new Connection(_context, this, _schedulerChooser, _outboundQueue, _conPacketHandler);
|
||||
byte receiveId[] = new byte[4];
|
||||
_context.random().nextBytes(receiveId);
|
||||
boolean reject = false;
|
||||
synchronized (_connectionLock) {
|
||||
if (locked_tooManyStreams()) {
|
||||
reject = true;
|
||||
} else {
|
||||
while (true) {
|
||||
ByteArray ba = new ByteArray(receiveId);
|
||||
Connection oldCon = (Connection)_connectionByInboundId.put(ba, con);
|
||||
if (oldCon == null) {
|
||||
break;
|
||||
} else {
|
||||
_connectionByInboundId.put(ba, oldCon);
|
||||
// receiveId already taken, try another
|
||||
_context.random().nextBytes(receiveId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (reject) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Refusing connection since we have exceeded our max of "
|
||||
+ _maxConcurrentStreams + " connections");
|
||||
PacketLocal reply = new PacketLocal(_context, synPacket.getOptionalFrom());
|
||||
reply.setFlag(Packet.FLAG_RESET);
|
||||
reply.setFlag(Packet.FLAG_SIGNATURE_INCLUDED);
|
||||
reply.setAckThrough(synPacket.getSequenceNum());
|
||||
reply.setSendStreamId(synPacket.getReceiveStreamId());
|
||||
reply.setReceiveStreamId(null);
|
||||
reply.setOptionalFrom(_session.getMyDestination());
|
||||
// this just sends the packet - no retries or whatnot
|
||||
_outboundQueue.enqueue(reply);
|
||||
return null;
|
||||
}
|
||||
|
||||
con.setReceiveStreamId(receiveId);
|
||||
try {
|
||||
con.getPacketHandler().receivePacket(synPacket, con);
|
||||
} catch (I2PException ie) {
|
||||
synchronized (_connectionLock) {
|
||||
_connectionByInboundId.remove(new ByteArray(receiveId));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
_context.statManager().addRateData("stream.connectionReceived", 1, 0);
|
||||
return con;
|
||||
}
|
||||
|
||||
private static final long DEFAULT_STREAM_DELAY_MAX = 10*1000;
|
||||
|
||||
/**
|
||||
* Build a new connection to the given peer. This blocks if there is no
|
||||
* connection delay, otherwise it returns immediately.
|
||||
*
|
||||
* @return new connection, or null if we have exceeded our limit
|
||||
*/
|
||||
public Connection connect(Destination peer, ConnectionOptions opts) {
|
||||
Connection con = null;
|
||||
byte receiveId[] = new byte[4];
|
||||
_context.random().nextBytes(receiveId);
|
||||
long expiration = _context.clock().now() + opts.getConnectTimeout();
|
||||
if (opts.getConnectTimeout() <= 0)
|
||||
expiration = _context.clock().now() + DEFAULT_STREAM_DELAY_MAX;
|
||||
_numWaiting++;
|
||||
while (true) {
|
||||
long remaining = expiration - _context.clock().now();
|
||||
if (remaining <= 0) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Refusing to connect since we have exceeded our max of "
|
||||
+ _maxConcurrentStreams + " connections");
|
||||
_numWaiting--;
|
||||
return null;
|
||||
}
|
||||
boolean reject = false;
|
||||
synchronized (_connectionLock) {
|
||||
if (locked_tooManyStreams()) {
|
||||
// allow a full buffer of pending/waiting streams
|
||||
if (_numWaiting > _maxConcurrentStreams) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Refusing connection since we have exceeded our max of "
|
||||
+ _maxConcurrentStreams + " and there are " + _numWaiting
|
||||
+ " waiting already");
|
||||
_numWaiting--;
|
||||
return null;
|
||||
}
|
||||
|
||||
// no remaining streams, lets wait a bit
|
||||
try { _connectionLock.wait(remaining); } catch (InterruptedException ie) {}
|
||||
} else {
|
||||
con = new Connection(_context, this, _schedulerChooser, _outboundQueue, _conPacketHandler, opts);
|
||||
con.setRemotePeer(peer);
|
||||
|
||||
ByteArray ba = new ByteArray(receiveId);
|
||||
while (_connectionByInboundId.containsKey(ba)) {
|
||||
_context.random().nextBytes(receiveId);
|
||||
}
|
||||
_connectionByInboundId.put(ba, con);
|
||||
break; // stop looping as a psuedo-wait
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ok we're in...
|
||||
con.setReceiveStreamId(receiveId);
|
||||
con.eventOccurred();
|
||||
|
||||
_log.debug("Connect() conDelay = " + opts.getConnectDelay());
|
||||
if (opts.getConnectDelay() <= 0) {
|
||||
con.waitForConnect();
|
||||
}
|
||||
if (_numWaiting > 0)
|
||||
_numWaiting--;
|
||||
|
||||
_context.statManager().addRateData("stream.connectionCreated", 1, 0);
|
||||
return con;
|
||||
}
|
||||
|
||||
private boolean locked_tooManyStreams() {
|
||||
int active = 0;
|
||||
for (Iterator iter = _connectionByInboundId.values().iterator(); iter.hasNext(); ) {
|
||||
Connection con = (Connection)iter.next();
|
||||
if (con.getIsConnected())
|
||||
active++;
|
||||
}
|
||||
|
||||
if ( (_connectionByInboundId.size() > 100) && (_log.shouldLog(Log.INFO)) )
|
||||
_log.info("More than 100 connections! " + active
|
||||
+ " total: " + _connectionByInboundId.size());
|
||||
|
||||
if (_maxConcurrentStreams <= 0) return false;
|
||||
if (_connectionByInboundId.size() < _maxConcurrentStreams) return false;
|
||||
return (active >= _maxConcurrentStreams);
|
||||
}
|
||||
|
||||
public MessageHandler getMessageHandler() { return _messageHandler; }
|
||||
public PacketHandler getPacketHandler() { return _packetHandler; }
|
||||
public ConnectionHandler getConnectionHandler() { return _connectionHandler; }
|
||||
public I2PSession getSession() { return _session; }
|
||||
public PacketQueue getPacketQueue() { return _outboundQueue; }
|
||||
|
||||
/**
|
||||
* Something b0rked hard, so kill all of our connections without mercy.
|
||||
* Don't bother sending close packets.
|
||||
*
|
||||
*/
|
||||
public void disconnectAllHard() {
|
||||
synchronized (_connectionLock) {
|
||||
for (Iterator iter = _connectionByInboundId.values().iterator(); iter.hasNext(); ) {
|
||||
Connection con = (Connection)iter.next();
|
||||
con.disconnect(false, false);
|
||||
}
|
||||
_connectionByInboundId.clear();
|
||||
_connectionLock.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop the (already closed) connection on the floor.
|
||||
*
|
||||
*/
|
||||
public void removeConnection(Connection con) {
|
||||
boolean removed = false;
|
||||
synchronized (_connectionLock) {
|
||||
Object o = _connectionByInboundId.remove(new ByteArray(con.getReceiveStreamId()));
|
||||
removed = (o == con);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Connection removed? " + removed + " remaining: "
|
||||
+ _connectionByInboundId.size() + ": " + con);
|
||||
if (!removed && _log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Failed to remove " + con +"\n" + _connectionByInboundId.values());
|
||||
_connectionLock.notifyAll();
|
||||
}
|
||||
if (removed) {
|
||||
_context.statManager().addRateData("stream.con.lifetimeMessagesSent", con.getLastSendId(), con.getLifetime());
|
||||
_context.statManager().addRateData("stream.con.lifetimeMessagesReceived", con.getHighestAckedThrough(), con.getLifetime());
|
||||
_context.statManager().addRateData("stream.con.lifetimeBytesSent", con.getLifetimeBytesSent(), con.getLifetime());
|
||||
_context.statManager().addRateData("stream.con.lifetimeBytesReceived", con.getLifetimeBytesReceived(), con.getLifetime());
|
||||
_context.statManager().addRateData("stream.con.lifetimeDupMessagesSent", con.getLifetimeDupMessagesSent(), con.getLifetime());
|
||||
_context.statManager().addRateData("stream.con.lifetimeDupMessagesReceived", con.getLifetimeDupMessagesReceived(), con.getLifetime());
|
||||
_context.statManager().addRateData("stream.con.lifetimeRTT", con.getOptions().getRTT(), con.getLifetime());
|
||||
_context.statManager().addRateData("stream.con.lifetimeCongestionSeenAt", con.getLastCongestionSeenAt(), con.getLifetime());
|
||||
_context.statManager().addRateData("stream.con.lifetimeSendWindowSize", con.getOptions().getWindowSize(), con.getLifetime());
|
||||
}
|
||||
}
|
||||
|
||||
/** return a set of Connection objects */
|
||||
public Set listConnections() {
|
||||
synchronized (_connectionLock) {
|
||||
return new HashSet(_connectionByInboundId.values());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean ping(Destination peer, long timeoutMs) {
|
||||
return ping(peer, timeoutMs, true);
|
||||
}
|
||||
public boolean ping(Destination peer, long timeoutMs, boolean blocking) {
|
||||
return ping(peer, timeoutMs, blocking, null, null, null);
|
||||
}
|
||||
public boolean ping(Destination peer, long timeoutMs, boolean blocking, SessionKey keyToUse, Set tagsToSend, PingNotifier notifier) {
|
||||
byte id[] = new byte[4];
|
||||
_context.random().nextBytes(id);
|
||||
ByteArray ba = new ByteArray(id);
|
||||
PacketLocal packet = new PacketLocal(_context, peer);
|
||||
packet.setSendStreamId(id);
|
||||
packet.setFlag(Packet.FLAG_ECHO);
|
||||
packet.setFlag(Packet.FLAG_SIGNATURE_INCLUDED);
|
||||
packet.setOptionalFrom(_session.getMyDestination());
|
||||
if ( (keyToUse != null) && (tagsToSend != null) ) {
|
||||
packet.setKeyUsed(keyToUse);
|
||||
packet.setTagsSent(tagsToSend);
|
||||
}
|
||||
|
||||
PingRequest req = new PingRequest(peer, packet, notifier);
|
||||
|
||||
synchronized (_pendingPings) {
|
||||
_pendingPings.put(ba, req);
|
||||
}
|
||||
|
||||
_outboundQueue.enqueue(packet);
|
||||
|
||||
if (blocking) {
|
||||
synchronized (req) {
|
||||
if (!req.pongReceived())
|
||||
try { req.wait(timeoutMs); } catch (InterruptedException ie) {}
|
||||
}
|
||||
|
||||
synchronized (_pendingPings) {
|
||||
_pendingPings.remove(ba);
|
||||
}
|
||||
} else {
|
||||
SimpleTimer.getInstance().addEvent(new PingFailed(ba, notifier), timeoutMs);
|
||||
}
|
||||
|
||||
boolean ok = req.pongReceived();
|
||||
return ok;
|
||||
}
|
||||
|
||||
interface PingNotifier {
|
||||
public void pingComplete(boolean ok);
|
||||
}
|
||||
|
||||
private class PingFailed implements SimpleTimer.TimedEvent {
|
||||
private ByteArray _ba;
|
||||
private PingNotifier _notifier;
|
||||
public PingFailed(ByteArray ba, PingNotifier notifier) {
|
||||
_ba = ba;
|
||||
_notifier = notifier;
|
||||
}
|
||||
|
||||
public void timeReached() {
|
||||
boolean removed = false;
|
||||
synchronized (_pendingPings) {
|
||||
Object o = _pendingPings.remove(_ba);
|
||||
if (o != null)
|
||||
removed = true;
|
||||
}
|
||||
if (removed) {
|
||||
if (_notifier != null)
|
||||
_notifier.pingComplete(false);
|
||||
_log.error("Ping failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PingRequest {
|
||||
private boolean _ponged;
|
||||
private Destination _peer;
|
||||
private PacketLocal _packet;
|
||||
private PingNotifier _notifier;
|
||||
public PingRequest(Destination peer, PacketLocal packet, PingNotifier notifier) {
|
||||
_ponged = false;
|
||||
_peer = peer;
|
||||
_packet = packet;
|
||||
_notifier = notifier;
|
||||
}
|
||||
public void pong() {
|
||||
_log.debug("Ping successful");
|
||||
_context.sessionKeyManager().tagsDelivered(_peer.getPublicKey(), _packet.getKeyUsed(), _packet.getTagsSent());
|
||||
synchronized (ConnectionManager.PingRequest.this) {
|
||||
_ponged = true;
|
||||
ConnectionManager.PingRequest.this.notifyAll();
|
||||
}
|
||||
if (_notifier != null)
|
||||
_notifier.pingComplete(true);
|
||||
}
|
||||
public boolean pongReceived() { return _ponged; }
|
||||
}
|
||||
|
||||
void receivePong(byte pingId[]) {
|
||||
ByteArray ba = new ByteArray(pingId);
|
||||
PingRequest req = null;
|
||||
synchronized (_pendingPings) {
|
||||
req = (PingRequest)_pendingPings.remove(ba);
|
||||
}
|
||||
if (req != null)
|
||||
req.pong();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* Define the current options for the con (and allow custom tweaking midstream)
|
||||
*
|
||||
*/
|
||||
public class ConnectionOptions extends I2PSocketOptionsImpl {
|
||||
private int _connectDelay;
|
||||
private boolean _fullySigned;
|
||||
private int _windowSize;
|
||||
private int _receiveWindow;
|
||||
private int _profile;
|
||||
private int _rtt;
|
||||
private int _resendDelay;
|
||||
private int _sendAckDelay;
|
||||
private int _maxMessageSize;
|
||||
private int _choke;
|
||||
private int _maxResends;
|
||||
private int _inactivityTimeout;
|
||||
private int _inactivityAction;
|
||||
private int _inboundBufferSize;
|
||||
|
||||
public static final int PROFILE_BULK = 1;
|
||||
public static final int PROFILE_INTERACTIVE = 2;
|
||||
|
||||
/** on inactivity timeout, do nothing */
|
||||
public static final int INACTIVITY_ACTION_NOOP = 0;
|
||||
/** on inactivity timeout, close the connection */
|
||||
public static final int INACTIVITY_ACTION_DISCONNECT = 1;
|
||||
/** on inactivity timeout, send a payload message */
|
||||
public static final int INACTIVITY_ACTION_SEND = 2;
|
||||
|
||||
public static final String PROP_CONNECT_DELAY = "i2p.streaming.connectDelay";
|
||||
public static final String PROP_PROFILE = "i2p.streaming.profile";
|
||||
public static final String PROP_MAX_MESSAGE_SIZE = "i2p.streaming.maxMessageSize";
|
||||
public static final String PROP_MAX_RESENDS = "i2p.streaming.maxResends";
|
||||
public static final String PROP_INITIAL_RTT = "i2p.streaming.initialRTT";
|
||||
public static final String PROP_INITIAL_RESEND_DELAY = "i2p.streaming.initialResendDelay";
|
||||
public static final String PROP_INITIAL_ACK_DELAY = "i2p.streaming.initialAckDelay";
|
||||
public static final String PROP_INITIAL_WINDOW_SIZE = "i2p.streaming.initialWindowSize";
|
||||
public static final String PROP_INITIAL_RECEIVE_WINDOW = "i2p.streaming.initialReceiveWindow";
|
||||
public static final String PROP_INACTIVITY_TIMEOUT = "i2p.streaming.inactivityTimeout";
|
||||
public static final String PROP_INACTIVITY_ACTION = "i2p.streaming.inactivityAction";
|
||||
|
||||
public ConnectionOptions() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ConnectionOptions(Properties opts) {
|
||||
super(opts);
|
||||
}
|
||||
|
||||
public ConnectionOptions(I2PSocketOptions opts) {
|
||||
super(opts);
|
||||
}
|
||||
|
||||
public ConnectionOptions(ConnectionOptions opts) {
|
||||
super(opts);
|
||||
if (opts != null) {
|
||||
setConnectDelay(opts.getConnectDelay());
|
||||
setProfile(opts.getProfile());
|
||||
setRTT(opts.getRTT());
|
||||
setRequireFullySigned(opts.getRequireFullySigned());
|
||||
setWindowSize(opts.getWindowSize());
|
||||
setResendDelay(opts.getResendDelay());
|
||||
setMaxMessageSize(opts.getMaxMessageSize());
|
||||
setChoke(opts.getChoke());
|
||||
setMaxResends(opts.getMaxResends());
|
||||
setInactivityTimeout(opts.getInactivityTimeout());
|
||||
setInactivityAction(opts.getInactivityAction());
|
||||
setInboundBufferSize(opts.getInboundBufferSize());
|
||||
}
|
||||
}
|
||||
|
||||
protected void init(Properties opts) {
|
||||
super.init(opts);
|
||||
setConnectDelay(getInt(opts, PROP_CONNECT_DELAY, -1));
|
||||
setProfile(getInt(opts, PROP_PROFILE, PROFILE_BULK));
|
||||
setMaxMessageSize(getInt(opts, PROP_MAX_MESSAGE_SIZE, Packet.MAX_PAYLOAD_SIZE));
|
||||
setRTT(getInt(opts, PROP_INITIAL_RTT, 30*1000));
|
||||
setReceiveWindow(getInt(opts, PROP_INITIAL_RECEIVE_WINDOW, 1));
|
||||
setResendDelay(getInt(opts, PROP_INITIAL_RESEND_DELAY, 5*1000));
|
||||
setSendAckDelay(getInt(opts, PROP_INITIAL_ACK_DELAY, 2*1000));
|
||||
setWindowSize(getInt(opts, PROP_INITIAL_WINDOW_SIZE, 1));
|
||||
setMaxResends(getInt(opts, PROP_MAX_RESENDS, 5));
|
||||
setWriteTimeout(getInt(opts, PROP_WRITE_TIMEOUT, -1));
|
||||
setInactivityTimeout(getInt(opts, PROP_INACTIVITY_TIMEOUT, 5*60*1000));
|
||||
setInactivityAction(getInt(opts, PROP_INACTIVITY_ACTION, INACTIVITY_ACTION_SEND));
|
||||
setInboundBufferSize((getMaxMessageSize() + 2) * Connection.MAX_WINDOW_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* how long will we wait after instantiating a new con
|
||||
* before actually attempting to connect. If this is
|
||||
* set to 0, connect ASAP. If it is greater than 0, wait
|
||||
* until the output stream is flushed, the buffer fills,
|
||||
* or that many milliseconds pass.
|
||||
*
|
||||
*/
|
||||
public int getConnectDelay() { return _connectDelay; }
|
||||
public void setConnectDelay(int delayMs) { _connectDelay = delayMs; }
|
||||
|
||||
/**
|
||||
* Do we want all packets in both directions to be signed,
|
||||
* or can we deal with signatures on the SYN and FIN packets
|
||||
* only?
|
||||
*
|
||||
*/
|
||||
public boolean getRequireFullySigned() { return _fullySigned; }
|
||||
public void setRequireFullySigned(boolean sign) { _fullySigned = sign; }
|
||||
|
||||
/**
|
||||
* How many messages will we send before waiting for an ACK?
|
||||
*
|
||||
*/
|
||||
public int getWindowSize() { return _windowSize; }
|
||||
public void setWindowSize(int numMsgs) {
|
||||
if (numMsgs > Connection.MAX_WINDOW_SIZE)
|
||||
numMsgs = Connection.MAX_WINDOW_SIZE;
|
||||
_windowSize = numMsgs;
|
||||
}
|
||||
|
||||
/** after how many consecutive messages should we ack? */
|
||||
public int getReceiveWindow() { return _receiveWindow; }
|
||||
public void setReceiveWindow(int numMsgs) { _receiveWindow = numMsgs; }
|
||||
|
||||
/**
|
||||
* What to set the round trip time estimate to (in milliseconds)
|
||||
*/
|
||||
public int getRTT() { return _rtt; }
|
||||
public void setRTT(int ms) {
|
||||
_rtt = ms;
|
||||
if (_rtt > 60*1000)
|
||||
_rtt = 60*1000;
|
||||
}
|
||||
|
||||
/** rtt = rtt*RTT_DAMPENING + (1-RTT_DAMPENING)*currentPacketRTT */
|
||||
private static final double RTT_DAMPENING = 0.9;
|
||||
|
||||
public void updateRTT(int measuredValue) {
|
||||
setRTT((int)(RTT_DAMPENING*_rtt + (1-RTT_DAMPENING)*measuredValue));
|
||||
}
|
||||
|
||||
/** How long after sending a packet will we wait before resending? */
|
||||
public int getResendDelay() { return _resendDelay; }
|
||||
public void setResendDelay(int ms) { _resendDelay = ms; }
|
||||
|
||||
/**
|
||||
* if there are packets we haven't ACKed yet and we don't
|
||||
* receive _receiveWindow messages before
|
||||
* (_lastSendTime+_sendAckDelay), send an ACK of what
|
||||
* we have received so far.
|
||||
*
|
||||
*/
|
||||
public int getSendAckDelay() { return _sendAckDelay; }
|
||||
public void setSendAckDelay(int delayMs) { _sendAckDelay = delayMs; }
|
||||
|
||||
/** What is the largest message we want to send or receive? */
|
||||
public int getMaxMessageSize() { return _maxMessageSize; }
|
||||
public void setMaxMessageSize(int bytes) { _maxMessageSize = bytes; }
|
||||
|
||||
/**
|
||||
* how long we want to wait before any data is transferred on the
|
||||
* connection in either direction
|
||||
*
|
||||
*/
|
||||
public int getChoke() { return _choke; }
|
||||
public void setChoke(int ms) { _choke = ms; }
|
||||
|
||||
/**
|
||||
* What profile do we want to use for this connection?
|
||||
*
|
||||
*/
|
||||
public int getProfile() { return _profile; }
|
||||
public void setProfile(int profile) {
|
||||
if (profile != PROFILE_BULK)
|
||||
throw new IllegalArgumentException("Only bulk is supported so far");
|
||||
_profile = profile;
|
||||
}
|
||||
|
||||
/**
|
||||
* How many times will we try to send a message before giving up?
|
||||
*
|
||||
*/
|
||||
public int getMaxResends() { return _maxResends; }
|
||||
public void setMaxResends(int numSends) { _maxResends = numSends; }
|
||||
|
||||
/**
|
||||
* What period of inactivity qualifies as "too long"?
|
||||
*
|
||||
*/
|
||||
public int getInactivityTimeout() { return _inactivityTimeout; }
|
||||
public void setInactivityTimeout(int timeout) { _inactivityTimeout = timeout; }
|
||||
|
||||
public int getInactivityAction() { return _inactivityAction; }
|
||||
public void setInactivityAction(int action) { _inactivityAction = action; }
|
||||
|
||||
/**
|
||||
* how much data are we willing to accept in our buffer?
|
||||
*
|
||||
*/
|
||||
public int getInboundBufferSize() { return _inboundBufferSize; }
|
||||
public void setInboundBufferSize(int bytes) { _inboundBufferSize = bytes; }
|
||||
|
||||
public String toString() {
|
||||
StringBuffer buf = new StringBuffer(128);
|
||||
buf.append("conDelay=").append(_connectDelay);
|
||||
buf.append(" maxSize=").append(_maxMessageSize);
|
||||
buf.append(" rtt=").append(_rtt);
|
||||
buf.append(" rwin=").append(_receiveWindow);
|
||||
buf.append(" resendDelay=").append(_resendDelay);
|
||||
buf.append(" ackDelay=").append(_sendAckDelay);
|
||||
buf.append(" cwin=").append(_windowSize);
|
||||
buf.append(" maxResends=").append(_maxResends);
|
||||
buf.append(" writeTimeout=").append(getWriteTimeout());
|
||||
buf.append(" inactivityTimeout=").append(_inactivityTimeout);
|
||||
buf.append(" inboundBuffer=").append(_inboundBufferSize);
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
public static void main(String args[]) {
|
||||
Properties p = new Properties();
|
||||
|
||||
p.setProperty(PROP_CONNECT_DELAY, "1000");
|
||||
ConnectionOptions c = new ConnectionOptions(p);
|
||||
System.out.println("opts: " + c);
|
||||
|
||||
c = new ConnectionOptions(new I2PSocketOptionsImpl(p));
|
||||
System.out.println("opts: " + c);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.I2PException;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.data.Destination;
|
||||
import net.i2p.util.Log;
|
||||
import net.i2p.util.SimpleTimer;
|
||||
|
||||
/**
|
||||
* Receive a packet for a particular connection - placing the data onto the
|
||||
* queue, marking packets as acked, updating various fields, etc.
|
||||
*
|
||||
*/
|
||||
public class ConnectionPacketHandler {
|
||||
private I2PAppContext _context;
|
||||
private Log _log;
|
||||
|
||||
public ConnectionPacketHandler(I2PAppContext context) {
|
||||
_context = context;
|
||||
_log = context.logManager().getLog(ConnectionPacketHandler.class);
|
||||
_context.statManager().createRateStat("stream.con.receiveMessageSize", "Size of a message received on a connection", "Stream", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
|
||||
_context.statManager().createRateStat("stream.con.receiveDuplicateSize", "Size of a duplicate message received on a connection", "Stream", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
|
||||
_context.statManager().createRateStat("stream.con.packetsAckedPerMessageReceived", "Size of a duplicate message received on a connection", "Stream", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
|
||||
_context.statManager().createRateStat("stream.sendsBeforeAck", "How many times a message was sent before it was ACKed?", "Stream", new long[] { 10*60*1000, 60*60*1000 });
|
||||
}
|
||||
|
||||
/** distribute a packet to the connection specified */
|
||||
void receivePacket(Packet packet, Connection con) throws I2PException {
|
||||
boolean ok = verifyPacket(packet, con);
|
||||
if (!ok) return;
|
||||
con.packetReceived();
|
||||
|
||||
if (con.getInputStream().getTotalQueuedSize() > con.getOptions().getInboundBufferSize()) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Inbound buffer exceeded on connection " + con + ": dropping " + packet);
|
||||
con.getOptions().setChoke(5*1000);
|
||||
return;
|
||||
}
|
||||
con.getOptions().setChoke(0);
|
||||
|
||||
_context.statManager().addRateData("stream.con.receiveMessageSize", packet.getPayloadSize(), 0);
|
||||
|
||||
boolean isNew = con.getInputStream().messageReceived(packet.getSequenceNum(), packet.getPayload());
|
||||
|
||||
if ( (packet.getSequenceNum() == 0) && (packet.getPayloadSize() > 0) ) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("seq=0 && size=" + packet.getPayloadSize() + ": isNew? " + isNew
|
||||
+ " packet: " + packet + " con: " + con);
|
||||
}
|
||||
|
||||
// close *after* receiving the data, as well as after verifying the signatures / etc
|
||||
if (packet.isFlagSet(Packet.FLAG_CLOSE) && packet.isFlagSet(Packet.FLAG_SIGNATURE_INCLUDED))
|
||||
con.closeReceived();
|
||||
|
||||
if (isNew) {
|
||||
con.incrementUnackedPacketsReceived();
|
||||
con.incrementBytesReceived(packet.getPayloadSize());
|
||||
|
||||
if (packet.isFlagSet(Packet.FLAG_DELAY_REQUESTED) && (packet.getOptionalDelay() <= 0) ) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Scheduling immediate ack for " + packet);
|
||||
con.setNextSendTime(_context.clock().now() + con.getOptions().getSendAckDelay());
|
||||
} else {
|
||||
int delay = con.getOptions().getSendAckDelay();
|
||||
if (packet.isFlagSet(Packet.FLAG_DELAY_REQUESTED)) // delayed ACK requested
|
||||
delay += packet.getOptionalDelay();
|
||||
con.setNextSendTime(delay + _context.clock().now());
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Scheduling ack in " + delay + "ms for received packet " + packet);
|
||||
}
|
||||
} else {
|
||||
if ( (packet.getSequenceNum() > 0) || (packet.getPayloadSize() > 0) ) {
|
||||
_context.statManager().addRateData("stream.con.receiveDuplicateSize", packet.getPayloadSize(), 0);
|
||||
con.incrementDupMessagesReceived(1);
|
||||
|
||||
// take note of congestion
|
||||
//con.getOptions().setResendDelay(con.getOptions().getResendDelay()*2);
|
||||
//con.getOptions().setWindowSize(con.getOptions().getWindowSize()/2);
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("congestion.. dup " + packet);
|
||||
SimpleTimer.getInstance().addEvent(new AckDup(con), con.getOptions().getSendAckDelay());
|
||||
//con.incrementUnackedPacketsReceived();
|
||||
con.setNextSendTime(_context.clock().now() + con.getOptions().getSendAckDelay());
|
||||
} else {
|
||||
if (packet.isFlagSet(Packet.FLAG_SYNCHRONIZE)) {
|
||||
//con.incrementUnackedPacketsReceived();
|
||||
con.setNextSendTime(_context.clock().now() + con.getOptions().getSendAckDelay());
|
||||
} else {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("ACK only packet received: " + packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int numResends = 0;
|
||||
List acked = con.ackPackets(packet.getAckThrough(), packet.getNacks());
|
||||
if ( (acked != null) && (acked.size() > 0) ) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(acked.size() + " of our packets acked with " + packet);
|
||||
// use the highest RTT, since these would likely be bunched together,
|
||||
// and the highest rtt lets us set our resend delay properly
|
||||
int highestRTT = -1;
|
||||
for (int i = 0; i < acked.size(); i++) {
|
||||
PacketLocal p = (PacketLocal)acked.get(i);
|
||||
if (p.getAckTime() > highestRTT) {
|
||||
//if (p.getNumSends() <= 1)
|
||||
highestRTT = p.getAckTime();
|
||||
}
|
||||
_context.statManager().addRateData("stream.sendsBeforeAck", p.getNumSends(), p.getAckTime());
|
||||
|
||||
if (p.getNumSends() > 1)
|
||||
numResends++;
|
||||
|
||||
// ACK the tags we delivered so we can use them
|
||||
if ( (p.getKeyUsed() != null) && (p.getTagsSent() != null)
|
||||
&& (p.getTagsSent().size() > 0) ) {
|
||||
_context.sessionKeyManager().tagsDelivered(p.getTo().getPublicKey(),
|
||||
p.getKeyUsed(),
|
||||
p.getTagsSent());
|
||||
}
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Packet acked after " + p.getAckTime() + "ms: " + p);
|
||||
}
|
||||
if (highestRTT > 0) {
|
||||
con.getOptions().updateRTT(highestRTT);
|
||||
}
|
||||
_context.statManager().addRateData("stream.con.packetsAckedPerMessageReceived", acked.size(), highestRTT);
|
||||
}
|
||||
|
||||
boolean fastAck = adjustWindow(con, isNew, packet.getSequenceNum(), numResends, (acked != null ? acked.size() : 0));
|
||||
con.eventOccurred();
|
||||
if (fastAck) {
|
||||
if (con.getLastSendTime() + con.getOptions().getRTT() < _context.clock().now()) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Fast ack for dup " + packet);
|
||||
con.ackImmediately();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean adjustWindow(Connection con, boolean isNew, long sequenceNum, int numResends, int acked) {
|
||||
if ( (!isNew) && (sequenceNum > 0) ) {
|
||||
// dup real packet
|
||||
int oldSize = con.getOptions().getWindowSize();
|
||||
con.congestionOccurred();
|
||||
oldSize >>>= 1;
|
||||
if (oldSize <= 0)
|
||||
oldSize = 1;
|
||||
con.getOptions().setWindowSize(oldSize);
|
||||
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Congestion occurred - new windowSize " + oldSize + " congestionSeenAt: "
|
||||
+ con.getLastCongestionSeenAt() + " (#resends: " + numResends
|
||||
+ ") for " + con);
|
||||
|
||||
return true;
|
||||
} else if (numResends > 0) {
|
||||
// window sizes are shrunk on resend, not on ack
|
||||
} else {
|
||||
if (acked > 0) {
|
||||
long lowest = con.getHighestAckedThrough();
|
||||
if (lowest >= con.getCongestionWindowEnd()) {
|
||||
// new packet that ack'ed uncongested data, or an empty ack
|
||||
int newWindowSize = con.getOptions().getWindowSize();
|
||||
|
||||
if (newWindowSize > con.getLastCongestionSeenAt() / 2) {
|
||||
// congestion avoidance
|
||||
|
||||
// we can't use newWindowSize += 1/newWindowSize, since we're
|
||||
// integers, so lets use a random distribution instead
|
||||
int shouldIncrement = _context.random().nextInt(newWindowSize);
|
||||
if (shouldIncrement <= 0)
|
||||
newWindowSize += 1;
|
||||
} else {
|
||||
// slow start
|
||||
newWindowSize += 1;
|
||||
}
|
||||
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("New window size " + newWindowSize + " congestionSeenAt: "
|
||||
+ con.getLastCongestionSeenAt() + " (#resends: " + numResends
|
||||
+ ") for " + con);
|
||||
con.getOptions().setWindowSize(newWindowSize);
|
||||
con.setCongestionWindowEnd(newWindowSize + lowest);
|
||||
}
|
||||
} else {
|
||||
// received a message that doesn't contain a new ack
|
||||
|
||||
// ehh. cant do this, as we SACK and the acks may be
|
||||
// received out of order:
|
||||
// Alice: RECEIVE 2
|
||||
// Alice: SEND ack 2 nack 1
|
||||
// Alice: RECEIVE 1
|
||||
// Alice: SEND ack 2
|
||||
// Bob: RECEIVE ack 2
|
||||
// Bob: RECEIVE ack 2 nack 1 <-- NOT bad
|
||||
|
||||
/*
|
||||
if (con.getUnackedPacketsSent() > 0) {
|
||||
// peer got a dup
|
||||
int oldSize = con.getOptions().getWindowSize();
|
||||
oldSize >>>= 1;
|
||||
if (oldSize <= 0)
|
||||
oldSize = 1;
|
||||
con.getOptions().setWindowSize(oldSize);
|
||||
return false;
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure this packet is ok and that we can continue processing its data.
|
||||
*
|
||||
* @return true if the packet is ok for this connection, false if we shouldn't
|
||||
* continue processing.
|
||||
*/
|
||||
private boolean verifyPacket(Packet packet, Connection con) throws I2PException {
|
||||
if (packet.isFlagSet(Packet.FLAG_RESET)) {
|
||||
verifyReset(packet, con);
|
||||
return false;
|
||||
} else {
|
||||
verifySignature(packet, con);
|
||||
|
||||
if (con.getSendStreamId() == null) {
|
||||
if (packet.isFlagSet(Packet.FLAG_SYNCHRONIZE)) {
|
||||
con.setSendStreamId(packet.getReceiveStreamId());
|
||||
con.setRemotePeer(packet.getOptionalFrom());
|
||||
return true;
|
||||
} else {
|
||||
// neither RST nor SYN and we dont have the stream id yet? nuh uh
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Packet without RST or SYN where we dont know stream ID: "
|
||||
+ packet);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!DataHelper.eq(con.getSendStreamId(), packet.getReceiveStreamId())) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Packet received with the wrong reply stream id: "
|
||||
+ con + " / " + packet);
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure this RST packet is valid, and if it is, act on it.
|
||||
*/
|
||||
private void verifyReset(Packet packet, Connection con) {
|
||||
if (DataHelper.eq(con.getReceiveStreamId(), packet.getSendStreamId())) {
|
||||
boolean ok = packet.verifySignature(_context, packet.getOptionalFrom(), null);
|
||||
if (!ok) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Received unsigned / forged RST on " + con);
|
||||
return;
|
||||
} else {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Reset received");
|
||||
// ok, valid RST
|
||||
con.resetReceived();
|
||||
con.eventOccurred();
|
||||
|
||||
// no further processing
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Received a packet for the wrong connection? wtf: "
|
||||
+ con + " / " + packet);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the signature if necessary.
|
||||
*
|
||||
* @throws I2PException if the signature was necessary and it was invalid
|
||||
*/
|
||||
private void verifySignature(Packet packet, Connection con) throws I2PException {
|
||||
// verify the signature if necessary
|
||||
if (con.getOptions().getRequireFullySigned() ||
|
||||
packet.isFlagSet(Packet.FLAG_SYNCHRONIZE) ||
|
||||
packet.isFlagSet(Packet.FLAG_CLOSE) ) {
|
||||
// we need a valid signature
|
||||
Destination from = con.getRemotePeer();
|
||||
if (from == null)
|
||||
from = packet.getOptionalFrom();
|
||||
boolean sigOk = packet.verifySignature(_context, from, null);
|
||||
if (!sigOk) {
|
||||
throw new I2PException("Received unsigned / forged packet: " + packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class AckDup implements SimpleTimer.TimedEvent {
|
||||
private long _created;
|
||||
private Connection _con;
|
||||
public AckDup(Connection con) {
|
||||
_created = _context.clock().now();
|
||||
_con = con;
|
||||
}
|
||||
public void timeReached() {
|
||||
if (_con.getLastSendTime() <= _created) {
|
||||
if (!_con.getIsConnected()) return;
|
||||
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Last sent was a while ago, and we want to ack a dup");
|
||||
// we haven't done anything since receiving the dup, send an
|
||||
// ack now
|
||||
_con.ackImmediately();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import java.net.ConnectException;
|
||||
import net.i2p.I2PException;
|
||||
|
||||
/**
|
||||
* Bridge to allow accepting new connections
|
||||
*
|
||||
*/
|
||||
public class I2PServerSocketFull implements I2PServerSocket {
|
||||
private I2PSocketManagerFull _socketManager;
|
||||
|
||||
public I2PServerSocketFull(I2PSocketManagerFull mgr) {
|
||||
_socketManager = mgr;
|
||||
}
|
||||
|
||||
public I2PSocket accept() throws I2PException {
|
||||
return _socketManager.receiveSocket();
|
||||
}
|
||||
|
||||
public void close() { _socketManager.getConnectionManager().setAllowIncomingConnections(false); }
|
||||
|
||||
public I2PSocketManager getManager() { return _socketManager; }
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import net.i2p.data.Destination;
|
||||
|
||||
/**
|
||||
* Bridge between the full streaming lib and the I2PSocket API
|
||||
*
|
||||
*/
|
||||
public class I2PSocketFull implements I2PSocket {
|
||||
private Connection _connection;
|
||||
private I2PSocket.SocketErrorListener _listener;
|
||||
|
||||
public I2PSocketFull(Connection con) {
|
||||
_connection = con;
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
Connection c = _connection;
|
||||
if (c == null) return;
|
||||
if (c.getIsConnected()) {
|
||||
OutputStream out = c.getOutputStream();
|
||||
if (out != null)
|
||||
out.close();
|
||||
c.disconnect(true);
|
||||
} else {
|
||||
//throw new IOException("Not connected");
|
||||
}
|
||||
destroy();
|
||||
}
|
||||
|
||||
public InputStream getInputStream() {
|
||||
return _connection.getInputStream();
|
||||
}
|
||||
|
||||
public I2PSocketOptions getOptions() {
|
||||
return _connection.getOptions();
|
||||
}
|
||||
|
||||
public OutputStream getOutputStream() throws IOException {
|
||||
return _connection.getOutputStream();
|
||||
}
|
||||
|
||||
public Destination getPeerDestination() {
|
||||
return _connection.getRemotePeer();
|
||||
}
|
||||
|
||||
public long getReadTimeout() {
|
||||
return _connection.getOptions().getReadTimeout();
|
||||
}
|
||||
|
||||
public Destination getThisDestination() {
|
||||
return _connection.getSession().getMyDestination();
|
||||
}
|
||||
|
||||
public void setOptions(I2PSocketOptions options) {
|
||||
if (options instanceof ConnectionOptions)
|
||||
_connection.setOptions((ConnectionOptions)options);
|
||||
else
|
||||
_connection.setOptions(new ConnectionOptions(options));
|
||||
}
|
||||
|
||||
public void setReadTimeout(long ms) {
|
||||
_connection.getOptions().setReadTimeout(ms);
|
||||
}
|
||||
|
||||
public void setSocketErrorListener(I2PSocket.SocketErrorListener lsnr) {
|
||||
_listener = lsnr;
|
||||
}
|
||||
|
||||
void destroy() {
|
||||
_connection = null;
|
||||
_listener = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.NoRouteToHostException;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.I2PException;
|
||||
import net.i2p.client.I2PSession;
|
||||
import net.i2p.client.I2PSessionException;
|
||||
import net.i2p.client.I2PSessionListener;
|
||||
import net.i2p.data.Destination;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
|
||||
/**
|
||||
* Centralize the coordination and multiplexing of the local client's streaming.
|
||||
* There should be one I2PSocketManager for each I2PSession, and if an application
|
||||
* is sending and receiving data through the streaming library using an
|
||||
* I2PSocketManager, it should not attempt to call I2PSession's setSessionListener
|
||||
* or receive any messages with its .receiveMessage
|
||||
*
|
||||
*/
|
||||
public class I2PSocketManagerFull implements I2PSocketManager {
|
||||
private I2PAppContext _context;
|
||||
private Log _log;
|
||||
private I2PSession _session;
|
||||
private I2PServerSocketFull _serverSocket;
|
||||
private ConnectionOptions _defaultOptions;
|
||||
private long _acceptTimeout;
|
||||
private String _name;
|
||||
private int _maxStreams;
|
||||
private static int __managerId = 0;
|
||||
private ConnectionManager _connectionManager;
|
||||
|
||||
/**
|
||||
* How long to wait for the client app to accept() before sending back CLOSE?
|
||||
* This includes the time waiting in the queue. Currently set to 5 seconds.
|
||||
*/
|
||||
private static final long ACCEPT_TIMEOUT_DEFAULT = 5*1000;
|
||||
|
||||
public I2PSocketManagerFull() {
|
||||
_context = null;
|
||||
_session = null;
|
||||
}
|
||||
public I2PSocketManagerFull(I2PAppContext context, I2PSession session, Properties opts, String name) {
|
||||
this();
|
||||
init(context, session, opts, name);
|
||||
}
|
||||
|
||||
/** how many streams will we allow at once? */
|
||||
public static final String PROP_MAX_STREAMS = "i2p.streaming.maxConcurrentStreams";
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public void init(I2PAppContext context, I2PSession session, Properties opts, String name) {
|
||||
_context = context;
|
||||
_session = session;
|
||||
_log = _context.logManager().getLog(I2PSocketManagerFull.class);
|
||||
|
||||
_maxStreams = -1;
|
||||
try {
|
||||
String num = (opts != null ? opts.getProperty(PROP_MAX_STREAMS, "-1") : "-1");
|
||||
_maxStreams = Integer.parseInt(num);
|
||||
} catch (NumberFormatException nfe) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Invalid max # of concurrent streams, defaulting to unlimited", nfe);
|
||||
_maxStreams = -1;
|
||||
}
|
||||
_connectionManager = new ConnectionManager(_context, _session, _maxStreams);
|
||||
_name = name + " " + (++__managerId);
|
||||
_acceptTimeout = ACCEPT_TIMEOUT_DEFAULT;
|
||||
_defaultOptions = new ConnectionOptions(opts);
|
||||
_serverSocket = new I2PServerSocketFull(this);
|
||||
|
||||
if (_log.shouldLog(Log.INFO)) {
|
||||
_log.info("Socket manager created. \ndefault options: " + _defaultOptions
|
||||
+ "\noriginal properties: " + opts);
|
||||
}
|
||||
}
|
||||
|
||||
public I2PSocketOptions buildOptions() { return buildOptions(null); }
|
||||
public I2PSocketOptions buildOptions(Properties opts) {
|
||||
return new ConnectionOptions(opts);
|
||||
}
|
||||
|
||||
public I2PSession getSession() {
|
||||
return _session;
|
||||
}
|
||||
|
||||
public ConnectionManager getConnectionManager() {
|
||||
return _connectionManager;
|
||||
}
|
||||
|
||||
public I2PSocket receiveSocket() throws I2PException {
|
||||
if (_session.isClosed()) throw new I2PException("Session closed");
|
||||
Connection con = _connectionManager.getConnectionHandler().accept(-1);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("receiveSocket() called: " + con);
|
||||
if (con != null) {
|
||||
I2PSocketFull sock = new I2PSocketFull(con);
|
||||
con.setSocket(sock);
|
||||
return sock;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ping the specified peer, returning true if they replied to the ping within
|
||||
* the timeout specified, false otherwise. This call blocks.
|
||||
*
|
||||
*/
|
||||
public boolean ping(Destination peer, long timeoutMs) {
|
||||
return _connectionManager.ping(peer, timeoutMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* How long should we wait for the client to .accept() a socket before
|
||||
* sending back a NACK/Close?
|
||||
*
|
||||
* @param ms milliseconds to wait, maximum
|
||||
*/
|
||||
public void setAcceptTimeout(long ms) { _acceptTimeout = ms; }
|
||||
public long getAcceptTimeout() { return _acceptTimeout; }
|
||||
|
||||
public void setDefaultOptions(I2PSocketOptions options) {
|
||||
_defaultOptions = new ConnectionOptions(options);
|
||||
}
|
||||
|
||||
public I2PSocketOptions getDefaultOptions() {
|
||||
return _defaultOptions;
|
||||
}
|
||||
|
||||
public I2PServerSocket getServerSocket() {
|
||||
_connectionManager.setAllowIncomingConnections(true);
|
||||
return _serverSocket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new connected socket (block until the socket is created)
|
||||
*
|
||||
* @param peer Destination to connect to
|
||||
* @param options I2P socket options to be used for connecting
|
||||
*
|
||||
* @throws NoRouteToHostException if the peer is not found or not reachable
|
||||
* @throws I2PException if there is some other I2P-related problem
|
||||
*/
|
||||
public I2PSocket connect(Destination peer, I2PSocketOptions options)
|
||||
throws I2PException, NoRouteToHostException {
|
||||
if (_connectionManager.getSession().isClosed())
|
||||
throw new I2PException("Session is closed");
|
||||
if (options == null)
|
||||
options = _defaultOptions;
|
||||
ConnectionOptions opts = null;
|
||||
if (options instanceof ConnectionOptions)
|
||||
opts = (ConnectionOptions)options;
|
||||
else
|
||||
opts = new ConnectionOptions(options);
|
||||
Connection con = _connectionManager.connect(peer, opts);
|
||||
if (con == null)
|
||||
throw new TooManyStreamsException("Too many streams (max " + _maxStreams + ")");
|
||||
I2PSocketFull socket = new I2PSocketFull(con);
|
||||
con.setSocket(socket);
|
||||
if (con.getConnectionError() != null) {
|
||||
con.disconnect(false);
|
||||
throw new NoRouteToHostException(con.getConnectionError());
|
||||
}
|
||||
return socket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new connected socket (block until the socket is created)
|
||||
*
|
||||
* @param peer Destination to connect to
|
||||
*
|
||||
* @throws NoRouteToHostException if the peer is not found or not reachable
|
||||
* @throws I2PException if there is some other I2P-related problem
|
||||
*/
|
||||
public I2PSocket connect(Destination peer) throws I2PException, NoRouteToHostException {
|
||||
return connect(peer, _defaultOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the socket manager, freeing all the associated resources. This
|
||||
* method will block untill all the managed sockets are closed.
|
||||
*
|
||||
*/
|
||||
public void destroySocketManager() {
|
||||
_connectionManager.disconnectAllHard();
|
||||
_connectionManager.setAllowIncomingConnections(false);
|
||||
// should we destroy the _session too?
|
||||
// yes, since the old lib did (and SAM wants it to, and i dont know why not)
|
||||
if ( (_session != null) && (!_session.isClosed()) ) {
|
||||
try {
|
||||
_session.destroySession();
|
||||
} catch (I2PSessionException ise) {
|
||||
_log.warn("Unable to destroy the session", ise);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a set of currently connected I2PSockets, either initiated locally or remotely.
|
||||
*
|
||||
*/
|
||||
public Set listSockets() {
|
||||
Set connections = _connectionManager.listConnections();
|
||||
Set rv = new HashSet(connections.size());
|
||||
for (Iterator iter = connections.iterator(); iter.hasNext(); ) {
|
||||
Connection con = (Connection)iter.next();
|
||||
if (con.getSocket() != null)
|
||||
rv.add(con.getSocket());
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
public String getName() { return _name; }
|
||||
public void setName(String name) { _name = name; }
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.client.I2PSession;
|
||||
import net.i2p.client.I2PSessionListener;
|
||||
import net.i2p.client.I2PSessionException;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Receive raw information from the I2PSession and turn it into
|
||||
* Packets, if we can.
|
||||
*
|
||||
*/
|
||||
public class MessageHandler implements I2PSessionListener {
|
||||
private ConnectionManager _manager;
|
||||
private I2PAppContext _context;
|
||||
private Log _log;
|
||||
|
||||
public MessageHandler(I2PAppContext ctx, ConnectionManager mgr) {
|
||||
_manager = mgr;
|
||||
_context = ctx;
|
||||
_log = ctx.logManager().getLog(MessageHandler.class);
|
||||
_context.statManager().createRateStat("stream.packetReceiveFailure", "When do we fail to decrypt or otherwise receive a packet sent to us?", "Stream", new long[] { 60*60*1000, 24*60*60*1000 });
|
||||
}
|
||||
|
||||
/** Instruct the client that the given session has received a message with
|
||||
* size # of bytes.
|
||||
* @param session session to notify
|
||||
* @param msgId message number available
|
||||
* @param size size of the message
|
||||
*/
|
||||
public void messageAvailable(I2PSession session, int msgId, long size) {
|
||||
byte data[] = null;
|
||||
try {
|
||||
data = session.receiveMessage(msgId);
|
||||
} catch (I2PSessionException ise) {
|
||||
_context.statManager().addRateData("stream.packetReceiveFailure", 1, 0);
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Error receiving the message", ise);
|
||||
return;
|
||||
}
|
||||
Packet packet = new Packet();
|
||||
try {
|
||||
packet.readPacket(data, 0, data.length);
|
||||
_manager.getPacketHandler().receivePacket(packet);
|
||||
} catch (IllegalArgumentException iae) {
|
||||
_context.statManager().addRateData("stream.packetReceiveFailure", 1, 0);
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Received an invalid packet", iae);
|
||||
}
|
||||
}
|
||||
|
||||
/** Instruct the client that the session specified seems to be under attack
|
||||
* and that the client may wish to move its destination to another router.
|
||||
* @param session session to report abuse to
|
||||
* @param severity how bad the abuse is
|
||||
*/
|
||||
public void reportAbuse(I2PSession session, int severity) {
|
||||
if (_log.shouldLog(Log.ERROR))
|
||||
_log.error("Abuse reported with severity " + severity);
|
||||
_manager.disconnectAllHard();
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the client that the session has been terminated
|
||||
*
|
||||
*/
|
||||
public void disconnected(I2PSession session) {
|
||||
if (_log.shouldLog(Log.ERROR))
|
||||
_log.error("I2PSession disconnected");
|
||||
_manager.disconnectAllHard();
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the client that some error occurred
|
||||
*
|
||||
*/
|
||||
public void errorOccurred(I2PSession session, String message, Throwable error) {
|
||||
if (_log.shouldLog(Log.ERROR))
|
||||
_log.error("error occurred: " + message, error);
|
||||
//_manager.disconnectAllHard();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,426 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InterruptedIOException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.data.ByteArray;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Stream that can be given messages out of order
|
||||
* yet present them in order.
|
||||
*
|
||||
*/
|
||||
public class MessageInputStream extends InputStream {
|
||||
private I2PAppContext _context;
|
||||
private Log _log;
|
||||
/**
|
||||
* List of ByteArray objects of data ready to be read,
|
||||
* with the first ByteArray at index 0, and the next
|
||||
* actual byte to be read at _readyDataBlockIndex of
|
||||
* that array.
|
||||
*
|
||||
*/
|
||||
private List _readyDataBlocks;
|
||||
private int _readyDataBlockIndex;
|
||||
/** highest message ID used in the readyDataBlocks */
|
||||
private long _highestReadyBlockId;
|
||||
/** highest overall message ID */
|
||||
private long _highestBlockId;
|
||||
/**
|
||||
* Message ID (Long) to ByteArray for blocks received
|
||||
* out of order when there are lower IDs not yet
|
||||
* received
|
||||
*/
|
||||
private Map _notYetReadyBlocks;
|
||||
/**
|
||||
* if we have received a flag saying there won't be later messages, EOF
|
||||
* after we have cleared what we have received.
|
||||
*/
|
||||
private boolean _closeReceived;
|
||||
/** if we don't want any more data, ignore the data */
|
||||
private boolean _locallyClosed;
|
||||
private int _readTimeout;
|
||||
private IOException _streamError;
|
||||
private long _readTotal;
|
||||
|
||||
private byte[] _oneByte = new byte[1];
|
||||
|
||||
private Object _dataLock;
|
||||
|
||||
public MessageInputStream(I2PAppContext ctx) {
|
||||
_context = ctx;
|
||||
_log = ctx.logManager().getLog(MessageInputStream.class);
|
||||
_readyDataBlocks = new ArrayList(4);
|
||||
_readyDataBlockIndex = 0;
|
||||
_highestReadyBlockId = -1;
|
||||
_highestBlockId = -1;
|
||||
_readTimeout = -1;
|
||||
_readTotal = 0;
|
||||
_notYetReadyBlocks = new HashMap(4);
|
||||
_dataLock = new Object();
|
||||
_closeReceived = false;
|
||||
_locallyClosed = false;
|
||||
}
|
||||
|
||||
/** What is the highest block ID we've completely received through? */
|
||||
public long getHighestReadyBockId() {
|
||||
synchronized (_dataLock) {
|
||||
return _highestReadyBlockId;
|
||||
}
|
||||
}
|
||||
|
||||
public long getHighestBlockId() {
|
||||
synchronized (_dataLock) {
|
||||
return _highestBlockId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the message IDs that are holes in our sequence - ones
|
||||
* past the highest ready ID and below the highest received message
|
||||
* ID. This may return null if there are no such IDs.
|
||||
*
|
||||
*/
|
||||
public long[] getNacks() {
|
||||
synchronized (_dataLock) {
|
||||
return locked_getNacks();
|
||||
}
|
||||
}
|
||||
private long[] locked_getNacks() {
|
||||
List ids = null;
|
||||
for (long i = _highestReadyBlockId + 1; i < _highestBlockId; i++) {
|
||||
Long l = new Long(i);
|
||||
if (_notYetReadyBlocks.containsKey(l)) {
|
||||
// ACK
|
||||
} else {
|
||||
if (ids == null)
|
||||
ids = new ArrayList(4);
|
||||
ids.add(l);
|
||||
}
|
||||
}
|
||||
if (ids != null) {
|
||||
long rv[] = new long[ids.size()];
|
||||
for (int i = 0; i < rv.length; i++)
|
||||
rv[i] = ((Long)ids.get(i)).longValue();
|
||||
return rv;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void updateAcks(PacketLocal packet) {
|
||||
synchronized (_dataLock) {
|
||||
packet.setAckThrough(_highestBlockId);
|
||||
packet.setNacks(locked_getNacks());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ascending list of block IDs greater than the highest
|
||||
* ready block ID, or null if there aren't any.
|
||||
*
|
||||
*/
|
||||
public long[] getOutOfOrderBlocks() {
|
||||
long blocks[] = null;
|
||||
synchronized (_dataLock) {
|
||||
int num = _notYetReadyBlocks.size();
|
||||
if (num <= 0) return null;
|
||||
blocks = new long[num];
|
||||
int i = 0;
|
||||
for (Iterator iter = _notYetReadyBlocks.keySet().iterator(); iter.hasNext(); ) {
|
||||
Long id = (Long)iter.next();
|
||||
blocks[i] = id.longValue();
|
||||
i++;
|
||||
}
|
||||
}
|
||||
Arrays.sort(blocks);
|
||||
return blocks;
|
||||
}
|
||||
|
||||
/** how many blocks have we received that we still have holes before? */
|
||||
public int getOutOfOrderBlockCount() {
|
||||
synchronized (_dataLock) {
|
||||
return _notYetReadyBlocks.size();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* how long a read() call should block (if less than 0, block indefinitely,
|
||||
* but if it is 0, do not block at all)
|
||||
*/
|
||||
public int getReadTimeout() { return _readTimeout; }
|
||||
public void setReadTimeout(int timeout) { _readTimeout = timeout; }
|
||||
|
||||
public void closeReceived() {
|
||||
synchronized (_dataLock) {
|
||||
if (_log.shouldLog(Log.DEBUG)) {
|
||||
StringBuffer buf = new StringBuffer(128);
|
||||
buf.append("Close received, ready bytes: ");
|
||||
long available = 0;
|
||||
for (int i = 0; i < _readyDataBlocks.size(); i++)
|
||||
available += ((ByteArray)_readyDataBlocks.get(i)).getData().length;
|
||||
available -= _readyDataBlockIndex;
|
||||
buf.append(available);
|
||||
buf.append(" blocks: ").append(_readyDataBlocks.size());
|
||||
|
||||
buf.append(" not ready blocks: ");
|
||||
long notAvailable = 0;
|
||||
for (Iterator iter = _notYetReadyBlocks.keySet().iterator(); iter.hasNext(); ) {
|
||||
Long id = (Long)iter.next();
|
||||
ByteArray ba = (ByteArray)_notYetReadyBlocks.get(id);
|
||||
buf.append(id).append(" ");
|
||||
|
||||
if (ba.getData() != null)
|
||||
notAvailable += ba.getData().length;
|
||||
}
|
||||
|
||||
buf.append("not ready bytes: ").append(notAvailable);
|
||||
buf.append(" highest ready block: ").append(_highestReadyBlockId);
|
||||
|
||||
_log.debug(buf.toString(), new Exception("closed"));
|
||||
}
|
||||
_closeReceived = true;
|
||||
_dataLock.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A new message has arrived - toss it on the appropriate queue (moving
|
||||
* previously pending messages to the ready queue if it fills the gap, etc).
|
||||
*
|
||||
* @return true if this is a new packet, false if it is a dup
|
||||
*/
|
||||
public boolean messageReceived(long messageId, byte payload[]) {
|
||||
synchronized (_dataLock) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("received " + messageId + " with " + payload.length);
|
||||
if (messageId <= _highestReadyBlockId) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("ignoring dup message " + messageId);
|
||||
_dataLock.notifyAll();
|
||||
return false; // already received
|
||||
}
|
||||
if (messageId > _highestBlockId)
|
||||
_highestBlockId = messageId;
|
||||
|
||||
if (_highestReadyBlockId + 1 == messageId) {
|
||||
if (!_locallyClosed && payload.length > 0) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("accepting bytes as ready: " + payload.length);
|
||||
_readyDataBlocks.add(new ByteArray(payload));
|
||||
}
|
||||
_highestReadyBlockId = messageId;
|
||||
long cur = _highestReadyBlockId + 1;
|
||||
// now pull in any previously pending blocks
|
||||
while (_notYetReadyBlocks.containsKey(new Long(cur))) {
|
||||
ByteArray ba = (ByteArray)_notYetReadyBlocks.remove(new Long(cur));
|
||||
if ( (ba != null) && (ba.getData() != null) && (ba.getData().length > 0) ) {
|
||||
_readyDataBlocks.add(ba);
|
||||
}
|
||||
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("making ready the block " + cur);
|
||||
cur++;
|
||||
_highestReadyBlockId++;
|
||||
}
|
||||
_dataLock.notifyAll();
|
||||
} else {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("message is out of order: " + messageId);
|
||||
if (_locallyClosed) // dont need the payload, just the msgId in order
|
||||
_notYetReadyBlocks.put(new Long(messageId), new ByteArray(null));
|
||||
else
|
||||
_notYetReadyBlocks.put(new Long(messageId), new ByteArray(payload));
|
||||
_dataLock.notifyAll();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public int read() throws IOException {
|
||||
int read = read(_oneByte, 0, 1);
|
||||
if (read < 0)
|
||||
return -1;
|
||||
else
|
||||
return _oneByte[0];
|
||||
}
|
||||
|
||||
public int read(byte target[]) throws IOException {
|
||||
return read(target, 0, target.length);
|
||||
}
|
||||
|
||||
public int read(byte target[], int offset, int length) throws IOException {
|
||||
if (_locallyClosed) throw new IOException("Already locally closed");
|
||||
throwAnyError();
|
||||
long expiration = -1;
|
||||
if (_readTimeout > 0)
|
||||
expiration = _readTimeout + System.currentTimeMillis();
|
||||
synchronized (_dataLock) {
|
||||
for (int i = 0; i < length; i++) {
|
||||
if ( (_readyDataBlocks.size() <= 0) && (i == 0) ) {
|
||||
// ok, we havent found anything, so lets block until we get
|
||||
// at least one byte
|
||||
|
||||
while (_readyDataBlocks.size() <= 0) {
|
||||
if ( (_notYetReadyBlocks.size() <= 0) && (_closeReceived) ) {
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("read(...," + offset + ", " + length + ")[" + i
|
||||
+ "] got EOF after " + _readTotal + " " + toString());
|
||||
return -1;
|
||||
} else {
|
||||
if (_readTimeout < 0) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("read(...," + offset+", " + length+ ")[" + i
|
||||
+ ") with no timeout: " + toString());
|
||||
try { _dataLock.wait(); } catch (InterruptedException ie) { }
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("read(...," + offset+", " + length+ ")[" + i
|
||||
+ ") with no timeout complete: " + toString());
|
||||
throwAnyError();
|
||||
} else if (_readTimeout > 0) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("read(...," + offset+", " + length+ ")[" + i
|
||||
+ ") with timeout: " + _readTimeout + ": " + toString());
|
||||
try { _dataLock.wait(_readTimeout); } catch (InterruptedException ie) { }
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("read(...," + offset+", " + length+ ")[" + i
|
||||
+ ") with timeout complete: " + _readTimeout + ": " + toString());
|
||||
throwAnyError();
|
||||
} else { // readTimeout == 0
|
||||
// noop, don't block
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("read(...," + offset+", " + length+ ")[" + i
|
||||
+ ") with nonblocking setup: " + toString());
|
||||
return i;
|
||||
}
|
||||
if (_readyDataBlocks.size() <= 0) {
|
||||
if ( (_readTimeout > 0) && (expiration < System.currentTimeMillis()) ) {
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("read(...," + offset+", " + length+ ")[" + i
|
||||
+ ") expired: " + toString());
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// we looped a few times then got data, so this pass doesnt count
|
||||
i--;
|
||||
} else if (_readyDataBlocks.size() <= 0) {
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("read(...," + offset+", " + length+ ")[" + i
|
||||
+ "] no more ready blocks, returning");
|
||||
return i;
|
||||
} else {
|
||||
// either was already ready, or we wait()ed and it arrived
|
||||
ByteArray cur = (ByteArray)_readyDataBlocks.get(0);
|
||||
byte rv = cur.getData()[_readyDataBlockIndex];
|
||||
_readyDataBlockIndex++;
|
||||
if (cur.getData().length <= _readyDataBlockIndex) {
|
||||
_readyDataBlockIndex = 0;
|
||||
_readyDataBlocks.remove(0);
|
||||
}
|
||||
_readTotal++;
|
||||
target[offset + i] = rv; // rv < 0 ? rv + 256 : rv
|
||||
if ( (_readyDataBlockIndex <= 3) || (_readyDataBlockIndex >= cur.getData().length - 5) ) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("read(...," + offset+", " + length+ ")[" + i
|
||||
+ "] after ready data: readyDataBlockIndex=" + _readyDataBlockIndex
|
||||
+ " readyBlocks=" + _readyDataBlocks.size()
|
||||
+ " readTotal=" + _readTotal);
|
||||
}
|
||||
}
|
||||
} // for (int i = 0; i < length; i++) {
|
||||
} // synchronized (_dataLock)
|
||||
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.info("read(...," + offset+", " + length+ ") read fully total read: " +_readTotal);
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
public int available() throws IOException {
|
||||
if (_locallyClosed) throw new IOException("Already closed, you wanker");
|
||||
throwAnyError();
|
||||
int numBytes = 0;
|
||||
synchronized (_dataLock) {
|
||||
for (int i = 0; i < _readyDataBlocks.size(); i++) {
|
||||
ByteArray cur = (ByteArray)_readyDataBlocks.get(i);
|
||||
if (i == 0)
|
||||
numBytes += cur.getData().length - _readyDataBlockIndex;
|
||||
else
|
||||
numBytes += cur.getData().length;
|
||||
}
|
||||
}
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.info("available(): " + numBytes + " " + toString());
|
||||
|
||||
return numBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* How many bytes are queued up for reading (or sitting in the out-of-order
|
||||
* buffer)?
|
||||
*
|
||||
*/
|
||||
public int getTotalQueuedSize() {
|
||||
synchronized (_dataLock) {
|
||||
if (_locallyClosed) return 0;
|
||||
int numBytes = 0;
|
||||
for (int i = 0; i < _readyDataBlocks.size(); i++) {
|
||||
ByteArray cur = (ByteArray)_readyDataBlocks.get(i);
|
||||
if (i == 0)
|
||||
numBytes += cur.getData().length - _readyDataBlockIndex;
|
||||
else
|
||||
numBytes += cur.getData().length;
|
||||
}
|
||||
for (Iterator iter = _notYetReadyBlocks.values().iterator(); iter.hasNext(); ) {
|
||||
ByteArray cur = (ByteArray)iter.next();
|
||||
numBytes += cur.getData().length;
|
||||
}
|
||||
return numBytes;
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
synchronized (_dataLock) {
|
||||
_readyDataBlocks.clear();
|
||||
|
||||
// we don't need the data, but we do need to keep track of the messageIds
|
||||
// received, so we can ACK accordingly
|
||||
for (Iterator iter = _notYetReadyBlocks.values().iterator(); iter.hasNext(); ) {
|
||||
ByteArray ba = (ByteArray)iter.next();
|
||||
ba.setData(null);
|
||||
}
|
||||
_locallyClosed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream b0rked, die with the given error
|
||||
*
|
||||
*/
|
||||
void streamErrorOccurred(IOException ioe) {
|
||||
_streamError = ioe;
|
||||
synchronized (_dataLock) {
|
||||
_dataLock.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
private void throwAnyError() throws IOException {
|
||||
if (_streamError != null) {
|
||||
IOException ioe = _streamError;
|
||||
_streamError = null;
|
||||
throw ioe;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.data.ByteArray;
|
||||
import net.i2p.util.ByteCache;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* A stream that we can shove data into that fires off those bytes
|
||||
* on flush or when the buffer is full. It also blocks according
|
||||
* to the data receiver's needs.
|
||||
*/
|
||||
public class MessageOutputStream extends OutputStream {
|
||||
private I2PAppContext _context;
|
||||
private Log _log;
|
||||
private byte _buf[];
|
||||
private int _valid;
|
||||
private Object _dataLock;
|
||||
private DataReceiver _dataReceiver;
|
||||
private IOException _streamError;
|
||||
private boolean _closed;
|
||||
private long _written;
|
||||
private int _writeTimeout;
|
||||
private ByteCache _dataCache;
|
||||
|
||||
public MessageOutputStream(I2PAppContext ctx, DataReceiver receiver) {
|
||||
this(ctx, receiver, Packet.MAX_PAYLOAD_SIZE);
|
||||
}
|
||||
public MessageOutputStream(I2PAppContext ctx, DataReceiver receiver, int bufSize) {
|
||||
super();
|
||||
_dataCache = ByteCache.getInstance(128, bufSize);
|
||||
_context = ctx;
|
||||
_log = ctx.logManager().getLog(MessageOutputStream.class);
|
||||
_buf = _dataCache.acquire().getData(); // new byte[bufSize];
|
||||
_dataReceiver = receiver;
|
||||
_dataLock = new Object();
|
||||
_written = 0;
|
||||
_closed = false;
|
||||
_writeTimeout = -1;
|
||||
}
|
||||
|
||||
public void setWriteTimeout(int ms) { _writeTimeout = ms; }
|
||||
public int getWriteTimeout() { return _writeTimeout; }
|
||||
|
||||
public void write(byte b[]) throws IOException {
|
||||
write(b, 0, b.length);
|
||||
}
|
||||
|
||||
public void write(byte b[], int off, int len) throws IOException {
|
||||
//if (_log.shouldLog(Log.DEBUG))
|
||||
// _log.debug("write(b[], " + off + ", " + len + ")");
|
||||
int cur = off;
|
||||
int remaining = len;
|
||||
while (remaining > 0) {
|
||||
WriteStatus ws = null;
|
||||
// we do any waiting outside the synchronized() block because we
|
||||
// want to allow other threads to flushAvailable() whenever they want.
|
||||
// this is the only method that *adds* to the _buf, and all
|
||||
// code that reads from it is synchronized
|
||||
synchronized (_dataLock) {
|
||||
if (_buf == null) throw new IOException("closed (buffer went away)");
|
||||
if (_valid + remaining < _buf.length) {
|
||||
// simply buffer the data, no flush
|
||||
System.arraycopy(b, cur, _buf, _valid, remaining);
|
||||
_valid += remaining;
|
||||
cur += remaining;
|
||||
_written += remaining;
|
||||
remaining = 0;
|
||||
} else {
|
||||
// buffer whatever we can fit then flush,
|
||||
// repeating until we've pushed all of the
|
||||
// data through
|
||||
int toWrite = _buf.length - _valid;
|
||||
System.arraycopy(b, cur, _buf, _valid, toWrite);
|
||||
remaining -= toWrite;
|
||||
cur += toWrite;
|
||||
_valid = _buf.length;
|
||||
ws = _dataReceiver.writeData(_buf, 0, _valid);
|
||||
_written += _valid;
|
||||
_valid = 0;
|
||||
throwAnyError();
|
||||
}
|
||||
}
|
||||
if (ws != null) {
|
||||
// ok, we've actually added a new packet - lets wait until
|
||||
// its accepted into the queue before moving on (so that we
|
||||
// dont fill our buffer instantly)
|
||||
ws.waitForAccept(_writeTimeout);
|
||||
if (!ws.writeAccepted()) {
|
||||
if (_writeTimeout > 0)
|
||||
throw new InterruptedIOException("Write not accepted within timeout");
|
||||
else
|
||||
throw new IOException("Write not accepted into the queue");
|
||||
}
|
||||
}
|
||||
}
|
||||
throwAnyError();
|
||||
}
|
||||
|
||||
public void write(int b) throws IOException {
|
||||
write(new byte[] { (byte)b }, 0, 1);
|
||||
throwAnyError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush the data already queued up, blocking until it has been
|
||||
* delivered.
|
||||
*
|
||||
* @throws IOException if the write fails
|
||||
* @throws InterruptedIOException if the write times out
|
||||
*/
|
||||
public void flush() throws IOException {
|
||||
WriteStatus ws = null;
|
||||
synchronized (_dataLock) {
|
||||
if (_buf == null) throw new IOException("closed (buffer went away)");
|
||||
ws = _dataReceiver.writeData(_buf, 0, _valid);
|
||||
_written += _valid;
|
||||
_valid = 0;
|
||||
_dataLock.notifyAll();
|
||||
}
|
||||
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("before waiting " + _writeTimeout + "ms for completion of " + ws);
|
||||
if (_closed &&
|
||||
( (_writeTimeout > Connection.DISCONNECT_TIMEOUT) ||
|
||||
(_writeTimeout <= 0) ) )
|
||||
ws.waitForCompletion(Connection.DISCONNECT_TIMEOUT);
|
||||
else
|
||||
ws.waitForCompletion(_writeTimeout);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("after waiting " + _writeTimeout + "ms for completion of " + ws);
|
||||
if (ws.writeFailed() && (_writeTimeout > 0) )
|
||||
throw new InterruptedIOException("Timed out during write");
|
||||
else if (ws.writeFailed())
|
||||
throw new IOException("Write failed");
|
||||
throwAnyError();
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
if (_closed) return;
|
||||
_closed = true;
|
||||
flush();
|
||||
_log.debug("Output stream closed after writing " + _written);
|
||||
ByteArray ba = null;
|
||||
synchronized (_dataLock) {
|
||||
if (_buf != null) {
|
||||
ba = new ByteArray(_buf);
|
||||
_buf = null;
|
||||
_valid = 0;
|
||||
}
|
||||
}
|
||||
if (ba != null) {
|
||||
_dataCache.release(ba);
|
||||
}
|
||||
}
|
||||
/** nonblocking close */
|
||||
public void closeInternal() {
|
||||
_closed = true;
|
||||
_streamError = new IOException("Closed internally");
|
||||
ByteArray ba = null;
|
||||
synchronized (_dataLock) {
|
||||
// flush any data, but don't wait for it
|
||||
_dataReceiver.writeData(_buf, 0, _valid);
|
||||
_written += _valid;
|
||||
_valid = 0;
|
||||
|
||||
if (_buf != null) {
|
||||
ba = new ByteArray(_buf);
|
||||
_buf = null;
|
||||
_valid = 0;
|
||||
}
|
||||
_dataLock.notifyAll();
|
||||
}
|
||||
if (ba != null) {
|
||||
_dataCache.release(ba);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean getClosed() { return _closed; }
|
||||
|
||||
private void throwAnyError() throws IOException {
|
||||
if (_streamError != null) {
|
||||
IOException ioe = _streamError;
|
||||
_streamError = null;
|
||||
throw ioe;
|
||||
}
|
||||
}
|
||||
|
||||
void streamErrorOccurred(IOException ioe) {
|
||||
_streamError = ioe;
|
||||
}
|
||||
|
||||
/**
|
||||
* called whenever the engine wants to push more data to the
|
||||
* peer
|
||||
*
|
||||
* @return true if the data was flushed
|
||||
*/
|
||||
void flushAvailable(DataReceiver target) throws IOException {
|
||||
flushAvailable(target, true);
|
||||
}
|
||||
void flushAvailable(DataReceiver target, boolean blocking) throws IOException {
|
||||
WriteStatus ws = null;
|
||||
synchronized (_dataLock) {
|
||||
// _buf may be null, but the data receiver can handle that just fine,
|
||||
// deciding whether or not to send a packet
|
||||
ws = target.writeData(_buf, 0, _valid);
|
||||
_written += _valid;
|
||||
_valid = 0;
|
||||
_dataLock.notifyAll();
|
||||
}
|
||||
if (blocking && ws != null) {
|
||||
ws.waitForAccept(_writeTimeout);
|
||||
if (ws.writeFailed())
|
||||
throw new IOException("Flush available failed");
|
||||
else if (!ws.writeAccepted())
|
||||
throw new InterruptedIOException("Flush available timed out");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
void destroy() {
|
||||
_dataReceiver = null;
|
||||
}
|
||||
|
||||
/** Define a component to receive data flushed from this stream */
|
||||
public interface DataReceiver {
|
||||
/**
|
||||
* Nonblocking write
|
||||
*/
|
||||
public WriteStatus writeData(byte buf[], int off, int size);
|
||||
}
|
||||
|
||||
/** Define a way to detect the status of a write */
|
||||
public interface WriteStatus {
|
||||
/** wait until the data written either fails or succeeds */
|
||||
public void waitForCompletion(int maxWaitMs);
|
||||
/**
|
||||
* wait until the data written is accepted into the outbound pool,
|
||||
* which we throttle rather than accept arbitrary data and queue
|
||||
*/
|
||||
public void waitForAccept(int maxWaitMs);
|
||||
/** the write was accepted. aka did the socket not close? */
|
||||
public boolean writeAccepted();
|
||||
/** did the write fail? */
|
||||
public boolean writeFailed();
|
||||
/** did the write succeed? */
|
||||
public boolean writeSuccessful();
|
||||
}
|
||||
}
|
||||
574
apps/streaming/java/src/net/i2p/client/streaming/Packet.java
Normal file
574
apps/streaming/java/src/net/i2p/client/streaming/Packet.java
Normal file
@@ -0,0 +1,574 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import java.util.Arrays;
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.data.DataFormatException;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.data.Destination;
|
||||
import net.i2p.data.Signature;
|
||||
import net.i2p.data.SigningPrivateKey;
|
||||
|
||||
/**
|
||||
* Contain a single packet transferred as part of a streaming connection.
|
||||
* The data format is as follows:<ul>
|
||||
* <li>{@link #getSendStreamId sendStreamId} [4 byte value]</li>
|
||||
* <li>{@link #getReceiveStreamId receiveStreamId} [4 byte value]</li>
|
||||
* <li>{@link #getSequenceNum sequenceNum} [4 byte unsigned integer]</li>
|
||||
* <li>{@link #getAckThrough ackThrough} [4 byte unsigned integer]</li>
|
||||
* <li>number of NACKs [1 byte unsigned integer]</li>
|
||||
* <li>that many {@link #getNacks NACKs}</li>
|
||||
* <li>{@link #getResendDelay resendDelay} [1 byte integer]</li>
|
||||
* <li>flags [2 byte value]</li>
|
||||
* <li>option data size [2 byte integer]</li>
|
||||
* <li>option data specified by those flags [0 or more bytes]</li>
|
||||
* <li>payload [remaining packet size]</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>The flags field above specifies some metadata about the packet, and in
|
||||
* turn may require certain additional data to be included. The flags are
|
||||
* as follows (with any data structures specified added to the options area
|
||||
* in the given order):</p><ol>
|
||||
* <li>{@link #FLAG_SYNCHRONIZE}: no option data</li>
|
||||
* <li>{@link #FLAG_CLOSE}: no option data</li>
|
||||
* <li>{@link #FLAG_RESET}: no option data</li>
|
||||
* <li>{@link #FLAG_SIGNATURE_INCLUDED}: {@link net.i2p.data.Signature}</li>
|
||||
* <li>{@link #FLAG_SIGNATURE_REQUESTED}: no option data</li>
|
||||
* <li>{@link #FLAG_FROM_INCLUDED}: {@link net.i2p.data.Destination}</li>
|
||||
* <li>{@link #FLAG_DELAY_REQUESTED}: 1 byte integer</li>
|
||||
* <li>{@link #FLAG_MAX_PACKET_SIZE_INCLUDED}: 2 byte integer</li>
|
||||
* <li>{@link #FLAG_PROFILE_INTERACTIVE}: no option data</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>If the signature is included, it uses the Destination's DSA key
|
||||
* to sign the entire header and payload with the space in the options
|
||||
* for the signature being set to all zeroes.</p>
|
||||
*
|
||||
* <p>If the sequenceNum is 0 and the SYN is not set, this is a plain ACK
|
||||
* packet that should not be ACKed</p>
|
||||
*
|
||||
*/
|
||||
public class Packet {
|
||||
private byte _sendStreamId[];
|
||||
private byte _receiveStreamId[];
|
||||
private long _sequenceNum;
|
||||
private long _ackThrough;
|
||||
private long _nacks[];
|
||||
private int _resendDelay;
|
||||
private int _flags;
|
||||
private byte _payload[];
|
||||
// the next four are set only if the flags say so
|
||||
private Signature _optionSignature;
|
||||
private Destination _optionFrom;
|
||||
private int _optionDelay;
|
||||
private int _optionMaxSize;
|
||||
|
||||
/**
|
||||
* The receiveStreamId will be set to this when the packet doesn't know
|
||||
* what ID will be assigned by the remote peer (aka this is the initial
|
||||
* synchronize packet)
|
||||
*
|
||||
*/
|
||||
public static final byte STREAM_ID_UNKNOWN[] = new byte[] { 0x00, 0x00, 0x00, 0x00 };
|
||||
|
||||
/**
|
||||
* This packet is creating a new socket connection (if the receiveStreamId
|
||||
* is STREAM_ID_UNKNOWN) or it is acknowledging a request to
|
||||
* create a connection and in turn is accepting the socket.
|
||||
*
|
||||
*/
|
||||
public static final int FLAG_SYNCHRONIZE = (1 << 0);
|
||||
/**
|
||||
* The sender of this packet will not be sending any more payload data.
|
||||
*/
|
||||
public static final int FLAG_CLOSE = (1 << 1);
|
||||
/**
|
||||
* This packet is being sent to signify that the socket does not exist
|
||||
* (or, if in response to an initial synchronize packet, that the
|
||||
* connection was refused).
|
||||
*
|
||||
*/
|
||||
public static final int FLAG_RESET = (1 << 2);
|
||||
/**
|
||||
* This packet contains a DSA signature from the packet's sender. This
|
||||
* signature is within the packet options. All synchronize packets must
|
||||
* have this flag set.
|
||||
*
|
||||
*/
|
||||
public static final int FLAG_SIGNATURE_INCLUDED = (1 << 3);
|
||||
/**
|
||||
* This packet wants the recipient to include signatures on subsequent
|
||||
* packets sent to the creator of this packet.
|
||||
*/
|
||||
public static final int FLAG_SIGNATURE_REQUESTED = (1 << 4);
|
||||
/**
|
||||
* This packet includes the full I2P destination of the packet's sender.
|
||||
* The initial synchronize packet must have this flag set.
|
||||
*/
|
||||
public static final int FLAG_FROM_INCLUDED = (1 << 5);
|
||||
/**
|
||||
* This packet includes an explicit request for the recipient to delay
|
||||
* sending any packets with data for a given amount of time.
|
||||
*
|
||||
*/
|
||||
public static final int FLAG_DELAY_REQUESTED = (1 << 6);
|
||||
/**
|
||||
* This packet includes a request that the recipient not send any
|
||||
* subsequent packets with payloads greater than a specific size.
|
||||
* If not set and no prior value was delivered, the maximum value
|
||||
* will be assumed (approximately 32KB).
|
||||
*
|
||||
*/
|
||||
public static final int FLAG_MAX_PACKET_SIZE_INCLUDED = (1 << 7);
|
||||
/**
|
||||
* If set, this packet is travelling as part of an interactive flow,
|
||||
* meaning it is more lag sensitive than throughput sensitive. aka
|
||||
* send data ASAP rather than waiting around to send full packets.
|
||||
*
|
||||
*/
|
||||
public static final int FLAG_PROFILE_INTERACTIVE = (1 << 8);
|
||||
/**
|
||||
* If set, this packet is a ping (if sendStreamId is set) or a
|
||||
* ping reply (if receiveStreamId is set).
|
||||
*/
|
||||
public static final int FLAG_ECHO = (1 << 9);
|
||||
|
||||
public static final int DEFAULT_MAX_SIZE = 32*1024;
|
||||
private static final int MAX_DELAY_REQUEST = 65535;
|
||||
|
||||
/** what stream is this packet a part of? */
|
||||
public byte[] getSendStreamId() {
|
||||
if ( (_sendStreamId == null) || (DataHelper.eq(_sendStreamId, STREAM_ID_UNKNOWN)) )
|
||||
return null;
|
||||
else
|
||||
return _sendStreamId;
|
||||
}
|
||||
public void setSendStreamId(byte[] id) {
|
||||
_sendStreamId = id;
|
||||
if ( (id != null) && (DataHelper.eq(id, STREAM_ID_UNKNOWN)) )
|
||||
_sendStreamId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream that replies should be sent on. if the
|
||||
* connection is still being built, this should be
|
||||
* null.
|
||||
*
|
||||
*/
|
||||
public byte[] getReceiveStreamId() {
|
||||
if ( (_receiveStreamId == null) || (DataHelper.eq(_receiveStreamId, STREAM_ID_UNKNOWN)) )
|
||||
return null;
|
||||
else
|
||||
return _receiveStreamId;
|
||||
}
|
||||
public void setReceiveStreamId(byte[] id) {
|
||||
_receiveStreamId = id;
|
||||
if ( (id != null) && (DataHelper.eq(id, STREAM_ID_UNKNOWN)) )
|
||||
_receiveStreamId = null;
|
||||
}
|
||||
|
||||
/** 0-indexed sequence number for this Packet in the sendStream */
|
||||
public long getSequenceNum() { return _sequenceNum; }
|
||||
public void setSequenceNum(long num) { _sequenceNum = num; }
|
||||
|
||||
/**
|
||||
* The highest packet sequence number that received
|
||||
* on the receiveStreamId. This field is ignored on the initial
|
||||
* connection packet (where receiveStreamId is the unknown id).
|
||||
*
|
||||
*/
|
||||
public long getAckThrough() { return _ackThrough; }
|
||||
public void setAckThrough(long id) { _ackThrough = id; }
|
||||
|
||||
/**
|
||||
* List of packet sequence numbers below the getAckThrough() value
|
||||
* have not been received. this may be null.
|
||||
*
|
||||
*/
|
||||
public long[] getNacks() { return _nacks; }
|
||||
public void setNacks(long nacks[]) { _nacks = nacks; }
|
||||
|
||||
/**
|
||||
* How long is the creator of this packet going to wait before
|
||||
* resending this packet (if it hasn't yet been ACKed). The
|
||||
* value is seconds since the packet was created.
|
||||
*
|
||||
*/
|
||||
public int getResendDelay() { return _resendDelay; }
|
||||
public void setResendDelay(int numSeconds) { _resendDelay = numSeconds; }
|
||||
|
||||
public static final int MAX_PAYLOAD_SIZE = 32*1024;
|
||||
|
||||
/** get the actual payload of the message. may be null */
|
||||
public byte[] getPayload() { return _payload; }
|
||||
public void setPayload(byte payload[]) {
|
||||
_payload = payload;
|
||||
if ( (payload != null) && (payload.length > MAX_PAYLOAD_SIZE) )
|
||||
throw new IllegalArgumentException("Too large payload: " + payload.length);
|
||||
}
|
||||
public int getPayloadSize() {
|
||||
return (_payload == null ? 0 : _payload.length);
|
||||
}
|
||||
|
||||
/** is a particular flag set on this packet? */
|
||||
public boolean isFlagSet(int flag) { return 0 != (_flags & flag); }
|
||||
public void setFlag(int flag) { _flags |= flag; }
|
||||
public void setFlag(int flag, boolean set) {
|
||||
if (set)
|
||||
_flags |= flag;
|
||||
else
|
||||
_flags &= ~flag;
|
||||
}
|
||||
|
||||
/** the signature on the packet (only included if the flag for it is set) */
|
||||
public Signature getOptionalSignature() { return _optionSignature; }
|
||||
public void setOptionalSignature(Signature sig) {
|
||||
setFlag(FLAG_SIGNATURE_INCLUDED, sig != null);
|
||||
_optionSignature = sig;
|
||||
}
|
||||
|
||||
/** the sender of the packet (only included if the flag for it is set) */
|
||||
public Destination getOptionalFrom() { return _optionFrom; }
|
||||
public void setOptionalFrom(Destination from) {
|
||||
setFlag(FLAG_FROM_INCLUDED, from != null);
|
||||
if (from == null) throw new RuntimeException("from is null!?");
|
||||
_optionFrom = from;
|
||||
}
|
||||
|
||||
/**
|
||||
* How many milliseconds the sender of this packet wants the recipient
|
||||
* to wait before sending any more data (only valid if the flag for it is
|
||||
* set)
|
||||
*/
|
||||
public int getOptionalDelay() { return _optionDelay; }
|
||||
public void setOptionalDelay(int delayMs) {
|
||||
setFlag(FLAG_DELAY_REQUESTED, delayMs > 0);
|
||||
if (delayMs > MAX_DELAY_REQUEST)
|
||||
_optionDelay = MAX_DELAY_REQUEST;
|
||||
else if (delayMs < 0)
|
||||
_optionDelay = 0;
|
||||
else
|
||||
_optionDelay = delayMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* What is the largest payload the sender of this packet wants to receive?
|
||||
*
|
||||
*/
|
||||
public int getOptionalMaxSize() { return _optionMaxSize; }
|
||||
public void setOptionalMaxSize(int numBytes) {
|
||||
setFlag(FLAG_MAX_PACKET_SIZE_INCLUDED, numBytes > 0);
|
||||
_optionMaxSize = numBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the packet to the buffer (starting at the offset) and return
|
||||
* the number of bytes written.
|
||||
*
|
||||
* @throws IllegalStateException if there is data missing or otherwise b0rked
|
||||
*/
|
||||
public int writePacket(byte buffer[], int offset) throws IllegalStateException {
|
||||
return writePacket(buffer, offset, true);
|
||||
}
|
||||
/**
|
||||
* @param includeSig if true, include the real signature, otherwise put zeroes
|
||||
* in its place.
|
||||
*/
|
||||
private int writePacket(byte buffer[], int offset, boolean includeSig) throws IllegalStateException {
|
||||
int cur = offset;
|
||||
if ( (_sendStreamId != null) && (_sendStreamId.length == 4) )
|
||||
System.arraycopy(_sendStreamId, 0, buffer, cur, _sendStreamId.length);
|
||||
else
|
||||
System.arraycopy(STREAM_ID_UNKNOWN, 0, buffer, cur, STREAM_ID_UNKNOWN.length);
|
||||
cur += 4;
|
||||
if ( (_receiveStreamId != null) && (_receiveStreamId.length == 4) )
|
||||
System.arraycopy(_receiveStreamId, 0, buffer, cur, _receiveStreamId.length);
|
||||
else
|
||||
System.arraycopy(STREAM_ID_UNKNOWN, 0, buffer, cur, STREAM_ID_UNKNOWN.length);
|
||||
cur += 4;
|
||||
DataHelper.toLong(buffer, cur, 4, _sequenceNum > 0 ? _sequenceNum : 0);
|
||||
cur += 4;
|
||||
DataHelper.toLong(buffer, cur, 4, _ackThrough > 0 ? _ackThrough : 0);
|
||||
cur += 4;
|
||||
if (_nacks != null) {
|
||||
DataHelper.toLong(buffer, cur, 1, _nacks.length);
|
||||
cur++;
|
||||
for (int i = 0; i < _nacks.length; i++) {
|
||||
DataHelper.toLong(buffer, cur, 4, _nacks[i]);
|
||||
cur += 4;
|
||||
}
|
||||
} else {
|
||||
DataHelper.toLong(buffer, cur, 1, 0);
|
||||
cur++;
|
||||
}
|
||||
DataHelper.toLong(buffer, cur, 1, _resendDelay > 0 ? _resendDelay : 0);
|
||||
cur++;
|
||||
DataHelper.toLong(buffer, cur, 2, _flags);
|
||||
cur += 2;
|
||||
|
||||
int optionSize = 0;
|
||||
if (isFlagSet(FLAG_DELAY_REQUESTED))
|
||||
optionSize += 2;
|
||||
if (isFlagSet(FLAG_FROM_INCLUDED))
|
||||
optionSize += _optionFrom.size();
|
||||
if (isFlagSet(FLAG_MAX_PACKET_SIZE_INCLUDED))
|
||||
optionSize += 2;
|
||||
if (isFlagSet(FLAG_SIGNATURE_INCLUDED))
|
||||
optionSize += Signature.SIGNATURE_BYTES;
|
||||
|
||||
DataHelper.toLong(buffer, cur, 2, optionSize);
|
||||
cur += 2;
|
||||
|
||||
if (isFlagSet(FLAG_DELAY_REQUESTED)) {
|
||||
DataHelper.toLong(buffer, cur, 2, _optionDelay > 0 ? _optionDelay : 0);
|
||||
cur += 2;
|
||||
}
|
||||
if (isFlagSet(FLAG_FROM_INCLUDED)) {
|
||||
cur += _optionFrom.writeBytes(buffer, cur);
|
||||
}
|
||||
if (isFlagSet(FLAG_MAX_PACKET_SIZE_INCLUDED)) {
|
||||
DataHelper.toLong(buffer, cur, 2, _optionMaxSize > 0 ? _optionMaxSize : DEFAULT_MAX_SIZE);
|
||||
cur += 2;
|
||||
}
|
||||
if (isFlagSet(FLAG_SIGNATURE_INCLUDED)) {
|
||||
if (includeSig)
|
||||
System.arraycopy(_optionSignature.getData(), 0, buffer, cur, Signature.SIGNATURE_BYTES);
|
||||
else // we're signing (or validating)
|
||||
Arrays.fill(buffer, cur, cur+Signature.SIGNATURE_BYTES, (byte)0x0);
|
||||
cur += Signature.SIGNATURE_BYTES;
|
||||
}
|
||||
|
||||
if (_payload != null) {
|
||||
try {
|
||||
System.arraycopy(_payload, 0, buffer, cur, _payload.length);
|
||||
} catch (ArrayIndexOutOfBoundsException aioobe) {
|
||||
System.err.println("payload.length: " + _payload.length + " buffer.length: " + buffer.length + " cur: " + cur);
|
||||
throw aioobe;
|
||||
}
|
||||
cur += _payload.length;
|
||||
}
|
||||
|
||||
return cur - offset;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* how large would this packet be if we wrote it
|
||||
*/
|
||||
public int writtenSize() throws IllegalStateException {
|
||||
int size = 0;
|
||||
size += 4; // _sendStreamId.length;
|
||||
size += 4; // _receiveStreamId.length;
|
||||
size += 4; // sequenceNum
|
||||
size += 4; // ackThrough
|
||||
if (_nacks != null) {
|
||||
size++; // nacks length
|
||||
size += 4 * _nacks.length;
|
||||
} else {
|
||||
size++; // nacks length
|
||||
}
|
||||
size++; // resendDelay
|
||||
size += 2; // flags
|
||||
|
||||
if (isFlagSet(FLAG_DELAY_REQUESTED))
|
||||
size += 2;
|
||||
if (isFlagSet(FLAG_FROM_INCLUDED))
|
||||
size += _optionFrom.size();
|
||||
if (isFlagSet(FLAG_MAX_PACKET_SIZE_INCLUDED))
|
||||
size += 2;
|
||||
if (isFlagSet(FLAG_SIGNATURE_INCLUDED))
|
||||
size += Signature.SIGNATURE_BYTES;
|
||||
|
||||
size += 2; // option size
|
||||
|
||||
if (_payload != null) {
|
||||
size += _payload.length;
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
/**
|
||||
* Read the packet from the buffer (starting at the offset) and return
|
||||
* the number of bytes read.
|
||||
*
|
||||
* @param buffer packet buffer containing the data
|
||||
* @param offset index into the buffer to start readign
|
||||
* @param length how many bytes within the buffer past the offset are
|
||||
* part of the packet?
|
||||
*
|
||||
* @throws IllegalArgumentException if the data is b0rked
|
||||
*/
|
||||
public void readPacket(byte buffer[], int offset, int length) throws IllegalArgumentException {
|
||||
if (buffer.length - offset < length)
|
||||
throw new IllegalArgumentException("len=" + buffer.length + " off=" + offset + " length=" + length);
|
||||
if (length < 22) // min header size
|
||||
throw new IllegalArgumentException("Too small: len=" + buffer.length);
|
||||
int cur = offset;
|
||||
_sendStreamId = new byte[4];
|
||||
System.arraycopy(buffer, cur, _sendStreamId, 0, 4);
|
||||
cur += 4;
|
||||
_receiveStreamId = new byte[4];
|
||||
System.arraycopy(buffer, cur, _receiveStreamId, 0, 4);
|
||||
cur += 4;
|
||||
_sequenceNum = DataHelper.fromLong(buffer, cur, 4);
|
||||
cur += 4;
|
||||
_ackThrough = DataHelper.fromLong(buffer, cur, 4);
|
||||
cur += 4;
|
||||
int numNacks = (int)DataHelper.fromLong(buffer, cur, 1);
|
||||
cur++;
|
||||
if (length < 22 + numNacks*4)
|
||||
throw new IllegalArgumentException("Too small with " + numNacks + " nacks: " + length);
|
||||
if (numNacks > 0) {
|
||||
_nacks = new long[numNacks];
|
||||
for (int i = 0; i < numNacks; i++) {
|
||||
_nacks[i] = DataHelper.fromLong(buffer, cur, 4);
|
||||
cur += 4;
|
||||
}
|
||||
} else {
|
||||
_nacks = null;
|
||||
}
|
||||
_resendDelay = (int)DataHelper.fromLong(buffer, cur, 1);
|
||||
cur++;
|
||||
_flags = (int)DataHelper.fromLong(buffer, cur, 2);
|
||||
cur += 2;
|
||||
|
||||
int optionSize = (int)DataHelper.fromLong(buffer, cur, 2);
|
||||
cur += 2;
|
||||
|
||||
if (length < 22 + numNacks*4 + optionSize)
|
||||
throw new IllegalArgumentException("Too small with " + numNacks + " nacks and "
|
||||
+ optionSize + " options: " + length);
|
||||
|
||||
int payloadBegin = cur + optionSize;
|
||||
int payloadSize = length - payloadBegin;
|
||||
if ( (payloadSize < 0) || (payloadSize > MAX_PAYLOAD_SIZE) )
|
||||
throw new IllegalArgumentException("length: " + length + " offset: " + offset + " begin: " + payloadBegin);
|
||||
|
||||
// skip ahead to the payload
|
||||
_payload = new byte[payloadSize];
|
||||
System.arraycopy(buffer, payloadBegin, _payload, 0, payloadSize);
|
||||
|
||||
// ok now lets go back and deal with the options
|
||||
if (isFlagSet(FLAG_DELAY_REQUESTED)) {
|
||||
_optionDelay = (int)DataHelper.fromLong(buffer, cur, 2);
|
||||
cur += 2;
|
||||
}
|
||||
if (isFlagSet(FLAG_FROM_INCLUDED)) {
|
||||
_optionFrom = new Destination();
|
||||
try {
|
||||
cur += _optionFrom.readBytes(buffer, cur);
|
||||
} catch (DataFormatException dfe) {
|
||||
throw new IllegalArgumentException("Bad from field: " + dfe.getMessage());
|
||||
}
|
||||
}
|
||||
if (isFlagSet(FLAG_MAX_PACKET_SIZE_INCLUDED)) {
|
||||
_optionMaxSize = (int)DataHelper.fromLong(buffer, cur, 2);
|
||||
cur += 2;
|
||||
}
|
||||
if (isFlagSet(FLAG_SIGNATURE_INCLUDED)) {
|
||||
_optionSignature = new Signature();
|
||||
byte buf[] = new byte[Signature.SIGNATURE_BYTES];
|
||||
System.arraycopy(buffer, cur, buf, 0, Signature.SIGNATURE_BYTES);
|
||||
_optionSignature.setData(buf);
|
||||
cur += Signature.SIGNATURE_BYTES;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the signature on the data is valid.
|
||||
*
|
||||
* @return true if the signature exists and validates against the data,
|
||||
* false otherwise.
|
||||
*/
|
||||
public boolean verifySignature(I2PAppContext ctx, Destination from, byte buffer[]) {
|
||||
if (!isFlagSet(FLAG_SIGNATURE_INCLUDED)) return false;
|
||||
if (_optionSignature == null) return false;
|
||||
|
||||
int size = writtenSize();
|
||||
|
||||
if (buffer == null)
|
||||
buffer = new byte[size];
|
||||
int written = writePacket(buffer, 0, false);
|
||||
if (written != size) {
|
||||
ctx.logManager().getLog(Packet.class).error("Written " + written + " size " + size + " for " + toString(), new Exception("moo"));
|
||||
return false;
|
||||
}
|
||||
boolean ok = ctx.dsa().verifySignature(_optionSignature, buffer, 0, size, from.getSigningPublicKey());
|
||||
if (!ok) {
|
||||
ctx.logManager().getLog(Packet.class).error("Signature failed on " + toString(), new Exception("moo"));
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign and write the packet to the buffer (starting at the offset) and return
|
||||
* the number of bytes written.
|
||||
*
|
||||
* @throws IllegalStateException if there is data missing or otherwise b0rked
|
||||
*/
|
||||
public int writeSignedPacket(byte buffer[], int offset, I2PAppContext ctx, SigningPrivateKey key) throws IllegalStateException {
|
||||
setFlag(FLAG_SIGNATURE_INCLUDED);
|
||||
int size = writePacket(buffer, offset, false);
|
||||
_optionSignature = ctx.dsa().sign(buffer, offset, size, key);
|
||||
// jump into the signed data and inject the signature where we
|
||||
// previously placed a bunch of zeroes
|
||||
int signatureOffset = offset
|
||||
+ 4 // sendStreamId
|
||||
+ 4 // receiveStreamId
|
||||
+ 4 // sequenceNum
|
||||
+ 4 // ackThrough
|
||||
+ (_nacks != null ? 4*_nacks.length + 1 : 1)
|
||||
+ 1 // resendDelay
|
||||
+ 2 // flags
|
||||
+ 2 // optionSize
|
||||
+ (isFlagSet(FLAG_DELAY_REQUESTED) ? 2 : 0)
|
||||
+ (isFlagSet(FLAG_FROM_INCLUDED) ? _optionFrom.size() : 0)
|
||||
+ (isFlagSet(FLAG_MAX_PACKET_SIZE_INCLUDED) ? 2 : 0);
|
||||
System.arraycopy(_optionSignature.getData(), 0, buffer, signatureOffset, Signature.SIGNATURE_BYTES);
|
||||
return size;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
StringBuffer buf = new StringBuffer(64);
|
||||
buf.append(toId(_sendStreamId));
|
||||
//buf.append("<-->");
|
||||
buf.append(toId(_receiveStreamId)).append(": #").append(_sequenceNum);
|
||||
if (_sequenceNum < 10)
|
||||
buf.append(" \t"); // so the tab lines up right
|
||||
else
|
||||
buf.append('\t');
|
||||
buf.append(toFlagString());
|
||||
buf.append(" ACK ").append(_ackThrough);
|
||||
if (_nacks != null) {
|
||||
buf.append(" NACK");
|
||||
for (int i = 0; i < _nacks.length; i++) {
|
||||
buf.append(" ").append(_nacks[i]);
|
||||
}
|
||||
}
|
||||
if ( (_payload != null) && (_payload.length > 0) )
|
||||
buf.append(" data: ").append(_payload.length);
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
private static final String toId(byte id[]) {
|
||||
if (id == null)
|
||||
return Base64.encode(STREAM_ID_UNKNOWN);
|
||||
else
|
||||
return Base64.encode(id);
|
||||
}
|
||||
|
||||
private final String toFlagString() {
|
||||
StringBuffer buf = new StringBuffer(32);
|
||||
if (isFlagSet(FLAG_CLOSE)) buf.append(" CLOSE");
|
||||
if (isFlagSet(FLAG_DELAY_REQUESTED)) buf.append(" DELAY ").append(_optionDelay);
|
||||
if (isFlagSet(FLAG_ECHO)) buf.append(" ECHO");
|
||||
if (isFlagSet(FLAG_FROM_INCLUDED)) buf.append(" FROM");
|
||||
if (isFlagSet(FLAG_MAX_PACKET_SIZE_INCLUDED)) buf.append(" MS");
|
||||
if (isFlagSet(FLAG_PROFILE_INTERACTIVE)) buf.append(" INTERACTIVE");
|
||||
if (isFlagSet(FLAG_RESET)) buf.append(" RESET");
|
||||
if (isFlagSet(FLAG_SIGNATURE_INCLUDED)) buf.append(" SIG");
|
||||
if (isFlagSet(FLAG_SIGNATURE_REQUESTED)) buf.append(" SIGREQ");
|
||||
if (isFlagSet(FLAG_SYNCHRONIZE)) buf.append(" SYN");
|
||||
return buf.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
import java.text.SimpleDateFormat;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.I2PException;
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.util.Log;
|
||||
import net.i2p.util.SimpleTimer;
|
||||
|
||||
/**
|
||||
* receive a packet and dispatch it correctly to the connection specified,
|
||||
* the server socket, or queue a reply RST packet.
|
||||
*
|
||||
*/
|
||||
public class PacketHandler {
|
||||
private ConnectionManager _manager;
|
||||
private I2PAppContext _context;
|
||||
private Log _log;
|
||||
private int _lastDelay;
|
||||
|
||||
public PacketHandler(I2PAppContext ctx, ConnectionManager mgr) {
|
||||
_manager = mgr;
|
||||
_context = ctx;
|
||||
_log = ctx.logManager().getLog(PacketHandler.class);
|
||||
_lastDelay = _context.random().nextInt(30*1000);
|
||||
}
|
||||
|
||||
private boolean choke(Packet packet) {
|
||||
if (false) {
|
||||
// artificial choke: 2% random drop and a 0-30s
|
||||
// random tiered delay from 0-30s
|
||||
if (_context.random().nextInt(100) >= 95) {
|
||||
displayPacket(packet, "DROP");
|
||||
return false;
|
||||
} else {
|
||||
// if (true) return true; // no lag, just drop
|
||||
/*
|
||||
int delay = _context.random().nextInt(5*1000);
|
||||
*/
|
||||
int delay = _context.random().nextInt(6*1000);
|
||||
int delayFactor = _context.random().nextInt(100);
|
||||
if (delayFactor > 80) {
|
||||
if (delayFactor > 98)
|
||||
delay *= 5;
|
||||
else if (delayFactor > 95)
|
||||
delay *= 4;
|
||||
else if (delayFactor > 90)
|
||||
delay *= 3;
|
||||
else
|
||||
delay *= 2;
|
||||
}
|
||||
|
||||
if (_context.random().nextInt(100) >= 20)
|
||||
delay = _lastDelay;
|
||||
|
||||
_lastDelay = delay;
|
||||
SimpleTimer.getInstance().addEvent(new Reinject(packet, delay), delay);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class Reinject implements SimpleTimer.TimedEvent {
|
||||
private Packet _packet;
|
||||
private int _delay;
|
||||
public Reinject(Packet packet, int delay) {
|
||||
_packet = packet;
|
||||
_delay = delay;
|
||||
}
|
||||
public void timeReached() {
|
||||
_log.debug("Reinjecting after " + _delay + ": " + _packet);
|
||||
receivePacketDirect(_packet);
|
||||
}
|
||||
}
|
||||
|
||||
void receivePacket(Packet packet) {
|
||||
boolean ok = choke(packet);
|
||||
if (ok)
|
||||
receivePacketDirect(packet);
|
||||
}
|
||||
|
||||
private void receivePacketDirect(Packet packet) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("packet received: " + packet);
|
||||
|
||||
byte sendId[] = packet.getSendStreamId();
|
||||
if (!isNonZero(sendId))
|
||||
sendId = null;
|
||||
|
||||
Connection con = (sendId != null ? _manager.getConnectionByInboundId(sendId) : null);
|
||||
if (con != null) {
|
||||
receiveKnownCon(con, packet);
|
||||
displayPacket(packet, "RECV");
|
||||
} else {
|
||||
receiveUnknownCon(packet, sendId);
|
||||
displayPacket(packet, "UNKN");
|
||||
}
|
||||
}
|
||||
|
||||
private static final SimpleDateFormat _fmt = new SimpleDateFormat("hh:mm:ss.SSS");
|
||||
void displayPacket(Packet packet, String prefix) {
|
||||
String msg = null;
|
||||
synchronized (_fmt) {
|
||||
msg = _fmt.format(new Date()) + ": " + prefix + " " + packet.toString();
|
||||
}
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
System.out.println(msg);
|
||||
}
|
||||
|
||||
private void receiveKnownCon(Connection con, Packet packet) {
|
||||
// the packet is pointed at a stream ID we're receiving on
|
||||
if (isValidMatch(con.getSendStreamId(), packet.getReceiveStreamId())) {
|
||||
// the packet's receive stream ID also matches what we expect
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("receive valid: " + packet);
|
||||
try {
|
||||
con.getPacketHandler().receivePacket(packet, con);
|
||||
} catch (I2PException ie) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Received forged packet for " + con, ie);
|
||||
}
|
||||
} else {
|
||||
if (packet.isFlagSet(Packet.FLAG_RESET)) {
|
||||
// refused
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("receive reset: " + packet);
|
||||
try {
|
||||
con.getPacketHandler().receivePacket(packet, con);
|
||||
} catch (I2PException ie) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Received forged reset for " + con, ie);
|
||||
}
|
||||
} else if (packet.isFlagSet(Packet.FLAG_SYNCHRONIZE)) {
|
||||
if ( (con.getSendStreamId() == null) ||
|
||||
(DataHelper.eq(con.getSendStreamId(), packet.getReceiveStreamId())) ) {
|
||||
byte oldId[] =con.getSendStreamId();
|
||||
// con fully established, w00t
|
||||
con.setSendStreamId(packet.getReceiveStreamId());
|
||||
try {
|
||||
con.getPacketHandler().receivePacket(packet, con);
|
||||
} catch (I2PException ie) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Received forged syn for " + con, ie);
|
||||
con.setSendStreamId(oldId);
|
||||
}
|
||||
} else {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Receive a syn packet with the wrong IDs: " + packet);
|
||||
}
|
||||
} else {
|
||||
// someone is sending us a packet on the wrong stream
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Received a packet on the wrong stream: " + packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void receiveUnknownCon(Packet packet, byte sendId[]) {
|
||||
if (packet.isFlagSet(Packet.FLAG_ECHO)) {
|
||||
if (packet.getSendStreamId() != null) {
|
||||
receivePing(packet);
|
||||
} else if (packet.getReceiveStreamId() != null) {
|
||||
receivePong(packet);
|
||||
} else {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Echo packet received with no stream IDs: " + packet);
|
||||
}
|
||||
} else {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Packet received on an unknown stream (and not an ECHO): " + packet);
|
||||
if (sendId == null) {
|
||||
Connection con = _manager.getConnectionByOutboundId(packet.getReceiveStreamId());
|
||||
if (con != null) {
|
||||
if (con.getAckedPackets() <= 0) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Received additional packets before the syn on " + con + ": " + packet);
|
||||
receiveKnownCon(con, packet);
|
||||
return;
|
||||
} else {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("hrmph, received while ack of syn was in flight on " + con + ": " + packet + " acked: " + con.getAckedPackets());
|
||||
receiveKnownCon(con, packet);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (packet.isFlagSet(Packet.FLAG_SYNCHRONIZE)) {
|
||||
_manager.getConnectionHandler().receiveNewSyn(packet);
|
||||
} else {
|
||||
if (_log.shouldLog(Log.WARN)) {
|
||||
StringBuffer buf = new StringBuffer(128);
|
||||
Set cons = _manager.listConnections();
|
||||
for (Iterator iter = cons.iterator(); iter.hasNext(); ) {
|
||||
Connection con = (Connection)iter.next();
|
||||
buf.append(con.toString()).append(" ");
|
||||
}
|
||||
_log.warn("Packet belongs to no other cons: " + packet + " connections: "
|
||||
+ buf.toString() + " sendId: "
|
||||
+ (sendId != null ? Base64.encode(sendId) : " unknown"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void receivePing(Packet packet) {
|
||||
boolean ok = packet.verifySignature(_context, packet.getOptionalFrom(), null);
|
||||
if (!ok) {
|
||||
if (_log.shouldLog(Log.WARN)) {
|
||||
if (packet.getOptionalFrom() == null)
|
||||
_log.warn("Ping with no from (flagged? " + packet.isFlagSet(Packet.FLAG_FROM_INCLUDED) + ")");
|
||||
else if (packet.getOptionalSignature() == null)
|
||||
_log.warn("Ping with no signature (flagged? " + packet.isFlagSet(Packet.FLAG_SIGNATURE_INCLUDED) + ")");
|
||||
else
|
||||
_log.warn("Forged ping, discard (from=" + packet.getOptionalFrom().calculateHash().toBase64()
|
||||
+ " sig=" + packet.getOptionalSignature().toBase64() + ")");
|
||||
}
|
||||
} else {
|
||||
PacketLocal pong = new PacketLocal(_context, packet.getOptionalFrom());
|
||||
pong.setFlag(Packet.FLAG_ECHO, true);
|
||||
pong.setFlag(Packet.FLAG_SIGNATURE_INCLUDED, false);
|
||||
pong.setReceiveStreamId(packet.getSendStreamId());
|
||||
_manager.getPacketQueue().enqueue(pong);
|
||||
}
|
||||
}
|
||||
|
||||
private void receivePong(Packet packet) {
|
||||
_manager.receivePong(packet.getReceiveStreamId());
|
||||
}
|
||||
|
||||
private static final boolean isValidMatch(byte conStreamId[], byte packetStreamId[]) {
|
||||
if ( (conStreamId == null) || (packetStreamId == null) ||
|
||||
(conStreamId.length != packetStreamId.length) )
|
||||
return false;
|
||||
|
||||
boolean nonZeroFound = false;
|
||||
for (int i = 0; i < conStreamId.length; i++) {
|
||||
if (conStreamId[i] != packetStreamId[i]) return false;
|
||||
if (conStreamId[i] != 0x0) nonZeroFound = true;
|
||||
}
|
||||
return nonZeroFound;
|
||||
}
|
||||
|
||||
private static final boolean isNonZero(byte[] b) {
|
||||
boolean nonZeroFound = false;
|
||||
for (int i = 0; b != null && i < b.length; i++) {
|
||||
if (b[i] != 0x0)
|
||||
nonZeroFound = true;
|
||||
}
|
||||
return nonZeroFound;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.data.Destination;
|
||||
import net.i2p.data.SessionKey;
|
||||
|
||||
/**
|
||||
* coordinate local attributes about a packet - send time, ack time, number of
|
||||
* retries, etc.
|
||||
*/
|
||||
public class PacketLocal extends Packet implements MessageOutputStream.WriteStatus {
|
||||
private I2PAppContext _context;
|
||||
private Connection _connection;
|
||||
private Destination _to;
|
||||
private SessionKey _keyUsed;
|
||||
private Set _tagsSent;
|
||||
private long _createdOn;
|
||||
private int _numSends;
|
||||
private long _lastSend;
|
||||
private long _acceptedOn;
|
||||
private long _ackOn;
|
||||
private long _cancelledOn;
|
||||
|
||||
public PacketLocal(I2PAppContext ctx, Destination to) {
|
||||
this(ctx, to, null);
|
||||
}
|
||||
public PacketLocal(I2PAppContext ctx, Destination to, Connection con) {
|
||||
_context = ctx;
|
||||
_createdOn = ctx.clock().now();
|
||||
_to = to;
|
||||
_connection = con;
|
||||
_lastSend = -1;
|
||||
_cancelledOn = -1;
|
||||
}
|
||||
|
||||
public Destination getTo() { return _to; }
|
||||
public void setTo(Destination to) { _to = to; }
|
||||
|
||||
public SessionKey getKeyUsed() { return _keyUsed; }
|
||||
public void setKeyUsed(SessionKey key) { _keyUsed = key; }
|
||||
|
||||
public Set getTagsSent() { return _tagsSent; }
|
||||
public void setTagsSent(Set tags) {
|
||||
if ( (_tagsSent != null) && (_tagsSent.size() > 0) && (tags.size() > 0) ) {
|
||||
//int old = _tagsSent.size();
|
||||
//_tagsSent.addAll(tags);
|
||||
if (!_tagsSent.equals(tags))
|
||||
System.out.println("ERROR: dup tags: old=" + _tagsSent.size() + " new=" + tags.size() + " packet: " + toString());
|
||||
} else {
|
||||
_tagsSent = tags;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean shouldSign() {
|
||||
return isFlagSet(FLAG_SIGNATURE_INCLUDED) ||
|
||||
isFlagSet(FLAG_SYNCHRONIZE) ||
|
||||
isFlagSet(FLAG_CLOSE) ||
|
||||
isFlagSet(FLAG_ECHO);
|
||||
}
|
||||
|
||||
/** last minute update of ack fields, just before write/sign */
|
||||
public void prepare() {
|
||||
if (_connection != null)
|
||||
_connection.getInputStream().updateAcks(this);
|
||||
}
|
||||
|
||||
public long getCreatedOn() { return _createdOn; }
|
||||
public long getLifetime() { return _context.clock().now() - _createdOn; }
|
||||
public void incrementSends() {
|
||||
_numSends++;
|
||||
_lastSend = _context.clock().now();
|
||||
}
|
||||
public void ackReceived() {
|
||||
synchronized (this) {
|
||||
if (_ackOn <= 0)
|
||||
_ackOn = _context.clock().now();
|
||||
notifyAll();
|
||||
}
|
||||
_connection = null;
|
||||
}
|
||||
public void cancelled() {
|
||||
synchronized (this) {
|
||||
_cancelledOn = _context.clock().now();
|
||||
notifyAll();
|
||||
}
|
||||
_connection = null;
|
||||
}
|
||||
|
||||
/** how long after packet creation was it acked? */
|
||||
public int getAckTime() {
|
||||
if (_ackOn <= 0)
|
||||
return -1;
|
||||
else
|
||||
return (int)(_ackOn - _createdOn);
|
||||
}
|
||||
public int getNumSends() { return _numSends; }
|
||||
public long getLastSend() { return _lastSend; }
|
||||
public Connection getConnection() { return _connection; }
|
||||
|
||||
public String toString() {
|
||||
String str = super.toString();
|
||||
if (_ackOn > 0)
|
||||
return str + " ack after " + getAckTime();
|
||||
else
|
||||
return str;
|
||||
}
|
||||
|
||||
public void waitForAccept(int maxWaitMs) {
|
||||
if (_connection == null)
|
||||
throw new IllegalStateException("Cannot wait for accept with no connection");
|
||||
long expiration = _context.clock().now()+maxWaitMs;
|
||||
boolean accepted = _connection.packetSendChoke(maxWaitMs);
|
||||
if (accepted)
|
||||
_acceptedOn = _context.clock().now();
|
||||
else
|
||||
_acceptedOn = -1;
|
||||
_connection = null;
|
||||
}
|
||||
|
||||
public void waitForCompletion(int maxWaitMs) {
|
||||
_connection = null;
|
||||
long expiration = _context.clock().now()+maxWaitMs;
|
||||
while (true) {
|
||||
long timeRemaining = expiration - _context.clock().now();
|
||||
if ( (timeRemaining <= 0) && (maxWaitMs > 0) ) return;
|
||||
try {
|
||||
synchronized (this) {
|
||||
if (_ackOn > 0) return;
|
||||
if (_cancelledOn > 0) return;
|
||||
if (timeRemaining > 60*1000)
|
||||
timeRemaining = 60*1000;
|
||||
else if (timeRemaining <= 0)
|
||||
timeRemaining = 10*1000;
|
||||
wait(timeRemaining);
|
||||
}
|
||||
} catch (InterruptedException ie) {}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean writeAccepted() { return _acceptedOn > 0 && _cancelledOn <= 0; }
|
||||
public boolean writeFailed() { return _cancelledOn > 0; }
|
||||
public boolean writeSuccessful() { return _ackOn > 0 && _cancelledOn <= 0; }
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
import java.util.HashSet;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.client.I2PSession;
|
||||
import net.i2p.client.I2PSessionException;
|
||||
import net.i2p.data.ByteArray;
|
||||
import net.i2p.data.SessionKey;
|
||||
import net.i2p.util.ByteCache;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Queue out packets to be sent through the session.
|
||||
* Well, thats the theory at least... in practice we just
|
||||
* send them immediately with no blocking, since the
|
||||
* mode=bestEffort doesnt block in the SDK.
|
||||
*
|
||||
*/
|
||||
class PacketQueue {
|
||||
private I2PAppContext _context;
|
||||
private Log _log;
|
||||
private I2PSession _session;
|
||||
private ConnectionManager _connectionManager;
|
||||
private byte _buf[];
|
||||
private ByteCache _cache = ByteCache.getInstance(64, 36*1024);
|
||||
|
||||
public PacketQueue(I2PAppContext context, I2PSession session, ConnectionManager mgr) {
|
||||
_context = context;
|
||||
_session = session;
|
||||
_connectionManager = mgr;
|
||||
_buf = _cache.acquire().getData(); // new byte[36*1024];
|
||||
_log = context.logManager().getLog(PacketQueue.class);
|
||||
_context.statManager().createRateStat("stream.con.sendMessageSize", "Size of a message sent on a connection", "Stream", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
|
||||
_context.statManager().createRateStat("stream.con.sendDuplicateSize", "Size of a message resent on a connection", "Stream", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
|
||||
}
|
||||
|
||||
protected void finalize() throws Throwable {
|
||||
_cache.release(new ByteArray(_buf));
|
||||
super.finalize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new packet to be sent out ASAP
|
||||
*/
|
||||
public void enqueue(PacketLocal packet) {
|
||||
packet.prepare();
|
||||
|
||||
SessionKey keyUsed = packet.getKeyUsed();
|
||||
if (keyUsed == null)
|
||||
keyUsed = new SessionKey();
|
||||
Set tagsSent = packet.getTagsSent();
|
||||
if (tagsSent == null)
|
||||
tagsSent = new HashSet();
|
||||
|
||||
// cache this from before sendMessage
|
||||
String conStr = (packet.getConnection() != null ? packet.getConnection().toString() : "");
|
||||
if (packet.getAckTime() > 0) {
|
||||
_log.debug("Not resending " + packet);
|
||||
return;
|
||||
} else {
|
||||
_log.debug("Sending... " + packet);
|
||||
}
|
||||
|
||||
long begin = 0;
|
||||
long end = 0;
|
||||
boolean sent = false;
|
||||
try {
|
||||
int size = 0;
|
||||
synchronized (this) {
|
||||
Arrays.fill(_buf, (byte)0x0);
|
||||
if (packet.shouldSign())
|
||||
size = packet.writeSignedPacket(_buf, 0, _context, _session.getPrivateKey());
|
||||
else
|
||||
size = packet.writePacket(_buf, 0);
|
||||
|
||||
// this should not block!
|
||||
begin = _context.clock().now();
|
||||
sent = _session.sendMessage(packet.getTo(), _buf, 0, size, keyUsed, tagsSent);
|
||||
end = _context.clock().now();
|
||||
}
|
||||
_context.statManager().addRateData("stream.con.sendMessageSize", size, packet.getLifetime());
|
||||
if (packet.getNumSends() > 1)
|
||||
_context.statManager().addRateData("stream.con.sendDuplicateSize", size, packet.getLifetime());
|
||||
|
||||
|
||||
Connection con = packet.getConnection();
|
||||
if (con != null) {
|
||||
con.incrementBytesSent(size);
|
||||
if (packet.getNumSends() > 1)
|
||||
con.incrementDupMessagesSent(1);
|
||||
}
|
||||
} catch (I2PSessionException ise) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Unable to send the packet " + packet, ise);
|
||||
}
|
||||
|
||||
if (!sent) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Send failed for " + packet);
|
||||
Connection c = packet.getConnection();
|
||||
if (c != null) // handle race on b0rk
|
||||
c.disconnect(false);
|
||||
} else {
|
||||
packet.setKeyUsed(keyUsed);
|
||||
packet.setTagsSent(tagsSent);
|
||||
packet.incrementSends();
|
||||
if (_log.shouldLog(Log.DEBUG)) {
|
||||
String msg = "SEND " + packet + (tagsSent.size() > 0
|
||||
? " with " + tagsSent.size() + " tags"
|
||||
: "")
|
||||
+ " send # " + packet.getNumSends()
|
||||
+ " sendTime: " + (end-begin)
|
||||
+ " con: " + conStr;
|
||||
_log.debug(msg);
|
||||
}
|
||||
_connectionManager.getPacketHandler().displayPacket(packet, "SEND");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Examine a connection's state and pick the right scheduler for it.
|
||||
*
|
||||
*/
|
||||
class SchedulerChooser {
|
||||
private I2PAppContext _context;
|
||||
private Log _log;
|
||||
private TaskScheduler _nullScheduler;
|
||||
/** list of TaskScheduler objects */
|
||||
private List _schedulers;
|
||||
|
||||
public SchedulerChooser(I2PAppContext context) {
|
||||
_context = context;
|
||||
_log = context.logManager().getLog(SchedulerChooser.class);
|
||||
_schedulers = createSchedulers();
|
||||
_nullScheduler = new NullScheduler();
|
||||
}
|
||||
|
||||
public TaskScheduler getScheduler(Connection con) {
|
||||
for (int i = 0; i < _schedulers.size(); i++) {
|
||||
TaskScheduler scheduler = (TaskScheduler)_schedulers.get(i);
|
||||
if (scheduler.accept(con)) {
|
||||
//if (_log.shouldLog(Log.DEBUG))
|
||||
// _log.debug("Scheduling for " + con + " with " + scheduler.getClass().getName());
|
||||
return scheduler;
|
||||
}
|
||||
}
|
||||
return _nullScheduler;
|
||||
}
|
||||
|
||||
private List createSchedulers() {
|
||||
List rv = new ArrayList(8);
|
||||
rv.add(new SchedulerPreconnect(_context));
|
||||
rv.add(new SchedulerConnecting(_context));
|
||||
rv.add(new SchedulerReceived(_context));
|
||||
rv.add(new SchedulerConnectedBulk(_context));
|
||||
rv.add(new SchedulerClosing(_context));
|
||||
rv.add(new SchedulerClosed(_context));
|
||||
rv.add(new SchedulerDead(_context));
|
||||
return rv;
|
||||
}
|
||||
private class NullScheduler implements TaskScheduler {
|
||||
private Log _log;
|
||||
public NullScheduler() {
|
||||
_log = _context.logManager().getLog(NullScheduler.class);
|
||||
}
|
||||
|
||||
public void eventOccurred(Connection con) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Event occurred on " + con, new Exception("source"));
|
||||
}
|
||||
public boolean accept(Connection con) { return true; }
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* <p>Scheduler used for after both sides have had their close packets
|
||||
* ACKed, but the final timeout hasn't passed.</p>
|
||||
*
|
||||
* <h2>Entry conditions:</h2><ul>
|
||||
* <li>Both sides have closed and ACKed.</li>
|
||||
* <li>Less than the final timeout period has passed since the last ACK.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Events:</h2><ul>
|
||||
* <li>Packets received</li>
|
||||
* <li>RESET received</li>
|
||||
* <li>Message sending fails (error talking to the session)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Next states:</h2>
|
||||
* <li>{@link SchedulerDead dead} - after the final timeout passes</li>
|
||||
* </ul>
|
||||
*
|
||||
*
|
||||
*/
|
||||
class SchedulerClosed extends SchedulerImpl {
|
||||
private Log _log;
|
||||
public SchedulerClosed(I2PAppContext ctx) {
|
||||
super(ctx);
|
||||
_log = ctx.logManager().getLog(SchedulerClosed.class);
|
||||
}
|
||||
|
||||
public boolean accept(Connection con) {
|
||||
if (con == null) return false;
|
||||
long timeSinceClose = _context.clock().now() - con.getCloseSentOn();
|
||||
boolean ok = (con.getCloseSentOn() > 0) &&
|
||||
(con.getCloseReceivedOn() > 0) &&
|
||||
(con.getUnackedPacketsReceived() <= 0) &&
|
||||
(con.getUnackedPacketsSent() <= 0) &&
|
||||
(!con.getResetReceived()) &&
|
||||
(timeSinceClose < Connection.DISCONNECT_TIMEOUT);
|
||||
boolean conTimeout = (con.getOptions().getConnectTimeout() < con.getLifetime()) &&
|
||||
con.getSendStreamId() == null &&
|
||||
con.getLifetime() < Connection.DISCONNECT_TIMEOUT;
|
||||
return (ok || conTimeout);
|
||||
}
|
||||
|
||||
public void eventOccurred(Connection con) {
|
||||
long timeLeft = con.getCloseSentOn() + Connection.DISCONNECT_TIMEOUT - _context.clock().now();
|
||||
reschedule(timeLeft, con);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* <p>Scheduler used for after both SYNs have been ACKed and both sides
|
||||
* have closed the stream, but either we haven't ACKed their close or
|
||||
* they haven't ACKed ours.</p>
|
||||
*
|
||||
* <h2>Entry conditions:</h2><ul>
|
||||
* <li>Both sides have closed.</li>
|
||||
* <li>At least one direction has not ACKed the close.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Events:</h2><ul>
|
||||
* <li>Packets received (which may or may not ACK the ones sent)</li>
|
||||
* <li>RESET received</li>
|
||||
* <li>Message sending fails (error talking to the session)</li>
|
||||
* <li>Message sending fails (too many resends)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Next states:</h2>
|
||||
* <li>{@link SchedulerClosed closed} - after both sending and receiving ACKs on the CLOSE</li>
|
||||
* <li>{@link SchedulerDead dead} - after sending or receiving a RESET</li>
|
||||
* </ul>
|
||||
*
|
||||
*/
|
||||
class SchedulerClosing extends SchedulerImpl {
|
||||
private Log _log;
|
||||
public SchedulerClosing(I2PAppContext ctx) {
|
||||
super(ctx);
|
||||
_log = ctx.logManager().getLog(SchedulerClosing.class);
|
||||
}
|
||||
|
||||
public boolean accept(Connection con) {
|
||||
boolean ok = (con != null) &&
|
||||
(con.getCloseSentOn() > 0) &&
|
||||
(con.getCloseReceivedOn() > 0) &&
|
||||
( (con.getUnackedPacketsReceived() > 0) || (con.getUnackedPacketsSent() > 0) );
|
||||
return ok;
|
||||
}
|
||||
|
||||
public void eventOccurred(Connection con) {
|
||||
if (con.getNextSendTime() <= 0)
|
||||
con.setNextSendTime(_context.clock().now() + con.getOptions().getSendAckDelay());
|
||||
long remaining = con.getNextSendTime() - _context.clock().now();
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Event occurred w/ remaining: " + remaining + " on " + con);
|
||||
if (remaining <= 0) {
|
||||
if (con.getCloseSentOn() <= 0) {
|
||||
con.sendAvailable();
|
||||
con.setNextSendTime(_context.clock().now() + con.getOptions().getSendAckDelay());
|
||||
} else {
|
||||
con.ackImmediately();
|
||||
}
|
||||
} else {
|
||||
//if (remaining < 5*1000)
|
||||
// remaining = 5*1000;
|
||||
//con.setNextSendTime(when
|
||||
reschedule(remaining, con);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* <p>Scheduler used for after our SYN has been sent and ACKed but one
|
||||
* (or more) sides haven't closed the stream yet. In addition, the
|
||||
* stream must be using the BULK profile, rather than the INTERACTIVE
|
||||
* profile.</p>
|
||||
*
|
||||
* <h2>Entry conditions:</h2><ul>
|
||||
* <li>Packets sent and ACKs received.</li>
|
||||
* <li>At least one direction is not closed</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Events:</h2><ul>
|
||||
* <li>Packets received (which may or may not ACK the ones sent)</li>
|
||||
* <li>Message flush (explicitly, through a full buffer, or stream closure)</li>
|
||||
* <li>RESET received</li>
|
||||
* <li>Message sending fails (error talking to the session)</li>
|
||||
* <li>Message sending fails (too many resends)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Next states:</h2>
|
||||
* <li>{@link SchedulerClosing closing} - after both sending and receiving a CLOSE</li>
|
||||
* <li>{@link SchedulerClosed closed} - after both sending and receiving ACKs on the CLOSE</li>
|
||||
* <li>{@link SchedulerDead dead} - after sending or receiving a RESET</li>
|
||||
* </ul>
|
||||
*
|
||||
*/
|
||||
class SchedulerConnectedBulk extends SchedulerImpl {
|
||||
private Log _log;
|
||||
public SchedulerConnectedBulk(I2PAppContext ctx) {
|
||||
super(ctx);
|
||||
_log = ctx.logManager().getLog(SchedulerConnectedBulk.class);
|
||||
}
|
||||
|
||||
public boolean accept(Connection con) {
|
||||
boolean ok = (con != null) &&
|
||||
(con.getAckedPackets() > 0) &&
|
||||
(con.getOptions().getProfile() == ConnectionOptions.PROFILE_BULK) &&
|
||||
(!con.getResetReceived()) &&
|
||||
( (con.getCloseSentOn() <= 0) || (con.getCloseReceivedOn() <= 0) );
|
||||
if (!ok) {
|
||||
//if (_log.shouldLog(Log.DEBUG))
|
||||
// _log.debug("con: " + con + " closeSentOn: " + con.getCloseSentOn()
|
||||
// + " closeReceivedOn: " + con.getCloseReceivedOn());
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
public void eventOccurred(Connection con) {
|
||||
if (con.getNextSendTime() <= 0)
|
||||
return;
|
||||
|
||||
long timeTillSend = con.getNextSendTime() - _context.clock().now();
|
||||
|
||||
if (timeTillSend <= 0) {
|
||||
con.setNextSendTime(-1);
|
||||
con.sendAvailable();
|
||||
} else {
|
||||
reschedule(timeTillSend, con);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* <p>Scheduler used once we've sent our SYN but it hasn't been ACKed yet.
|
||||
* This connection may or may not be locally created.</p>
|
||||
*
|
||||
* <h2>Entry conditions:</h2><ul>
|
||||
* <li>Packets sent but none ACKed</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Events:</h2><ul>
|
||||
* <li>Packets received (which may or may not ACK the ones sent)</li>
|
||||
* <li>Message flush (explicitly, through a full buffer, or stream closure)</li>
|
||||
* <li>Connection establishment timeout</li>
|
||||
* <li>RESET received</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Next states:</h2>
|
||||
* <li>{@link SchedulerConnectedBulk connected} - after receiving an ACK</li>
|
||||
* <li>{@link SchedulerClosing closing} - after both sending and receiving a CLOSE</li>
|
||||
* <li>{@link SchedulerClosed closed} - after both sending and receiving ACKs on the CLOSE</li>
|
||||
* <li>{@link SchedulerDead dead} - after sending or receiving a RESET</li>
|
||||
* </ul>
|
||||
*
|
||||
*/
|
||||
class SchedulerConnecting extends SchedulerImpl {
|
||||
private Log _log;
|
||||
|
||||
public SchedulerConnecting(I2PAppContext ctx) {
|
||||
super(ctx);
|
||||
_log = ctx.logManager().getLog(SchedulerConnecting.class);
|
||||
}
|
||||
|
||||
public boolean accept(Connection con) {
|
||||
if (con == null) return false;
|
||||
boolean notYetConnected = (con.getIsConnected()) &&
|
||||
(con.getSendStreamId() == null) &&
|
||||
(con.getLastSendId() >= 0) &&
|
||||
(con.getAckedPackets() <= 0) &&
|
||||
(!con.getResetReceived());
|
||||
return notYetConnected;
|
||||
}
|
||||
|
||||
public void eventOccurred(Connection con) {
|
||||
long waited = _context.clock().now() - con.getCreatedOn();
|
||||
if ( (con.getOptions().getConnectTimeout() > 0) &&
|
||||
(con.getOptions().getConnectTimeout() <= waited) ) {
|
||||
con.setConnectionError("Timeout waiting for ack (waited " + waited + "ms)");
|
||||
con.disconnect(false);
|
||||
reschedule(0, con);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("waited too long: " + waited);
|
||||
return;
|
||||
} else {
|
||||
if (con.getOptions().getConnectTimeout() > 0)
|
||||
reschedule(con.getOptions().getConnectTimeout(), con);
|
||||
}
|
||||
/*
|
||||
long timeTillSend = con.getNextSendTime() - _context.clock().now();
|
||||
if ( (timeTillSend <= 0) && (con.getNextSendTime() > 0) ) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("send next on " + con);
|
||||
con.sendAvailable();
|
||||
con.setNextSendTime(-1);
|
||||
} else {
|
||||
if (con.getNextSendTime() > 0) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("time till send: " + timeTillSend + " on " + con);
|
||||
reschedule(timeTillSend, con);
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* <p>Scheduler used for after the final timeout has passed or the
|
||||
* connection was reset.</p>
|
||||
*
|
||||
* <h2>Entry conditions:</h2><ul>
|
||||
* <li>Both sides have closed and ACKed and the timeout has passed. <br />
|
||||
* <b>or</b></li>
|
||||
* <li>A RESET was received</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Events:</h2><ul>
|
||||
* <li>None</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Next states:</h2>
|
||||
* <li>None</li>
|
||||
* </ul>
|
||||
*
|
||||
*
|
||||
*/
|
||||
class SchedulerDead extends SchedulerImpl {
|
||||
private Log _log;
|
||||
public SchedulerDead(I2PAppContext ctx) {
|
||||
super(ctx);
|
||||
_log = ctx.logManager().getLog(SchedulerDead.class);
|
||||
}
|
||||
|
||||
public boolean accept(Connection con) {
|
||||
if (con == null) return false;
|
||||
long timeSinceClose = _context.clock().now() - con.getCloseSentOn();
|
||||
boolean nothingLeftToDo = (con.getCloseSentOn() > 0) &&
|
||||
(con.getCloseReceivedOn() > 0) &&
|
||||
(con.getUnackedPacketsReceived() <= 0) &&
|
||||
(con.getUnackedPacketsSent() <= 0) &&
|
||||
(timeSinceClose >= Connection.DISCONNECT_TIMEOUT);
|
||||
boolean timedOut = (con.getOptions().getConnectTimeout() < con.getLifetime()) &&
|
||||
con.getSendStreamId() == null &&
|
||||
con.getLifetime() >= Connection.DISCONNECT_TIMEOUT;
|
||||
return con.getResetReceived() || nothingLeftToDo || timedOut;
|
||||
}
|
||||
|
||||
public void eventOccurred(Connection con) {
|
||||
con.disconnectComplete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.util.SimpleTimer;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Base scheduler
|
||||
*/
|
||||
abstract class SchedulerImpl implements TaskScheduler {
|
||||
protected I2PAppContext _context;
|
||||
private Log _log;
|
||||
|
||||
public SchedulerImpl(I2PAppContext ctx) {
|
||||
_context = ctx;
|
||||
_log = ctx.logManager().getLog(SchedulerImpl.class);
|
||||
}
|
||||
|
||||
protected void reschedule(long msToWait, Connection con) {
|
||||
SimpleTimer.getInstance().addEvent(new ConEvent(con), msToWait);
|
||||
}
|
||||
|
||||
private class ConEvent implements SimpleTimer.TimedEvent {
|
||||
private Connection _connection;
|
||||
private Exception _addedBy;
|
||||
public ConEvent(Connection con) {
|
||||
_connection = con;
|
||||
//_addedBy = new Exception("added by");
|
||||
}
|
||||
public void timeReached() {
|
||||
//if (_log.shouldLog(Log.DEBUG))
|
||||
// _log.debug("firing event on " + _connection, _addedBy);
|
||||
_connection.eventOccurred();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* <p>Scheduler used for locally created connections where we have not yet
|
||||
* sent the initial SYN packet.</p>
|
||||
*
|
||||
* <h2>Entry conditions:</h2><ul>
|
||||
* <li>Locally created</li>
|
||||
* <li>No packets sent or received</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Events:</h2><ul>
|
||||
* <li>Message flush (explicitly, through a full buffer, or stream closure)</li>
|
||||
* <li>Initial delay timeout (causing implicit flush of any data available)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Next states:</h2>
|
||||
* <li>{@link SchedulerConnecting connecting} - after sending a packet</li>
|
||||
* </ul>
|
||||
*/
|
||||
class SchedulerPreconnect extends SchedulerImpl {
|
||||
private Log _log;
|
||||
|
||||
public SchedulerPreconnect(I2PAppContext ctx) {
|
||||
super(ctx);
|
||||
_log = ctx.logManager().getLog(SchedulerPreconnect.class);
|
||||
}
|
||||
|
||||
public boolean accept(Connection con) {
|
||||
return (con != null) &&
|
||||
(con.getSendStreamId() == null) &&
|
||||
(con.getLastSendId() < 0);
|
||||
}
|
||||
|
||||
public void eventOccurred(Connection con) {
|
||||
if (con.getNextSendTime() < 0)
|
||||
con.setNextSendTime(_context.clock().now() + con.getOptions().getConnectDelay());
|
||||
|
||||
long timeTillSend = con.getNextSendTime() - _context.clock().now();
|
||||
if (timeTillSend <= 0) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Send available for the SYN on " + con);
|
||||
con.sendAvailable();
|
||||
con.setNextSendTime(-1);
|
||||
} else {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Wait " + timeTillSend + " before sending the SYN on " + con);
|
||||
reschedule(timeTillSend, con);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Scheduler used after receiving an inbound connection but before
|
||||
* we have sent our own SYN.
|
||||
*
|
||||
*/
|
||||
class SchedulerReceived extends SchedulerImpl {
|
||||
private Log _log;
|
||||
|
||||
public SchedulerReceived(I2PAppContext ctx) {
|
||||
super(ctx);
|
||||
_log = ctx.logManager().getLog(SchedulerReceived.class);
|
||||
}
|
||||
|
||||
public boolean accept(Connection con) {
|
||||
return (con != null) &&
|
||||
(con.getLastSendId() < 0) &&
|
||||
(con.getSendStreamId() != null);
|
||||
}
|
||||
|
||||
public void eventOccurred(Connection con) {
|
||||
if (con.getUnackedPacketsReceived() <= 0) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("hmm, state is received, but no unacked packets received?");
|
||||
return;
|
||||
}
|
||||
|
||||
long timeTillSend = con.getNextSendTime() - _context.clock().now();
|
||||
if (timeTillSend <= 0) {
|
||||
if (con.getNextSendTime() > 0) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("received con... send a packet");
|
||||
con.sendAvailable();
|
||||
con.setNextSendTime(-1);
|
||||
} else {
|
||||
con.setNextSendTime(_context.clock().now() + con.getOptions().getSendAckDelay());
|
||||
reschedule(con.getOptions().getSendAckDelay(), con);
|
||||
}
|
||||
} else {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("received con... time till next send: " + timeTillSend);
|
||||
reschedule(timeTillSend, con);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
/**
|
||||
* Coordinates what we do 'next'. The scheduler used by a connection is
|
||||
* selected based upon its current state.
|
||||
*
|
||||
*/
|
||||
interface TaskScheduler {
|
||||
/**
|
||||
* An event has occurred (timeout, message sent, or message received),
|
||||
* so schedule what to do next based on our current state.
|
||||
*
|
||||
*/
|
||||
public void eventOccurred(Connection con);
|
||||
|
||||
/**
|
||||
* Determine whether this scheduler is fit to operate against the
|
||||
* given connection
|
||||
*
|
||||
*/
|
||||
public boolean accept(Connection con);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import net.i2p.I2PException;
|
||||
|
||||
/**
|
||||
* We attempted to have more open streams than we are willing to put up with
|
||||
*
|
||||
*/
|
||||
public class TooManyStreamsException extends I2PException {
|
||||
public TooManyStreamsException(String message, Throwable parent) {
|
||||
super(message, parent);
|
||||
}
|
||||
|
||||
public TooManyStreamsException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public TooManyStreamsException() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Properties;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.client.I2PClient;
|
||||
import net.i2p.client.I2PClientFactory;
|
||||
import net.i2p.client.I2PSession;
|
||||
import net.i2p.data.Destination;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class ConnectInactivityTest {
|
||||
private Log _log;
|
||||
private I2PSession _client;
|
||||
private I2PSession _server;
|
||||
public void test() {
|
||||
try {
|
||||
I2PAppContext context = I2PAppContext.getGlobalContext();
|
||||
_log = context.logManager().getLog(ConnectTest.class);
|
||||
_log.debug("creating server session");
|
||||
_server = createSession();
|
||||
_log.debug("running server");
|
||||
runServer(context, _server);
|
||||
_log.debug("creating client session");
|
||||
_client = createSession();
|
||||
_log.debug("running client");
|
||||
runClient(context, _client);
|
||||
} catch (Exception e) {
|
||||
_log.error("error running", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void runClient(I2PAppContext ctx, I2PSession session) {
|
||||
Thread t = new Thread(new ClientRunner(ctx, session));
|
||||
t.setName("client");
|
||||
t.setDaemon(false);
|
||||
t.start();
|
||||
}
|
||||
|
||||
private void runServer(I2PAppContext ctx, I2PSession session) {
|
||||
Thread t = new Thread(new ServerRunner(ctx, session));
|
||||
t.setName("server");
|
||||
t.setDaemon(false);
|
||||
t.start();
|
||||
}
|
||||
|
||||
private class ServerRunner implements Runnable {
|
||||
private I2PAppContext _context;
|
||||
private I2PSession _session;
|
||||
private Log _log;
|
||||
public ServerRunner(I2PAppContext ctx, I2PSession session) {
|
||||
_context = ctx;
|
||||
_session = session;
|
||||
_log = ctx.logManager().getLog(ServerRunner.class);
|
||||
}
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
Properties opts = new Properties();
|
||||
I2PSocketManager mgr = new I2PSocketManagerFull(_context, _session, opts, "client");
|
||||
_log.debug("manager created");
|
||||
I2PServerSocket ssocket = mgr.getServerSocket();
|
||||
_log.debug("server socket created");
|
||||
I2PSocket socket = ssocket.accept();
|
||||
_log.debug("socket accepted: " + socket);
|
||||
try { Thread.sleep(10*60*1000); } catch (InterruptedException ie) {}
|
||||
socket.close();
|
||||
ssocket.close();
|
||||
_session.destroySession();
|
||||
} catch (Exception e) {
|
||||
_log.error("error running", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class ClientRunner implements Runnable {
|
||||
private I2PAppContext _context;
|
||||
private I2PSession _session;
|
||||
private Log _log;
|
||||
public ClientRunner(I2PAppContext ctx, I2PSession session) {
|
||||
_context = ctx;
|
||||
_session = session;
|
||||
_log = ctx.logManager().getLog(ClientRunner.class);
|
||||
}
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
Properties opts = new Properties();
|
||||
I2PSocketManager mgr = new I2PSocketManagerFull(_context, _session, opts, "client");
|
||||
_log.debug("manager created");
|
||||
I2PSocket socket = mgr.connect(_server.getMyDestination());
|
||||
_log.debug("socket created");
|
||||
try { Thread.sleep(10*60*1000); } catch (InterruptedException ie) {}
|
||||
socket.close();
|
||||
_log.debug("socket closed");
|
||||
//_session.destroySession();
|
||||
} catch (Exception e) {
|
||||
_log.error("error running", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private I2PSession createSession() {
|
||||
try {
|
||||
I2PClient client = I2PClientFactory.createClient();
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
|
||||
Destination dest = client.createDestination(baos);
|
||||
Properties p = new Properties();
|
||||
p.setProperty(I2PClient.PROP_TCP_HOST, "localhost");
|
||||
p.setProperty(I2PClient.PROP_TCP_PORT, "10001");
|
||||
I2PSession sess = client.createSession(new ByteArrayInputStream(baos.toByteArray()), p);
|
||||
sess.connect();
|
||||
return sess;
|
||||
} catch (Exception e) {
|
||||
_log.error("error running", e);
|
||||
throw new RuntimeException("b0rk b0rk b0rk");
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String args[]) {
|
||||
ConnectInactivityTest ct = new ConnectInactivityTest();
|
||||
ct.test();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Properties;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.client.I2PClient;
|
||||
import net.i2p.client.I2PClientFactory;
|
||||
import net.i2p.client.I2PSession;
|
||||
import net.i2p.data.Destination;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class ConnectTest {
|
||||
private Log _log;
|
||||
private I2PSession _server;
|
||||
public void test() {
|
||||
try {
|
||||
I2PAppContext context = I2PAppContext.getGlobalContext();
|
||||
_log = context.logManager().getLog(ConnectTest.class);
|
||||
_log.debug("creating server session");
|
||||
_server = createSession();
|
||||
_log.debug("running server");
|
||||
runServer(context, _server);
|
||||
for (int i = 0; i < 5; i++) {
|
||||
_log.debug("running client");
|
||||
runClient(context, createSession());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
_log.error("error running", e);
|
||||
}
|
||||
try { Thread.sleep(10*60*1000); } catch (Exception e) {}
|
||||
}
|
||||
|
||||
private void runClient(I2PAppContext ctx, I2PSession session) {
|
||||
Thread t = new Thread(new ClientRunner(ctx, session));
|
||||
t.setName("client");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
|
||||
private void runServer(I2PAppContext ctx, I2PSession session) {
|
||||
Thread t = new Thread(new ServerRunner(ctx, session));
|
||||
t.setName("server");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
|
||||
private class ServerRunner implements Runnable {
|
||||
private I2PAppContext _context;
|
||||
private I2PSession _session;
|
||||
private Log _log;
|
||||
public ServerRunner(I2PAppContext ctx, I2PSession session) {
|
||||
_context = ctx;
|
||||
_session = session;
|
||||
_log = ctx.logManager().getLog(ServerRunner.class);
|
||||
}
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
Properties opts = new Properties();
|
||||
I2PSocketManager mgr = new I2PSocketManagerFull(_context, _session, opts, "client");
|
||||
_log.debug("manager created");
|
||||
I2PServerSocket ssocket = mgr.getServerSocket();
|
||||
_log.debug("server socket created");
|
||||
while (true) {
|
||||
I2PSocket socket = ssocket.accept();
|
||||
_log.debug("socket accepted: " + socket);
|
||||
try { Thread.sleep(5*1000); } catch (InterruptedException ie) {}
|
||||
socket.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
_log.error("error running", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class ClientRunner implements Runnable {
|
||||
private I2PAppContext _context;
|
||||
private I2PSession _session;
|
||||
private Log _log;
|
||||
public ClientRunner(I2PAppContext ctx, I2PSession session) {
|
||||
_context = ctx;
|
||||
_session = session;
|
||||
_log = ctx.logManager().getLog(ClientRunner.class);
|
||||
}
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
Properties opts = new Properties();
|
||||
I2PSocketManager mgr = new I2PSocketManagerFull(_context, _session, opts, "client");
|
||||
_log.debug("manager created");
|
||||
I2PSocket socket = mgr.connect(_server.getMyDestination());
|
||||
_log.debug("socket created");
|
||||
try { Thread.sleep(5*1000); } catch (InterruptedException ie) {}
|
||||
socket.close();
|
||||
_log.debug("socket closed");
|
||||
mgr.destroySocketManager();
|
||||
mgr = null;
|
||||
socket = null;
|
||||
} catch (Exception e) {
|
||||
_log.error("error running", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private I2PSession createSession() {
|
||||
try {
|
||||
I2PClient client = I2PClientFactory.createClient();
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
|
||||
Destination dest = client.createDestination(baos);
|
||||
I2PSession sess = client.createSession(new ByteArrayInputStream(baos.toByteArray()), System.getProperties());
|
||||
sess.connect();
|
||||
return sess;
|
||||
} catch (Exception e) {
|
||||
_log.error("error running", e);
|
||||
throw new RuntimeException("b0rk b0rk b0rk");
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String args[]) {
|
||||
System.setProperty(I2PClient.PROP_TCP_HOST, "localhost");
|
||||
System.setProperty(I2PClient.PROP_TCP_PORT, "11001");
|
||||
ConnectTest ct = new ConnectTest();
|
||||
ct.test();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Properties;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.client.I2PClient;
|
||||
import net.i2p.client.I2PClientFactory;
|
||||
import net.i2p.client.I2PSession;
|
||||
import net.i2p.data.Destination;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Try to connect to a new nonexistant peer and, of course,
|
||||
* timeout.
|
||||
*/
|
||||
public class ConnectTimeoutTest {
|
||||
private Log _log;
|
||||
private I2PSession _client;
|
||||
private I2PSession _server;
|
||||
private Destination _serverDest;
|
||||
|
||||
public void testNonexistant() {
|
||||
try {
|
||||
I2PAppContext context = I2PAppContext.getGlobalContext();
|
||||
_log = context.logManager().getLog(ConnectTest.class);
|
||||
_log.debug("creating server dest");
|
||||
try {
|
||||
_serverDest = I2PClientFactory.createClient().createDestination(new ByteArrayOutputStream());
|
||||
} catch (Exception e) {}
|
||||
_log.debug("creating client session");
|
||||
_client = createSession();
|
||||
_log.debug("running client");
|
||||
runClient(context, _client);
|
||||
} catch (Exception e) {
|
||||
_log.error("error running", e);
|
||||
}
|
||||
while (true) { synchronized (this) { try { wait(); } catch (Exception e) {} } }
|
||||
}
|
||||
|
||||
private void runClient(I2PAppContext ctx, I2PSession session) {
|
||||
Thread t = new Thread(new ClientRunner(ctx, session));
|
||||
t.setName("client");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
|
||||
private class ClientRunner implements Runnable {
|
||||
private I2PAppContext _context;
|
||||
private I2PSession _session;
|
||||
private Log _log;
|
||||
public ClientRunner(I2PAppContext ctx, I2PSession session) {
|
||||
_context = ctx;
|
||||
_session = session;
|
||||
_log = ctx.logManager().getLog(ClientRunner.class);
|
||||
}
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
I2PSocketManager mgr = I2PSocketManagerFactory.createManager("localhost", 10001, getProps());
|
||||
_log.debug("manager created");
|
||||
_log.debug("options: " + mgr.getDefaultOptions());
|
||||
I2PSocket socket = mgr.connect(_serverDest);
|
||||
_log.debug("socket created");
|
||||
socket.getOutputStream().write("you smell".getBytes());
|
||||
socket.getOutputStream().flush();
|
||||
_log.error("wtf, shouldn't have flushed");
|
||||
socket.close();
|
||||
_log.debug("socket closed");
|
||||
} catch (Exception e) {
|
||||
_log.error("error running (yay!)", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private I2PSession createSession() {
|
||||
try {
|
||||
I2PClient client = I2PClientFactory.createClient();
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
|
||||
Destination dest = client.createDestination(baos);
|
||||
Properties p = getProps();
|
||||
|
||||
I2PSession sess = client.createSession(new ByteArrayInputStream(baos.toByteArray()), p);
|
||||
sess.connect();
|
||||
return sess;
|
||||
} catch (Exception e) {
|
||||
_log.error("error running", e);
|
||||
throw new RuntimeException("b0rk b0rk b0rk");
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String args[]) {
|
||||
ConnectTimeoutTest ct = new ConnectTimeoutTest();
|
||||
ct.testNonexistant();
|
||||
}
|
||||
|
||||
private static Properties getProps() {
|
||||
Properties p = new Properties();
|
||||
p.setProperty(I2PSocketManagerFactory.PROP_MANAGER, I2PSocketManagerFull.class.getName());
|
||||
p.setProperty("tunnels.depthInbound", "0");
|
||||
p.setProperty(I2PClient.PROP_TCP_HOST, "localhost");
|
||||
p.setProperty(I2PClient.PROP_TCP_PORT, "10001");
|
||||
p.setProperty(ConnectionOptions.PROP_CONNECT_TIMEOUT, "30000");
|
||||
//p.setProperty(ConnectionOptions.PROP_CONNECT_DELAY, "10000");
|
||||
p.setProperty(ConnectionOptions.PROP_CONNECT_DELAY, "0");
|
||||
return p;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Date;
|
||||
import java.util.Properties;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.client.I2PClient;
|
||||
import net.i2p.client.I2PClientFactory;
|
||||
import net.i2p.client.I2PSession;
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.data.Destination;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class EchoLargeTest {
|
||||
private Log _log;
|
||||
private I2PSession _client;
|
||||
private I2PSession _server;
|
||||
|
||||
public void test() {
|
||||
try {
|
||||
I2PAppContext context = I2PAppContext.getGlobalContext();
|
||||
_log = context.logManager().getLog(ConnectTest.class);
|
||||
_log.debug("creating server session");
|
||||
_server = createSession();
|
||||
_log.debug("running server");
|
||||
runServer(context, _server);
|
||||
_log.debug("creating client session");
|
||||
_client = createSession();
|
||||
_log.debug("running client");
|
||||
runClient(context, _client);
|
||||
} catch (Exception e) {
|
||||
_log.error("error running", e);
|
||||
}
|
||||
try { Thread.sleep(300*1000); } catch (Exception e) {}
|
||||
}
|
||||
|
||||
private void runClient(I2PAppContext ctx, I2PSession session) {
|
||||
Thread t = new Thread(new ClientRunner(ctx, session));
|
||||
t.setName("client");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
|
||||
private void runServer(I2PAppContext ctx, I2PSession session) {
|
||||
Thread t = new Thread(new ServerRunner(ctx, session));
|
||||
t.setName("server");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
|
||||
private class ServerRunner implements Runnable {
|
||||
private I2PAppContext _context;
|
||||
private I2PSession _session;
|
||||
private Log _log;
|
||||
public ServerRunner(I2PAppContext ctx, I2PSession session) {
|
||||
_context = ctx;
|
||||
_session = session;
|
||||
_log = ctx.logManager().getLog(ServerRunner.class);
|
||||
}
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
Properties opts = new Properties();
|
||||
I2PSocketManager mgr = new I2PSocketManagerFull(_context, _session, opts, "client");
|
||||
_log.debug("manager created");
|
||||
I2PServerSocket ssocket = mgr.getServerSocket();
|
||||
_log.debug("server socket created");
|
||||
while (true) {
|
||||
I2PSocket socket = ssocket.accept();
|
||||
_log.debug("socket accepted: " + socket);
|
||||
InputStream in = socket.getInputStream();
|
||||
OutputStream out = socket.getOutputStream();
|
||||
_log.debug("server streams built");
|
||||
byte buf[] = new byte[128*1024];
|
||||
while (buf != null) {
|
||||
for (int i = 0; i < buf.length; i++) {
|
||||
int c = in.read();
|
||||
if (c == -1) {
|
||||
buf = null;
|
||||
break;
|
||||
} else {
|
||||
buf[i] = (byte)(c & 0xFF);
|
||||
}
|
||||
}
|
||||
if (buf != null) {
|
||||
_log.debug("* server read the full buffer");
|
||||
out.write(buf);
|
||||
out.flush();
|
||||
}
|
||||
}
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Closing the received server socket");
|
||||
socket.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
_log.error("error running", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class ClientRunner implements Runnable {
|
||||
private I2PAppContext _context;
|
||||
private I2PSession _session;
|
||||
private Log _log;
|
||||
public ClientRunner(I2PAppContext ctx, I2PSession session) {
|
||||
_context = ctx;
|
||||
_session = session;
|
||||
_log = ctx.logManager().getLog(ClientRunner.class);
|
||||
}
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
Properties opts = new Properties();
|
||||
I2PSocketManager mgr = new I2PSocketManagerFull(_context, _session, opts, "client");
|
||||
_log.debug("manager created");
|
||||
I2PSocket socket = mgr.connect(_server.getMyDestination());
|
||||
_log.debug("socket created");
|
||||
InputStream in = socket.getInputStream();
|
||||
OutputStream out = socket.getOutputStream();
|
||||
for (int i = 0; i < 3; i++) {
|
||||
byte buf[] = new byte[128*1024];
|
||||
_context.random().nextBytes(buf);
|
||||
byte orig[] = new byte[buf.length];
|
||||
System.arraycopy(buf, 0, orig, 0, buf.length);
|
||||
out.write(buf);
|
||||
_log.debug("client wrote a buffer");
|
||||
out.flush();
|
||||
_log.debug("client flushed");
|
||||
|
||||
byte rbuf[] = new byte[buf.length];
|
||||
for (int j = 0; j < buf.length; j++) {
|
||||
int c = in.read();
|
||||
if (c == -1) {
|
||||
buf = null;
|
||||
break;
|
||||
} else {
|
||||
//_log.debug("client read: " + ((char)c));
|
||||
if (c < 0) c += 256;
|
||||
rbuf[j] = (byte)(c & 0xFF);
|
||||
}
|
||||
}
|
||||
if (buf != null) {
|
||||
_log.debug("* client read a full buffer");
|
||||
int firstOff = -1;
|
||||
for (int k = 0; k < orig.length; k++) {
|
||||
if (orig[k] != rbuf[k]) {
|
||||
firstOff = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (firstOff < 0) {
|
||||
System.out.println("** Read match");
|
||||
} else {
|
||||
System.out.println("** Read does not match: first off = " + firstOff);
|
||||
_log.error("read does not match (first off = " + firstOff + "): \n"
|
||||
+ Base64.encode(orig) + "\n"
|
||||
+ Base64.encode(rbuf));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Closing the client socket");
|
||||
socket.close();
|
||||
_log.debug("socket closed");
|
||||
|
||||
Thread.sleep(5*1000);
|
||||
System.exit(0);
|
||||
} catch (Exception e) {
|
||||
_log.error("error running", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private I2PSession createSession() {
|
||||
try {
|
||||
I2PClient client = I2PClientFactory.createClient();
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
|
||||
Destination dest = client.createDestination(baos);
|
||||
I2PSession sess = client.createSession(new ByteArrayInputStream(baos.toByteArray()), new Properties());
|
||||
sess.connect();
|
||||
return sess;
|
||||
} catch (Exception e) {
|
||||
_log.error("error running", e);
|
||||
throw new RuntimeException("b0rk b0rk b0rk");
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String args[]) {
|
||||
EchoLargeTest et = new EchoLargeTest();
|
||||
et.test();
|
||||
}
|
||||
}
|
||||
179
apps/streaming/java/test/net/i2p/client/streaming/EchoTest.java
Normal file
179
apps/streaming/java/test/net/i2p/client/streaming/EchoTest.java
Normal file
@@ -0,0 +1,179 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Date;
|
||||
import java.util.Properties;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.client.I2PClient;
|
||||
import net.i2p.client.I2PClientFactory;
|
||||
import net.i2p.client.I2PSession;
|
||||
import net.i2p.data.Destination;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class EchoTest {
|
||||
private Log _log;
|
||||
private I2PSession _client;
|
||||
private I2PSession _server;
|
||||
public void test() {
|
||||
try {
|
||||
I2PAppContext context = I2PAppContext.getGlobalContext();
|
||||
_log = context.logManager().getLog(ConnectTest.class);
|
||||
_log.debug("creating server session");
|
||||
_server = createSession();
|
||||
_log.debug("running server");
|
||||
runServer(context, _server);
|
||||
_log.debug("creating client session");
|
||||
_client = createSession();
|
||||
_log.debug("running client");
|
||||
runClient(context, _client);
|
||||
} catch (Exception e) {
|
||||
_log.error("error running", e);
|
||||
}
|
||||
try { Thread.sleep(300*1000); } catch (Exception e) {}
|
||||
}
|
||||
|
||||
private void runClient(I2PAppContext ctx, I2PSession session) {
|
||||
Thread t = new Thread(new ClientRunner(ctx, session));
|
||||
t.setName("client");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
|
||||
private void runServer(I2PAppContext ctx, I2PSession session) {
|
||||
Thread t = new Thread(new ServerRunner(ctx, session));
|
||||
t.setName("server");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
|
||||
private class ServerRunner implements Runnable {
|
||||
private I2PAppContext _context;
|
||||
private I2PSession _session;
|
||||
private Log _log;
|
||||
public ServerRunner(I2PAppContext ctx, I2PSession session) {
|
||||
_context = ctx;
|
||||
_session = session;
|
||||
_log = ctx.logManager().getLog(ServerRunner.class);
|
||||
}
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
Properties opts = new Properties();
|
||||
I2PSocketManager mgr = new I2PSocketManagerFull(_context, _session, opts, "client");
|
||||
_log.debug("manager created");
|
||||
I2PServerSocket ssocket = mgr.getServerSocket();
|
||||
_log.debug("server socket created");
|
||||
while (true) {
|
||||
I2PSocket socket = ssocket.accept();
|
||||
_log.debug("socket accepted: " + socket);
|
||||
InputStream in = socket.getInputStream();
|
||||
OutputStream out = socket.getOutputStream();
|
||||
_log.debug("server streams built");
|
||||
byte buf[] = new byte[5];
|
||||
while (buf != null) {
|
||||
for (int i = 0; i < buf.length; i++) {
|
||||
int c = in.read();
|
||||
if (c == -1) {
|
||||
buf = null;
|
||||
break;
|
||||
} else {
|
||||
buf[i] = (byte)(c & 0xFF);
|
||||
}
|
||||
}
|
||||
if (buf != null) {
|
||||
_log.debug("* server read: " + new String(buf));
|
||||
out.write(buf);
|
||||
out.flush();
|
||||
}
|
||||
}
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Closing the received server socket");
|
||||
socket.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
_log.error("error running", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class ClientRunner implements Runnable {
|
||||
private I2PAppContext _context;
|
||||
private I2PSession _session;
|
||||
private Log _log;
|
||||
public ClientRunner(I2PAppContext ctx, I2PSession session) {
|
||||
_context = ctx;
|
||||
_session = session;
|
||||
_log = ctx.logManager().getLog(ClientRunner.class);
|
||||
}
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
Properties opts = new Properties();
|
||||
I2PSocketManager mgr = new I2PSocketManagerFull(_context, _session, opts, "client");
|
||||
_log.debug("manager created");
|
||||
I2PSocket socket = mgr.connect(_server.getMyDestination());
|
||||
_log.debug("socket created");
|
||||
InputStream in = socket.getInputStream();
|
||||
OutputStream out = socket.getOutputStream();
|
||||
for (int i = 0; i < 3; i++) {
|
||||
out.write("blah!".getBytes());
|
||||
_log.debug("client wrote a line");
|
||||
out.flush();
|
||||
_log.debug("client flushed");
|
||||
byte buf[] = new byte[5];
|
||||
|
||||
for (int j = 0; j < buf.length; j++) {
|
||||
int c = in.read();
|
||||
if (c == -1) {
|
||||
buf = null;
|
||||
break;
|
||||
} else {
|
||||
//_log.debug("client read: " + ((char)c));
|
||||
buf[j] = (byte)(c & 0xFF);
|
||||
}
|
||||
}
|
||||
if (buf != null) {
|
||||
_log.debug("* client read: " + new String(buf));
|
||||
}
|
||||
}
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Closing the client socket");
|
||||
socket.close();
|
||||
_log.debug("socket closed");
|
||||
|
||||
Thread.sleep(5*1000);
|
||||
System.exit(0);
|
||||
} catch (Exception e) {
|
||||
_log.error("error running", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private I2PSession createSession() {
|
||||
try {
|
||||
I2PClient client = I2PClientFactory.createClient();
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
|
||||
Destination dest = client.createDestination(baos);
|
||||
I2PSession sess = client.createSession(new ByteArrayInputStream(baos.toByteArray()), new Properties());
|
||||
sess.connect();
|
||||
return sess;
|
||||
} catch (Exception e) {
|
||||
_log.error("error running", e);
|
||||
throw new RuntimeException("b0rk b0rk b0rk");
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String args[]) {
|
||||
EchoTest et = new EchoTest();
|
||||
et.test();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Stress test the MessageInputStream
|
||||
*/
|
||||
public class MessageInputStreamTest {
|
||||
private I2PAppContext _context;
|
||||
private Log _log;
|
||||
|
||||
public MessageInputStreamTest() {
|
||||
_context = I2PAppContext.getGlobalContext();
|
||||
_log = _context.logManager().getLog(MessageInputStreamTest.class);
|
||||
}
|
||||
|
||||
public void testInOrder() {
|
||||
byte orig[] = new byte[256*1024];
|
||||
_context.random().nextBytes(orig);
|
||||
|
||||
MessageInputStream in = new MessageInputStream(_context);
|
||||
for (int i = 0; i < orig.length / 1024; i++) {
|
||||
byte msg[] = new byte[1024];
|
||||
System.arraycopy(orig, i*1024, msg, 0, 1024);
|
||||
in.messageReceived(i, msg);
|
||||
}
|
||||
|
||||
byte read[] = new byte[orig.length];
|
||||
try {
|
||||
int howMany = DataHelper.read(in, read);
|
||||
if (howMany != orig.length)
|
||||
throw new RuntimeException("Failed test: not enough bytes read [" + howMany + "]");
|
||||
if (!DataHelper.eq(orig, read))
|
||||
throw new RuntimeException("Failed test: data read is not equal");
|
||||
|
||||
_log.info("Passed test: in order");
|
||||
} catch (IOException ioe) {
|
||||
throw new RuntimeException("IOError reading: " + ioe.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void testRandomOrder() {
|
||||
byte orig[] = new byte[256*1024];
|
||||
_context.random().nextBytes(orig);
|
||||
|
||||
MessageInputStream in = new MessageInputStream(_context);
|
||||
ArrayList order = new ArrayList(32);
|
||||
for (int i = 0; i < orig.length / 1024; i++)
|
||||
order.add(new Integer(i));
|
||||
Collections.shuffle(order);
|
||||
for (int i = 0; i < orig.length / 1024; i++) {
|
||||
byte msg[] = new byte[1024];
|
||||
Integer cur = (Integer)order.get(i);
|
||||
System.arraycopy(orig, cur.intValue()*1024, msg, 0, 1024);
|
||||
in.messageReceived(cur.intValue(), msg);
|
||||
_log.debug("Injecting " + cur);
|
||||
}
|
||||
|
||||
byte read[] = new byte[orig.length];
|
||||
try {
|
||||
int howMany = DataHelper.read(in, read);
|
||||
if (howMany != orig.length)
|
||||
throw new RuntimeException("Failed test: not enough bytes read [" + howMany + "]");
|
||||
if (!DataHelper.eq(orig, read))
|
||||
throw new RuntimeException("Failed test: data read is not equal");
|
||||
|
||||
_log.info("Passed test: random order");
|
||||
} catch (IOException ioe) {
|
||||
throw new RuntimeException("IOError reading: " + ioe.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void testRandomDups() {
|
||||
byte orig[] = new byte[256*1024];
|
||||
_context.random().nextBytes(orig);
|
||||
|
||||
MessageInputStream in = new MessageInputStream(_context);
|
||||
for (int n = 0; n < 3; n++) {
|
||||
ArrayList order = new ArrayList(32);
|
||||
for (int i = 0; i < orig.length / 1024; i++)
|
||||
order.add(new Integer(i));
|
||||
Collections.shuffle(order);
|
||||
for (int i = 0; i < orig.length / 1024; i++) {
|
||||
byte msg[] = new byte[1024];
|
||||
Integer cur = (Integer)order.get(i);
|
||||
System.arraycopy(orig, cur.intValue()*1024, msg, 0, 1024);
|
||||
in.messageReceived(cur.intValue(), msg);
|
||||
_log.debug("Injecting " + cur);
|
||||
}
|
||||
}
|
||||
|
||||
byte read[] = new byte[orig.length];
|
||||
try {
|
||||
int howMany = DataHelper.read(in, read);
|
||||
if (howMany != orig.length)
|
||||
throw new RuntimeException("Failed test: not enough bytes read [" + howMany + "]");
|
||||
if (!DataHelper.eq(orig, read))
|
||||
throw new RuntimeException("Failed test: data read is not equal");
|
||||
|
||||
_log.info("Passed test: random dups");
|
||||
} catch (IOException ioe) {
|
||||
throw new RuntimeException("IOError reading: " + ioe.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void testStaggered() {
|
||||
byte orig[] = new byte[256*1024];
|
||||
byte read[] = new byte[orig.length];
|
||||
_context.random().nextBytes(orig);
|
||||
|
||||
MessageInputStream in = new MessageInputStream(_context);
|
||||
ArrayList order = new ArrayList(32);
|
||||
for (int i = 0; i < orig.length / 1024; i++)
|
||||
order.add(new Integer(i));
|
||||
Collections.shuffle(order);
|
||||
|
||||
int offset = 0;
|
||||
for (int i = 0; i < orig.length / 1024; i++) {
|
||||
byte msg[] = new byte[1024];
|
||||
Integer cur = (Integer)order.get(i);
|
||||
System.arraycopy(orig, cur.intValue()*1024, msg, 0, 1024);
|
||||
in.messageReceived(cur.intValue(), msg);
|
||||
_log.debug("Injecting " + cur);
|
||||
|
||||
try {
|
||||
if (in.available() > 0) {
|
||||
int curRead = in.read(read, offset, read.length-offset);
|
||||
_log.debug("read " + curRead);
|
||||
if (curRead == -1)
|
||||
throw new RuntimeException("EOF with offset " + offset);
|
||||
else
|
||||
offset += curRead;
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
throw new RuntimeException("IOE: " + ioe.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (!DataHelper.eq(orig, read))
|
||||
throw new RuntimeException("Failed test: data read is not equal");
|
||||
|
||||
_log.info("Passed test: staggered");
|
||||
}
|
||||
|
||||
public static void main(String args[]) {
|
||||
MessageInputStreamTest t = new MessageInputStreamTest();
|
||||
try {
|
||||
t.testInOrder();
|
||||
t.testRandomOrder();
|
||||
t.testRandomDups();
|
||||
t.testStaggered();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
try { Thread.sleep(10*1000); } catch (InterruptedException ie) {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class MessageOutputStreamTest {
|
||||
private I2PAppContext _context;
|
||||
private Log _log;
|
||||
|
||||
public MessageOutputStreamTest() {
|
||||
_context = I2PAppContext.getGlobalContext();
|
||||
_log = _context.logManager().getLog(MessageOutputStreamTest.class);
|
||||
}
|
||||
|
||||
public void test() {
|
||||
Receiver receiver = new Receiver();
|
||||
MessageOutputStream out = new MessageOutputStream(_context, receiver);
|
||||
byte buf[] = new byte[128*1024];
|
||||
_context.random().nextBytes(buf);
|
||||
try {
|
||||
out.write(buf);
|
||||
out.flush();
|
||||
} catch (IOException ioe) { ioe.printStackTrace(); }
|
||||
byte read[] = receiver.getData();
|
||||
int firstOff = -1;
|
||||
for (int k = 0; k < buf.length; k++) {
|
||||
if (buf[k] != read[k]) {
|
||||
firstOff = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (firstOff < 0) {
|
||||
System.out.println("** Read match");
|
||||
} else {
|
||||
System.out.println("** Read does not match: first off = " + firstOff);
|
||||
_log.error("read does not match (first off = " + firstOff + "): \n"
|
||||
+ Base64.encode(buf) + "\n"
|
||||
+ Base64.encode(read));
|
||||
}
|
||||
}
|
||||
|
||||
private class Receiver implements MessageOutputStream.DataReceiver {
|
||||
private ByteArrayOutputStream _data;
|
||||
public Receiver() {
|
||||
_data = new ByteArrayOutputStream();
|
||||
}
|
||||
public MessageOutputStream.WriteStatus writeData(byte[] buf, int off, int size) {
|
||||
_data.write(buf, off, size);
|
||||
return new DummyWriteStatus();
|
||||
}
|
||||
public byte[] getData() { return _data.toByteArray(); }
|
||||
}
|
||||
|
||||
private static class DummyWriteStatus implements MessageOutputStream.WriteStatus {
|
||||
public void waitForAccept(int maxWaitMs) { return; }
|
||||
public void waitForCompletion(int maxWaitMs) { return; }
|
||||
public boolean writeAccepted() { return true; }
|
||||
public boolean writeFailed() { return false; }
|
||||
public boolean writeSuccessful() { return true; }
|
||||
}
|
||||
|
||||
public static void main(String args[]) {
|
||||
MessageOutputStreamTest t = new MessageOutputStreamTest();
|
||||
t.test();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Properties;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.client.I2PClient;
|
||||
import net.i2p.client.I2PClientFactory;
|
||||
import net.i2p.client.I2PSession;
|
||||
import net.i2p.data.Destination;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class PingTest {
|
||||
public void test() {
|
||||
try {
|
||||
I2PAppContext context = I2PAppContext.getGlobalContext();
|
||||
I2PSession session = createSession();
|
||||
ConnectionManager mgr = new ConnectionManager(context, session, -1);
|
||||
Log log = context.logManager().getLog(PingTest.class);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
log.debug("ping " + i);
|
||||
long before = context.clock().now();
|
||||
boolean ponged = mgr.ping(session.getMyDestination(), 2*1000);
|
||||
long after = context.clock().now();
|
||||
log.debug("ponged? " + ponged + " after " + (after-before) + "ms");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
try { Thread.sleep(30*1000); } catch (Exception e) {}
|
||||
|
||||
}
|
||||
|
||||
private I2PSession createSession() {
|
||||
try {
|
||||
I2PClient client = I2PClientFactory.createClient();
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
|
||||
Destination dest = client.createDestination(baos);
|
||||
I2PSession sess = client.createSession(new ByteArrayInputStream(baos.toByteArray()), new Properties());
|
||||
sess.connect();
|
||||
return sess;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException("b0rk b0rk b0rk");
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String args[]) {
|
||||
PingTest pt = new PingTest();
|
||||
pt.test();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
/**
|
||||
* Usage: StreamSinkTest [(old|new) [#hops [#kb]]]
|
||||
*/
|
||||
public class StreamSinkTest {
|
||||
/* private static String HOST1 = "dev.i2p.net";
|
||||
private static String HOST2 = "dev.i2p.net";
|
||||
private static String PORT1 = "4101";
|
||||
private static String PORT2 = "4501";
|
||||
/*
|
||||
private static String HOST1 = "localhost";
|
||||
private static String HOST2 = "localhost";
|
||||
private static String PORT1 = "7654";
|
||||
private static String PORT2 = "7654";
|
||||
*/
|
||||
private static String HOST1 = "localhost";
|
||||
private static String HOST2 = "localhost";
|
||||
private static String PORT1 = "10001";
|
||||
private static String PORT2 = "11001";
|
||||
/* */
|
||||
|
||||
public static void main(String args[]) {
|
||||
boolean old = false;
|
||||
int hops = 0;
|
||||
int kb = 32*1024;
|
||||
if (args.length > 0) {
|
||||
if ("old".equals(args[0]))
|
||||
old = true;
|
||||
}
|
||||
if (args.length > 1) {
|
||||
try {
|
||||
hops = Integer.parseInt(args[1]);
|
||||
} catch (NumberFormatException nfe) {
|
||||
hops = 0;
|
||||
}
|
||||
}
|
||||
if (args.length > 2) {
|
||||
try {
|
||||
kb = Integer.parseInt(args[2]);
|
||||
} catch (NumberFormatException nfe) {
|
||||
kb = 32*1024;
|
||||
}
|
||||
}
|
||||
|
||||
if (!old)
|
||||
System.setProperty(I2PSocketManagerFactory.PROP_MANAGER, I2PSocketManagerFull.class.getName());
|
||||
System.setProperty("tunnels.depthInbound", ""+hops);
|
||||
|
||||
new Thread(new Runnable() {
|
||||
public void run() {
|
||||
StreamSinkServer.main(new String[] { HOST1, PORT1, "streamSinkTestDir", "streamSinkTestServer.key" });
|
||||
}
|
||||
}, "server").start();
|
||||
|
||||
try { Thread.sleep(60*1000); } catch (Exception e) {}
|
||||
|
||||
//run(256, 1);
|
||||
//run(256, 1000);
|
||||
//run(4*1024, 10);
|
||||
run(kb, 1);
|
||||
//run(1*1024, 1);
|
||||
//run("/home/jrandom/streamSinkTestDir/clientSink36766.dat", 1);
|
||||
//run(512*1024, 1);
|
||||
try { Thread.sleep(10*1000); } catch (InterruptedException e) {}
|
||||
System.out.println("Shutting down");
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
private static void run(final int kb, final int msBetweenWrites) {
|
||||
Thread t = new Thread(new Runnable() {
|
||||
public void run() {
|
||||
StreamSinkClient.main(new String[] { HOST2, PORT2, kb+"", msBetweenWrites+"", "streamSinkTestServer.key" });
|
||||
}
|
||||
});
|
||||
t.start();
|
||||
|
||||
System.out.println("client and server started: size = " + kb + "KB, delay = " + msBetweenWrites);
|
||||
try {
|
||||
t.join();
|
||||
} catch (InterruptedException ie) {}
|
||||
}
|
||||
|
||||
private static void run(final String filename, final int msBetweenWrites) {
|
||||
Thread t = new Thread(new Runnable() {
|
||||
public void run() {
|
||||
StreamSinkSend.main(new String[] { filename, msBetweenWrites+"", "streamSinkTestServer.key" });
|
||||
}
|
||||
});
|
||||
t.start();
|
||||
|
||||
System.out.println("client and server started: file " + filename + ", delay = " + msBetweenWrites);
|
||||
try {
|
||||
t.join();
|
||||
} catch (InterruptedException ie) {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import net.i2p.client.I2PClient;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class StreamSinkTestClient {
|
||||
public static void main(String args[]) {
|
||||
System.setProperty(I2PSocketManagerFactory.PROP_MANAGER, I2PSocketManagerFull.class.getName());
|
||||
//System.setProperty(I2PClient.PROP_TCP_HOST, "dev.i2p.net");
|
||||
//System.setProperty(I2PClient.PROP_TCP_PORT, "4501");
|
||||
System.setProperty("tunnels.depthInbound", "0");
|
||||
|
||||
if (args.length <= 0) {
|
||||
send("/home/jrandom/libjbigi.so");
|
||||
} else {
|
||||
for (int i = 0; i < args.length; i++)
|
||||
send(args[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void send(final String filename) {
|
||||
Thread t = new Thread(new Runnable() {
|
||||
public void run() {
|
||||
StreamSinkSend.main(new String[] { filename, "0", "streamSinkTestLiveServer.key" });
|
||||
}
|
||||
}, "client " + filename);
|
||||
t.start();
|
||||
try { t.join(); } catch (Exception e) {}
|
||||
System.err.println("Done sending");
|
||||
try { Thread.sleep(120*1000); } catch (Exception e) {}
|
||||
//System.exit(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package net.i2p.client.streaming;
|
||||
|
||||
import net.i2p.client.I2PClient;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class StreamSinkTestServer {
|
||||
public static void main(String args[]) {
|
||||
System.setProperty(I2PSocketManagerFactory.PROP_MANAGER, I2PSocketManagerFull.class.getName());
|
||||
//System.setProperty(I2PClient.PROP_TCP_HOST, "dev.i2p.net");
|
||||
//System.setProperty(I2PClient.PROP_TCP_PORT, "4101");
|
||||
System.setProperty("tunnels.depthInbound", "0");
|
||||
|
||||
new Thread(new Runnable() {
|
||||
public void run() {
|
||||
StreamSinkServer.main(new String[] { "streamSinkTestLiveDir", "streamSinkTestLiveServer.key" });
|
||||
}
|
||||
}, "server").start();
|
||||
}
|
||||
}
|
||||
@@ -46,8 +46,8 @@ public class SysTray implements SysTrayMenuListener {
|
||||
_portString = _configFile.getProperty("port", "7657");
|
||||
_showIcon = Boolean.valueOf(_configFile.getProperty("visible", "true")).booleanValue();
|
||||
|
||||
if (!(new File("router.config")).exists())
|
||||
openRouterConsole("http://localhost:" + _portString + "/index.jsp");
|
||||
//if (!(new File("router.config")).exists())
|
||||
// openRouterConsole("http://localhost:" + _portString + "/index.jsp");
|
||||
|
||||
if ( (System.getProperty("os.name").startsWith("Windows")) && (!Boolean.getBoolean("systray.disable")) )
|
||||
_instance = new SysTray();
|
||||
|
||||
@@ -147,4 +147,14 @@ public class UrlLauncher {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void main(String args[]) {
|
||||
UrlLauncher launcher = new UrlLauncher();
|
||||
try {
|
||||
if (args.length > 0)
|
||||
launcher.openUrl(args[0]);
|
||||
else
|
||||
launcher.openUrl("http://localhost:7657/index.jsp");
|
||||
} catch (Exception e) {}
|
||||
}
|
||||
}
|
||||
|
||||
26
build.xml
26
build.xml
@@ -10,7 +10,7 @@
|
||||
<echo message=" distclean: clean up all derived files" />
|
||||
<echo message=" javadoc: generate javadoc for the entire project into ./build/javadoc" />
|
||||
</target>
|
||||
<target name="dist" depends="distclean, pkg, javadoc">
|
||||
<target name="dist" depends="pkg, javadoc">
|
||||
</target>
|
||||
<target name="build" depends="builddep, jar, buildWEB" />
|
||||
<target name="buildclean" depends="distclean, build" />
|
||||
@@ -19,6 +19,7 @@
|
||||
<ant dir="core/java/" target="jar" />
|
||||
<ant dir="router/java/" target="jar" />
|
||||
<ant dir="apps/ministreaming/java/" target="jar" />
|
||||
<ant dir="apps/streaming/java/" target="jar" />
|
||||
<ant dir="apps/i2ptunnel/java/" target="jar" />
|
||||
<ant dir="apps/sam/java/" target="jar" />
|
||||
<ant dir="apps/heartbeat/java/" target="jar" />
|
||||
@@ -46,6 +47,7 @@
|
||||
<copy file="core/java/build/i2p.jar" todir="build/" />
|
||||
<copy file="router/java/build/router.jar" todir="build/" />
|
||||
<copy file="apps/ministreaming/java/build/mstreaming.jar" todir="build/" />
|
||||
<copy file="apps/streaming/java/build/streaming.jar" todir="build/" />
|
||||
<copy file="apps/i2ptunnel/java/build/i2ptunnel.jar" todir="build/" />
|
||||
<copy file="apps/i2ptunnel/java/build/i2ptunnel.war" todir="build/" />
|
||||
<copy file="apps/sam/java/build/sam.jar" todir="build/" />
|
||||
@@ -75,6 +77,7 @@
|
||||
<pathelement location="router/java/src" />
|
||||
<pathelement location="router/java/test" />
|
||||
<pathelement location="apps/ministreaming/java/src" />
|
||||
<pathelement location="apps/streaming/java/src" />
|
||||
<pathelement location="apps/i2ptunnel/java/src" />
|
||||
<pathelement location="apps/systray/java/src" />
|
||||
<pathelement location="apps/routerconsole/java/src" />
|
||||
@@ -93,6 +96,7 @@
|
||||
<ant dir="core/java/" target="distclean" />
|
||||
<ant dir="router/java/" target="distclean" />
|
||||
<ant dir="apps/ministreaming/java/" target="distclean" />
|
||||
<ant dir="apps/streaming/java/" target="distclean" />
|
||||
<ant dir="apps/i2ptunnel/java/" target="distclean" />
|
||||
<ant dir="apps/sam/java/" target="distclean" />
|
||||
<ant dir="apps/heartbeat/java/" target="distclean" />
|
||||
@@ -158,6 +162,7 @@
|
||||
<copy file="build/jbigi.jar" todir="pkg-temp/lib" />
|
||||
<copy file="build/jnet.jar" todir="pkg-temp/lib/" />
|
||||
<copy file="build/mstreaming.jar" todir="pkg-temp/lib/" />
|
||||
<copy file="build/streaming.jar" todir="pkg-temp/lib/" />
|
||||
<copy file="build/netmonitor.jar" todir="pkg-temp/lib/" />
|
||||
<copy file="build/org.mortbay.jetty-jdk1.2.jar" todir="pkg-temp/lib/" />
|
||||
<copy file="build/org.mortbay.jetty.jar" todir="pkg-temp/lib/" />
|
||||
@@ -176,7 +181,6 @@
|
||||
<copy file="installer/resources/clients.config" todir="pkg-temp/" />
|
||||
<copy file="installer/resources/i2prouter" todir="pkg-temp/" />
|
||||
<copy file="installer/resources/i2prouter.bat" todir="pkg-temp/" />
|
||||
<copy file="installer/resources/i2prouter_win9x.bat" todir="pkg-temp/" />
|
||||
<copy file="installer/resources/i2ptunnel.config" todir="pkg-temp/" />
|
||||
<!-- <copy file="installer/resources/install_i2p_service_unix" todir="pkg-temp/" /> -->
|
||||
<copy file="installer/resources/install_i2p_service_winnt.bat" todir="pkg-temp/" />
|
||||
@@ -207,11 +211,16 @@
|
||||
<copy file="history.txt" todir="pkg-temp/" />
|
||||
<mkdir dir="pkg-temp/docs" />
|
||||
<copy file="readme.html" todir="pkg-temp/docs/" />
|
||||
<copy file="installer/resources/startconsole.html" todir="pkg-temp/docs/" />
|
||||
<copy file="installer/resources/start.ico" todir="pkg-temp/docs/" />
|
||||
<copy file="installer/resources/console.ico" todir="pkg-temp/docs/" />
|
||||
<copy file="installer/resources/uninstall.ico" todir="pkg-temp/docs/" />
|
||||
<mkdir dir="pkg-temp/eepsite" />
|
||||
<mkdir dir="pkg-temp/eepsite/webapps" />
|
||||
<mkdir dir="pkg-temp/eepsite/logs" />
|
||||
<mkdir dir="pkg-temp/eepsite/docroot" />
|
||||
<copy file="installer/resources/eepsite_index.html" tofile="pkg-temp/eepsite/docroot/index.html" />
|
||||
<copy file="installer/resources/favicon.ico" tofile="pkg-temp/eepsite/docroot/favicon.ico" />
|
||||
<copy file="installer/resources/jetty.xml" tofile="pkg-temp/eepsite/jetty.xml" />
|
||||
</target>
|
||||
<target name="tarball" depends="preppkg">
|
||||
@@ -219,11 +228,13 @@
|
||||
<tarfileset dir="pkg-temp" includes="**/*" prefix="i2p" />
|
||||
</tar>
|
||||
</target>
|
||||
<target name="updater" depends="build">
|
||||
<target name="updater" depends="distclean, build">
|
||||
<delete dir="pkg-temp" />
|
||||
<copy file="build/i2p.jar" todir="pkg-temp/lib/" />
|
||||
<copy file="build/i2ptunnel.jar" todir="pkg-temp/lib/" />
|
||||
<copy file="build/mstreaming.jar" todir="pkg-temp/lib/" />
|
||||
<copy file="build/streaming.jar" todir="pkg-temp/lib/" />
|
||||
<copy file="build/sam.jar" todir="pkg-temp/lib/" />
|
||||
<copy file="build/router.jar" todir="pkg-temp/lib/" />
|
||||
<copy file="build/routerconsole.jar" todir="pkg-temp/lib/" />
|
||||
<copy file="build/i2ptunnel.war" todir="pkg-temp/webapps/" />
|
||||
@@ -237,6 +248,15 @@
|
||||
</target>
|
||||
<taskdef name="izpack" classpath="${basedir}/installer/lib/izpack/standalone-compiler.jar" classname="com.izforge.izpack.ant.IzPackTask" />
|
||||
<target name="installer" depends="preppkg">
|
||||
<jar destfile="./pkg-temp/lib/copy.jar" basedir="./core/java/build/obj" includes="net/i2p/util/*.class">
|
||||
<manifest><attribute name="Main-Class" value="net.i2p.util.Copy" /></manifest>
|
||||
</jar>
|
||||
<jar destfile="./pkg-temp/lib/delete.jar" basedir="./core/java/build/obj" includes="net/i2p/util/*.class">
|
||||
<manifest><attribute name="Main-Class" value="net.i2p.util.Delete" /></manifest>
|
||||
</jar>
|
||||
<jar destfile="./pkg-temp/lib/exec.jar" basedir="./core/java/build/obj" includes="net/i2p/util/*.class">
|
||||
<manifest><attribute name="Main-Class" value="net.i2p.util.Exec" /></manifest>
|
||||
</jar>
|
||||
<izpack input="${basedir}/installer/install.xml" output="${basedir}/install.jar" installerType="standard" basedir="${basedir}" />
|
||||
</target>
|
||||
</project>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user