package net.i2p.sam.client;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.Map;
import javax.net.ssl.SSLSocket;
import gnu.getopt.Getopt;
import net.i2p.I2PAppContext;
import net.i2p.data.Base32;
import net.i2p.data.DataHelper;
import net.i2p.util.I2PAppThread;
import net.i2p.util.I2PSSLSocketFactory;
import net.i2p.util.Log;
import net.i2p.util.VersionComparator;
/**
* Swiss army knife tester.
* Sends a file (datafile) to a peer (b64 dest in peerDestFile).
*
* Usage: SAMStreamSend [options] peerDestFile dataFile
*
* See apps/sam/doc/README-test.txt for info on test setup.
* Sends data in one of 5 modes.
* Optionally uses SSL.
* Configurable SAM client version.
*
*/
public class SAMStreamSend {
private final I2PAppContext _context;
private final Log _log;
private final String _samHost;
private final String _samPort;
private final String _destFile;
private final String _dataFile;
private String _conOptions;
private SAMReader _reader, _reader2;
private boolean _isV3;
private boolean _isV32;
private String _v3ID;
//private boolean _dead;
/** Connection id (Integer) to peer (Flooder) */
private final Map<String, Sender> _remotePeers;
private static I2PSSLSocketFactory _sslSocketFactory;
private static final int STREAM=0, DG=1, V1DG=2, RAW=3, V1RAW=4;
private static final int MASTER=8;
private static final String USAGE = "Usage: SAMStreamSend [-s] [-x] [-m mode] [-v version] [-b samHost] [-p samPort]\n" +
" [-o opt=val] [-u user] [-w password] peerDestFile dataDir\n" +
" modes: stream: 0; datagram: 1; v1datagram: 2; raw: 3; v1raw: 4\n" +
" default is stream\n" +
" -s: use SSL\n" +
" -x: use master session (forces -v 3.3)\n" +
" multiple -o session options are allowed";
public static void main(String args[]) {
Getopt g = new Getopt("SAM", args, "sxhb:m:o:p:u:v:w:");
boolean isSSL = false;
boolean isMaster = false;
int mode = STREAM;
String version = "3.3";
String host = "127.0.0.1";
String port = "7656";
String user = null;
String password = null;
String opts = "inbound.length=0 outbound.length=0";
int c;
while ((c = g.getopt()) != -1) {
switch (c) {
case 's':
isSSL = true;
break;
case 'x':
isMaster = true;
break;
case 'm':
mode = Integer.parseInt(g.getOptarg());
if (mode < 0 || mode > V1RAW) {
System.err.println(USAGE);
return;
}
break;
case 'v':
version = g.getOptarg();
break;
case 'b':
host = g.getOptarg();
break;
case 'o':
opts = opts + ' ' + g.getOptarg();
break;
case 'p':
port = g.getOptarg();
break;
case 'u':
user = g.getOptarg();
break;
case 'w':
password = g.getOptarg();
break;
case 'h':
case '?':
case ':':
default:
System.err.println(USAGE);
return;
} // switch
} // while
int startArgs = g.getOptind();
if (args.length - startArgs != 2) {
System.err.println(USAGE);
return;
}
if (isMaster) {
mode += MASTER;
version = "3.3";
}
if ((user == null && password != null) ||
(user != null && password == null)) {
System.err.println("both user and password or neither");
return;
}
if (user != null && password != null && VersionComparator.comp(version, "3.2") < 0) {
System.err.println("user/password require 3.2");
return;
}
I2PAppContext ctx = I2PAppContext.getGlobalContext();
SAMStreamSend sender = new SAMStreamSend(ctx, host, port,
args[startArgs], args[startArgs + 1]);
sender.startup(version, isSSL, mode, user, password, opts);
}
public SAMStreamSend(I2PAppContext ctx, String samHost, String samPort, String destFile, String dataFile) {
_context = ctx;
_log = ctx.logManager().getLog(SAMStreamSend.class);
//_dead = false;
_samHost = samHost;
_samPort = samPort;
_destFile = destFile;
_dataFile = dataFile;
_conOptions = "";
_remotePeers = new HashMap<String, Sender>();
}
public void startup(String version, boolean isSSL, int mode, String user, String password, String sessionOpts) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Starting up");
try {
Socket sock = connect(isSSL);
SAMEventHandler eventHandler = new SendEventHandler(_context);
_reader = new SAMReader(_context, sock.getInputStream(), eventHandler);
_reader.startReading();
if (_log.shouldLog(Log.DEBUG))
_log.debug("Reader created");
OutputStream out = sock.getOutputStream();
String ourDest = handshake(out, version, true, eventHandler, mode, user, password, sessionOpts);
if (mode >= MASTER)
mode -= MASTER;
if (ourDest == null)
throw new IOException("handshake failed");
if (_log.shouldLog(Log.DEBUG))
_log.debug("Handshake complete. we are " + ourDest);
if (_isV3 && mode == STREAM) {
Socket sock2 = connect(isSSL);
eventHandler = new SendEventHandler(_context);
_reader2 = new SAMReader(_context, sock2.getInputStream(), eventHandler);
_reader2.startReading();
if (_log.shouldLog(Log.DEBUG))
_log.debug("Reader2 created");
out = sock2.getOutputStream();
String ok = handshake(out, version, false, eventHandler, mode, user, password, "");
if (ok == null)
throw new IOException("2nd handshake failed");
if (_log.shouldLog(Log.DEBUG))
_log.debug("Handshake2 complete.");
}
if (mode == DG || mode == RAW)
out = null;
send(out, eventHandler, mode);
} catch (IOException e) {
_log.error("Unable to connect to SAM at " + _samHost + ":" + _samPort, e);
if (_reader != null)
_reader.stopReading();
if (_reader2 != null)
_reader2.stopReading();
}
}
private class SendEventHandler extends SAMEventHandler {
public SendEventHandler(I2PAppContext ctx) { super(ctx); }
@Override
public void streamClosedReceived(String result, String id, String message) {
Sender sender = null;
synchronized (_remotePeers) {
sender = _remotePeers.remove(id);
}
if (sender != null) {
sender.closed();
if (_log.shouldLog(Log.DEBUG))
_log.debug("Connection " + sender.getConnectionId() + " closed to " + sender.getDestination());
} else {
_log.error("not connected to " + id + " but we were just closed?");
}
}
}
private Socket connect(boolean isSSL) throws IOException {
int port = Integer.parseInt(_samPort);
if (!isSSL)
return new Socket(_samHost, port);
synchronized(SAMStreamSink.class) {
if (_sslSocketFactory == null) {
try {
_sslSocketFactory = new I2PSSLSocketFactory(
_context, true, "certificates/sam");
} catch (GeneralSecurityException gse) {
throw new IOException("SSL error", gse);
}
}
}
SSLSocket sock = (SSLSocket) _sslSocketFactory.createSocket(_samHost, port);
I2PSSLSocketFactory.verifyHostname(_context, sock, _samHost);
return sock;
}
/**
* @param isMaster is this the control socket
* @return our b64 dest or null
*/
private String handshake(OutputStream samOut, String version, boolean isMaster,
SAMEventHandler eventHandler, int mode, String user, String password,
String opts) {
synchronized (samOut) {
try {
if (user != null && password != null)
samOut.write(("HELLO VERSION MIN=1.0 MAX=" + version + " USER=\"" + user.replace("\"", "\\\"") +
"\" PASSWORD=\"" + password.replace("\"", "\\\"") + "\"\n").getBytes("UTF-8"));
else
samOut.write(("HELLO VERSION MIN=1.0 MAX=" + version + '\n').getBytes("UTF-8"));
samOut.flush();
if (_log.shouldLog(Log.DEBUG))
_log.debug("Hello sent");
String hisVersion = eventHandler.waitForHelloReply();
if (_log.shouldLog(Log.DEBUG))
_log.debug("Hello reply found: " + hisVersion);
if (hisVersion == null)
throw new IOException("Hello failed");
if (!isMaster)
return "OK";
_isV3 = VersionComparator.comp(hisVersion, "3") >= 0;
if (_isV3) {
_isV32 = VersionComparator.comp(hisVersion, "3.2") >= 0;
byte[] id = new byte[5];
_context.random().nextBytes(id);
_v3ID = Base32.encode(id);
if (_isV32)
_v3ID = "xx€€xx" + _v3ID;
_conOptions = "ID=" + _v3ID;
}
boolean masterMode; // are we using v3.3 master session
String command;
if (mode >= MASTER) {
masterMode = true;
command = "ADD";
mode -= MASTER;
} else {
masterMode = false;
command = "CREATE DESTINATION=TRANSIENT";
}
String style;
if (mode == STREAM)
style = "STREAM";
else if (mode == DG || mode == V1DG)
style = "DATAGRAM";
else // RAW or V1RAW
style = "RAW";
if (masterMode) {
if (mode == V1DG || mode == V1RAW)
throw new IllegalArgumentException("v1 dg/raw incompatible with master session");
String req = "SESSION CREATE DESTINATION=TRANSIENT STYLE=MASTER ID=masterSend " + opts + '\n';
samOut.write(req.getBytes("UTF-8"));
samOut.flush();
if (_log.shouldLog(Log.DEBUG))
_log.debug("SESSION CREATE STYLE=MASTER sent");
boolean ok = eventHandler.waitForSessionCreateReply();
if (!ok)
throw new IOException("SESSION CREATE STYLE=MASTER failed");
if (_log.shouldLog(Log.DEBUG))
_log.debug("SESSION CREATE STYLE=MASTER reply found: " + ok);
// PORT required even if we aren't listening for this test
if (mode != STREAM)
opts += " PORT=9999";
}
String req = "SESSION " + command + " STYLE=" + style + ' ' + _conOptions + ' ' + opts + '\n';
samOut.write(req.getBytes("UTF-8"));
samOut.flush();
if (_log.shouldLog(Log.DEBUG))
_log.debug("SESSION " + command + " sent");
boolean ok;
if (masterMode)
ok = eventHandler.waitForSessionAddReply();
else
ok = eventHandler.waitForSessionCreateReply();
if (!ok)
throw new IOException("SESSION " + command + " failed");
if (_log.shouldLog(Log.DEBUG))
_log.debug("SESSION " + command + " reply found: " + ok);
if (masterMode) {
// do a bunch more
req = "SESSION ADD STYLE=STREAM FROM_PORT=99 ID=stream99\n";
samOut.write(req.getBytes("UTF-8"));
req = "SESSION ADD STYLE=STREAM FROM_PORT=98 ID=stream98\n";
samOut.write(req.getBytes("UTF-8"));
req = "SESSION REMOVE ID=stream99\n";
samOut.write(req.getBytes("UTF-8"));
samOut.flush();
}
req = "NAMING LOOKUP NAME=ME\n";
samOut.write(req.getBytes("UTF-8"));
samOut.flush();
if (_log.shouldLog(Log.DEBUG))
_log.debug("Naming lookup sent");
String destination = eventHandler.waitForNamingReply("ME");
if (_log.shouldLog(Log.DEBUG))
_log.debug("Naming lookup reply found: " + destination);
if (destination == null) {
_log.error("No naming lookup reply found!");
return null;
} else {
_log.info("We are " + destination);
}
return destination;
} catch (IOException e) {
_log.error("Error handshaking", e);
return null;
}
}
}
private void send(OutputStream samOut, SAMEventHandler eventHandler, int mode) throws IOException {
Sender sender = new Sender(samOut, eventHandler, mode);
boolean ok = sender.openConnection();
if (ok) {
I2PAppThread t = new I2PAppThread(sender, "Sender");
t.start();
} else {
throw new IOException("Sender failed to connect");
}
}
private class Sender implements Runnable {
private final String _connectionId;
private String _remoteDestination;
private InputStream _in;
private volatile boolean _closed;
private long _started;
private long _totalSent;
private final OutputStream _samOut;
private final SAMEventHandler _eventHandler;
private final int _mode;
private final DatagramSocket _dgSock;
private final InetSocketAddress _dgSAM;
public Sender(OutputStream samOut, SAMEventHandler eventHandler, int mode) throws IOException {
_samOut = samOut;
_eventHandler = eventHandler;
_mode = mode;
if (mode == DG || mode == RAW) {
// samOut will be null
_dgSock = new DatagramSocket();
_dgSAM = new InetSocketAddress(_samHost, 7655);
} else {
_dgSock = null;
_dgSAM = null;
}
synchronized (_remotePeers) {
if (_v3ID != null)
_connectionId = _v3ID;
else
_connectionId = Integer.toString(_remotePeers.size() + 1);
_remotePeers.put(_connectionId, Sender.this);
}
}
public boolean openConnection() {
FileInputStream fin = null;
try {
fin = new FileInputStream(_destFile);
byte dest[] = new byte[1024];
int read = DataHelper.read(fin, dest);
_remoteDestination = DataHelper.getUTF8(dest, 0, read);
_context.statManager().createRateStat("send." + _connectionId + ".totalSent", "Data size sent", "swarm", new long[] { 30*1000, 60*1000, 5*60*1000 });
_context.statManager().createRateStat("send." + _connectionId + ".started", "When we start", "swarm", new long[] { 5*60*1000 });
_context.statManager().createRateStat("send." + _connectionId + ".lifetime", "How long we talk to a peer", "swarm", new long[] { 5*60*1000 });
if (_mode == STREAM) {
StringBuilder buf = new StringBuilder(1024);
buf.append("STREAM CONNECT ID=").append(_connectionId).append(" DESTINATION=").append(_remoteDestination);
// not supported until 3.2 but 3.0-3.1 will ignore
if (_isV3)
buf.append(" FROM_PORT=1234 TO_PORT=5678");
buf.append('\n');
byte[] msg = DataHelper.getUTF8(buf.toString());
synchronized (_samOut) {
_samOut.write(msg);
_samOut.flush();
}
_log.debug("STREAM CONNECT sent, waiting for STREAM STATUS...");
boolean ok = _eventHandler.waitForStreamStatusReply();
if (!ok)
throw new IOException("STREAM CONNECT failed");
}
_in = new FileInputStream(_dataFile);
return true;
} catch (IOException ioe) {
_log.error("Unable to connect", ioe);
return false;
} finally {
if(fin != null) {
try {
fin.close();
} catch(IOException ioe) {}
}
}
}
public String getConnectionId() { return _connectionId; }
public String getDestination() { return _remoteDestination; }
public void closed() {
if (_closed) return;
_closed = true;
long lifetime = _context.clock().now() - _started;
_context.statManager().addRateData("send." + _connectionId + ".lifetime", lifetime, lifetime);
try { _in.close(); } catch (IOException ioe) {}
}
public void run() {
_started = _context.clock().now();
_context.statManager().addRateData("send." + _connectionId + ".started", 1, 0);
final long toSend = (new File(_dataFile)).length();
byte data[] = new byte[8192];
long lastSend = _context.clock().now();
while (!_closed) {
try {
int read = _in.read(data);
long now = _context.clock().now();
if (read == -1) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("EOF from the data for " + _connectionId + " after " + (now-lastSend));
break;
} else if (read > 0) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Sending " + read + " on " + _connectionId + " after " + (now-lastSend));
lastSend = now;
if (_samOut != null) {
synchronized (_samOut) {
if (!_isV3 || _mode == V1DG || _mode == V1RAW) {
String m;
if (_mode == STREAM) {
m = "STREAM SEND ID=" + _connectionId + " SIZE=" + read + "\n";
} else if (_mode == V1DG) {
m = "DATAGRAM SEND DESTINATION=" + _remoteDestination + " SIZE=" + read + "\n";
} else if (_mode == V1RAW) {
m = "RAW SEND DESTINATION=" + _remoteDestination + " SIZE=" + read + "\n";
} else {
throw new IOException("unsupported mode " + _mode);
}
byte msg[] = DataHelper.getUTF8(m);
_samOut.write(msg);
}
_samOut.write(data, 0, read);
_samOut.flush();
}
} else {
// real datagrams
ByteArrayOutputStream baos = new ByteArrayOutputStream(read + 1024);
baos.write(DataHelper.getUTF8("3.0 "));
baos.write(DataHelper.getUTF8(_v3ID));
baos.write((byte) ' ');
baos.write(DataHelper.getUTF8(_remoteDestination));
if (_isV32) {
// only set TO_PORT to test session setting of FROM_PORT
if (_mode == RAW)
baos.write(DataHelper.getUTF8(" PROTOCOL=123 TO_PORT=5678"));
else
baos.write(DataHelper.getUTF8(" TO_PORT=5678"));
baos.write(DataHelper.getUTF8(" SEND_TAGS=19 TAG_THRESHOLD=13 EXPIRES=33 SEND_LEASESET=true"));
}
baos.write((byte) '\n');
baos.write(data, 0, read);
byte[] pkt = baos.toByteArray();
DatagramPacket p = new DatagramPacket(pkt, pkt.length, _dgSAM);
_dgSock.send(p);
try { Thread.sleep(25); } catch (InterruptedException ie) {}
}
_totalSent += read;
_context.statManager().addRateData("send." + _connectionId + ".totalSent", _totalSent, 0);
}
} catch (IOException ioe) {
_log.error("Error sending", ioe);
break;
}
}
if (_samOut != null) {
if (_isV3) {
try {
_samOut.close();
} catch (IOException ioe) {
_log.info("Error closing", ioe);
}
} else {
try {
byte msg[] = ("STREAM CLOSE ID=" + _connectionId + "\n").getBytes("UTF-8");
synchronized (_samOut) {
_samOut.write(msg);
_samOut.flush();
// we can't close this yet, we will lose data
//_samOut.close();
}
} catch (IOException ioe) {
_log.info("Error closing", ioe);
}
}
} else if (_dgSock != null) {
_dgSock.close();
}
closed();
// stop the reader, since we're only doing this once for testing
// you wouldn't do this in a real application
// closing the master socket too fast will kill the data socket flushing through
try {
Thread.sleep(10000);
} catch (InterruptedException ie) {}
if (_log.shouldLog(Log.DEBUG))
_log.debug("Runner exiting");
if (toSend != _totalSent)
_log.error("Only sent " + _totalSent + " of " + toSend + " bytes");
if (_reader2 != null)
_reader2.stopReading();
_reader.stopReading();
}
}
}