Pure-java implementation of the SAM bridge. It actually supports the
HELLO, DEST and NAMING commands, plus SESSIONs with STYLE=RAW. It works with aum's client-side Python library (modulo a small change in the HELLO command, required to allow protocol versioning, not yet implemented in the client lib). (human)
This commit is contained in:
105
apps/sam/java/src/net/i2p/sam/SAMBridge.java
Normal file
105
apps/sam/java/src/net/i2p/sam/SAMBridge.java
Normal file
@ -0,0 +1,105 @@
|
||||
package net.i2p.sam;
|
||||
/*
|
||||
* free (adj.): unencumbered; not under the control of others
|
||||
* Written by human in 2004 and released into the public domain
|
||||
* with no warranty of any kind, either expressed or implied.
|
||||
* It probably won't make your computer catch on fire, or eat
|
||||
* your children, but it might. Use at your own risk.
|
||||
*
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
|
||||
import net.i2p.util.I2PThread;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* SAM bridge implementation.
|
||||
*
|
||||
* @author human
|
||||
*/
|
||||
public class SAMBridge implements Runnable {
|
||||
|
||||
private final static Log _log = new Log(SAMBridge.class);
|
||||
private ServerSocket serverSocket;
|
||||
|
||||
private boolean acceptConnections = true;
|
||||
|
||||
private final static int SAM_LISTENPORT = 7656;
|
||||
|
||||
/**
|
||||
* Build a new SAM bridge listening on 127.0.0.1.
|
||||
*
|
||||
* @param listenPort The port to listen on
|
||||
*/
|
||||
public SAMBridge(int listenPort) {
|
||||
this((String)null, listenPort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a new SAM bridge.
|
||||
*
|
||||
* @param listenHost The network interface to listen on
|
||||
* @param listenPort The port to listen on
|
||||
*/
|
||||
public SAMBridge(String listenHost, int listenPort) {
|
||||
try {
|
||||
if (listenHost != null) {
|
||||
serverSocket = new ServerSocket(listenPort, 0,
|
||||
InetAddress.getByName(listenHost));
|
||||
_log.debug("SAM bridge listening on "
|
||||
+ listenHost + ":" + listenPort);
|
||||
} else {
|
||||
serverSocket = new ServerSocket(listenPort);
|
||||
_log.debug("SAM bridge listening on 0.0.0.0:" + listenPort);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
_log.error("Error starting SAM bridge on "
|
||||
+ (listenHost == null ? "0.0.0.0" : listenHost)
|
||||
+ ":" + listenPort, e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static void main(String args[]) {
|
||||
SAMBridge bridge = new SAMBridge(SAM_LISTENPORT);
|
||||
I2PThread t = new I2PThread(bridge, "SAMListener");
|
||||
t.start();
|
||||
}
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
while (acceptConnections) {
|
||||
Socket s = serverSocket.accept();
|
||||
_log.debug("New connection from "
|
||||
+ s.getInetAddress().toString() + ":"
|
||||
+ s.getPort());
|
||||
|
||||
try {
|
||||
SAMHandler handler = SAMHandlerFactory.createSAMHandler(s);
|
||||
if (handler == null) {
|
||||
_log.debug("SAM handler has not been instantiated");
|
||||
try {
|
||||
s.close();
|
||||
} catch (IOException e) {}
|
||||
continue;
|
||||
}
|
||||
handler.startHandling();
|
||||
} catch (SAMException e) {
|
||||
_log.error("SAM error: " + e.getMessage());
|
||||
s.close();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
_log.error("Unexpected error while listening for connections", e);
|
||||
} finally {
|
||||
try {
|
||||
_log.debug("Shutting down, closing server socket");
|
||||
serverSocket.close();
|
||||
} catch (IOException e) {}
|
||||
}
|
||||
}
|
||||
}
|
25
apps/sam/java/src/net/i2p/sam/SAMException.java
Normal file
25
apps/sam/java/src/net/i2p/sam/SAMException.java
Normal file
@ -0,0 +1,25 @@
|
||||
package net.i2p.sam;
|
||||
/*
|
||||
* free (adj.): unencumbered; not under the control of others
|
||||
* Written by human in 2004 and released into the public domain
|
||||
* with no warranty of any kind, either expressed or implied.
|
||||
* It probably won't make your computer catch on fire, or eat
|
||||
* your children, but it might. Use at your own risk.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Exception thrown by SAM methods
|
||||
*
|
||||
* @author human
|
||||
*/
|
||||
public class SAMException extends Exception {
|
||||
|
||||
public SAMException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public SAMException(String s) {
|
||||
super(s);
|
||||
}
|
||||
}
|
104
apps/sam/java/src/net/i2p/sam/SAMHandler.java
Normal file
104
apps/sam/java/src/net/i2p/sam/SAMHandler.java
Normal file
@ -0,0 +1,104 @@
|
||||
package net.i2p.sam;
|
||||
/*
|
||||
* free (adj.): unencumbered; not under the control of others
|
||||
* Written by human in 2004 and released into the public domain
|
||||
* with no warranty of any kind, either expressed or implied.
|
||||
* It probably won't make your computer catch on fire, or eat
|
||||
* your children, but it might. Use at your own risk.
|
||||
*
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
|
||||
import net.i2p.util.I2PThread;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Base class for SAM protocol handlers. It implements common
|
||||
* methods, but is not able to actually parse the protocol itself:
|
||||
* this task is delegated to subclasses.
|
||||
*
|
||||
* @author human
|
||||
*/
|
||||
public abstract class SAMHandler implements Runnable {
|
||||
|
||||
private final static Log _log = new Log(SAMHandler.class);
|
||||
|
||||
protected I2PThread thread = null;
|
||||
|
||||
private Object socketWLock = new Object(); // Guards writings on socket
|
||||
private OutputStream socketOS = null; // Stream associated to socket
|
||||
protected Socket socket = null;
|
||||
|
||||
protected int verMajor = 0;
|
||||
protected int verMinor = 0;
|
||||
|
||||
private boolean stopHandler = false;
|
||||
private Object stopLock = new Object();
|
||||
|
||||
/**
|
||||
* Start handling the SAM connection, detaching an handling thread.
|
||||
*
|
||||
*/
|
||||
public void startHandling() {
|
||||
thread = new I2PThread(this, "SAMHandler");
|
||||
thread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually handle the SAM protocol.
|
||||
*
|
||||
*/
|
||||
protected abstract void handle();
|
||||
|
||||
/**
|
||||
* Write a byte array on the handler's socket. This method must
|
||||
* always be used when writing data, unless you really know what
|
||||
* you're doing.
|
||||
*
|
||||
* @param data A byte array to be written
|
||||
*/
|
||||
protected void writeBytes(byte[] data) throws IOException {
|
||||
synchronized (socketWLock) {
|
||||
if (socketOS == null) {
|
||||
socketOS = socket.getOutputStream();
|
||||
}
|
||||
socketOS.write(data);
|
||||
socketOS.flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the SAM handler
|
||||
*
|
||||
*/
|
||||
public void stopHandling() {
|
||||
synchronized (stopLock) {
|
||||
stopHandler = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should the handler be stopped?
|
||||
*
|
||||
* @return True if the handler should be stopped, false otherwise
|
||||
*/
|
||||
protected boolean shouldStop() {
|
||||
synchronized (stopLock) {
|
||||
return stopHandler;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a string describing the handler.
|
||||
*
|
||||
* @return A String describing the handler;
|
||||
*/
|
||||
public abstract String toString();
|
||||
|
||||
public final void run() {
|
||||
handle();
|
||||
}
|
||||
}
|
179
apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java
Normal file
179
apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java
Normal file
@ -0,0 +1,179 @@
|
||||
package net.i2p.sam;
|
||||
/*
|
||||
* free (adj.): unencumbered; not under the control of others
|
||||
* Written by human in 2004 and released into the public domain
|
||||
* with no warranty of any kind, either expressed or implied.
|
||||
* It probably won't make your computer catch on fire, or eat
|
||||
* your children, but it might. Use at your own risk.
|
||||
*
|
||||
*/
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.Socket;
|
||||
import java.util.Properties;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* SAM handler factory class.
|
||||
*/
|
||||
public class SAMHandlerFactory {
|
||||
|
||||
private final static Log _log = new Log(SAMHandlerFactory.class);
|
||||
|
||||
/**
|
||||
* Return the right SAM handler depending on the protocol version
|
||||
* required by the client.
|
||||
*
|
||||
* @param s Socket attached to SAM client
|
||||
*
|
||||
* @return A SAM protocol handler
|
||||
*/
|
||||
public static SAMHandler createSAMHandler(Socket s) throws SAMException {
|
||||
BufferedReader br;
|
||||
StringTokenizer tok;
|
||||
|
||||
try {
|
||||
br = new BufferedReader(new InputStreamReader(s.getInputStream(),
|
||||
"ISO-8859-1"));
|
||||
tok = new StringTokenizer(br.readLine(), " ");
|
||||
} catch (IOException e) {
|
||||
throw new SAMException("Error reading from socket: "
|
||||
+ e.getMessage());
|
||||
} catch (Exception e) {
|
||||
throw new SAMException("Unexpected error: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
|
||||
// Message format: HELLO VERSION MIN=v1 MAX=v2
|
||||
if (tok.countTokens() != 4) {
|
||||
throw new SAMException("Bad format in HELLO message");
|
||||
}
|
||||
if (!tok.nextToken().equals("HELLO")) {
|
||||
throw new SAMException("Bad domain in HELLO message");
|
||||
}
|
||||
{
|
||||
String opcode;
|
||||
if (!(opcode = tok.nextToken()).equals("VERSION")) {
|
||||
throw new SAMException("Unrecognized HELLO message opcode: \""
|
||||
+ opcode + "\"");
|
||||
}
|
||||
}
|
||||
|
||||
Properties props;
|
||||
props = SAMUtils.parseParams(tok);
|
||||
if (props == null) {
|
||||
throw new SAMException("No parameters in HELLO VERSION message");
|
||||
}
|
||||
|
||||
String minVer = props.getProperty("MIN");
|
||||
if (minVer == null) {
|
||||
throw new SAMException("Missing MIN parameter in HELLO VERSION message");
|
||||
}
|
||||
|
||||
String maxVer = props.getProperty("MAX");
|
||||
if (maxVer == null) {
|
||||
throw new SAMException("Missing MAX parameter in HELLO VERSION message");
|
||||
}
|
||||
|
||||
String ver = chooseBestVersion(minVer, maxVer);
|
||||
if (ver == null) {
|
||||
// Let's answer negatively
|
||||
try {
|
||||
OutputStream out = s.getOutputStream();
|
||||
out.write("HELLO REPLY RESULT=NOVERSION\n".getBytes("ISO-8859-1"));
|
||||
return null;
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
_log.error("Caught UnsupportedEncodingException ("
|
||||
+ e.getMessage() + ")");
|
||||
throw new SAMException("Character encoding error: "
|
||||
+ e.getMessage());
|
||||
} catch (IOException e) {
|
||||
throw new SAMException("Error reading from socket: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Let's answer positively
|
||||
try {
|
||||
OutputStream out = s.getOutputStream();
|
||||
out.write(("HELLO REPLY RESULT=OK VERSION="
|
||||
+ ver + "\n").getBytes("ISO-8859-1"));
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
_log.error("Caught UnsupportedEncodingException ("
|
||||
+ e.getMessage() + ")");
|
||||
throw new SAMException("Character encoding error: "
|
||||
+ e.getMessage());
|
||||
} catch (IOException e) {
|
||||
throw new SAMException("Error writing to socket: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
|
||||
// ...and instantiate the right SAM handler
|
||||
int verMajor = getMajor(ver);
|
||||
int verMinor = getMinor(ver);
|
||||
SAMHandler handler;
|
||||
switch (verMajor) {
|
||||
case 1:
|
||||
handler = new SAMv1Handler(s, verMajor, verMinor);
|
||||
break;
|
||||
default:
|
||||
_log.error("BUG! Trying to initialize the wrong SAM version!");
|
||||
throw new SAMException("BUG triggered! (handler instantiation)");
|
||||
}
|
||||
|
||||
return handler;
|
||||
}
|
||||
|
||||
/* Return the best version we can use, or null on failure */
|
||||
private static String chooseBestVersion(String minVer, String maxVer) {
|
||||
int minMajor = getMajor(minVer), minMinor = getMinor(minVer);
|
||||
int maxMajor = getMajor(maxVer), maxMinor = getMinor(maxVer);
|
||||
|
||||
// Consistency checks
|
||||
if ((minMajor == -1) || (minMinor == -1)
|
||||
|| (maxMajor == -1) || (maxMinor == -1)) {
|
||||
return null;
|
||||
}
|
||||
if (minMajor > maxMajor) {
|
||||
return null;
|
||||
} else if ((minMajor == maxMajor) && (minMinor > maxMinor)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((minMajor >= 1) && (minMinor >= 0)) {
|
||||
return "1.0";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/* Get the major protocol version from a string */
|
||||
private static int getMajor(String ver) {
|
||||
try {
|
||||
String major = ver.substring(0, ver.indexOf("."));
|
||||
return Integer.parseInt(major);
|
||||
} catch (NumberFormatException e) {
|
||||
return -1;
|
||||
} catch (ArrayIndexOutOfBoundsException e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Get the minor protocol version from a string */
|
||||
private static int getMinor(String ver) {
|
||||
try {
|
||||
String major = ver.substring(ver.indexOf(".") + 1);
|
||||
return Integer.parseInt(major);
|
||||
} catch (NumberFormatException e) {
|
||||
return -1;
|
||||
} catch (ArrayIndexOutOfBoundsException e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
31
apps/sam/java/src/net/i2p/sam/SAMRawReceiver.java
Normal file
31
apps/sam/java/src/net/i2p/sam/SAMRawReceiver.java
Normal file
@ -0,0 +1,31 @@
|
||||
package net.i2p.sam;
|
||||
/*
|
||||
* free (adj.): unencumbered; not under the control of others
|
||||
* Written by human in 2004 and released into the public domain
|
||||
* with no warranty of any kind, either expressed or implied.
|
||||
* It probably won't make your computer catch on fire, or eat
|
||||
* your children, but it might. Use at your own risk.
|
||||
*
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Interface for sending raw data to a SAM client
|
||||
*/
|
||||
public interface SAMRawReceiver {
|
||||
|
||||
/**
|
||||
* Send a byte array to a SAM client, without informations
|
||||
* regarding the sender.
|
||||
*
|
||||
* @param data Byte array to be written
|
||||
*/
|
||||
public void receiveRawBytes(byte data[]) throws IOException;
|
||||
|
||||
/**
|
||||
* Stop receiving data.
|
||||
*
|
||||
*/
|
||||
public void stopReceiving();
|
||||
}
|
213
apps/sam/java/src/net/i2p/sam/SAMRawSession.java
Normal file
213
apps/sam/java/src/net/i2p/sam/SAMRawSession.java
Normal file
@ -0,0 +1,213 @@
|
||||
package net.i2p.sam;
|
||||
/*
|
||||
* free (adj.): unencumbered; not under the control of others
|
||||
* Written by human in 2004 and released into the public domain
|
||||
* with no warranty of any kind, either expressed or implied.
|
||||
* It probably won't make your computer catch on fire, or eat
|
||||
* your children, but it might. Use at your own risk.
|
||||
*
|
||||
*/
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Properties;
|
||||
|
||||
import net.i2p.client.I2PClient;
|
||||
import net.i2p.client.I2PClientFactory;
|
||||
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.HexDump;
|
||||
import net.i2p.util.I2PThread;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* SAM RAW session class.
|
||||
*
|
||||
* @author human
|
||||
*/
|
||||
public class SAMRawSession {
|
||||
|
||||
private final static Log _log = new Log(SAMRawSession.class);
|
||||
|
||||
private I2PSession session = null;
|
||||
|
||||
private SAMRawReceiver recv = null;
|
||||
|
||||
private SAMRawSessionHandler handler = null;
|
||||
|
||||
/**
|
||||
* Create a new SAM RAW session.
|
||||
*
|
||||
* @param dest Base64-encoded destination (private key)
|
||||
* @param props Properties to setup the I2P session
|
||||
* @param recv Object that will receive incoming data
|
||||
*/
|
||||
public SAMRawSession(String dest, Properties props,
|
||||
SAMRawReceiver recv) throws DataFormatException, I2PSessionException {
|
||||
ByteArrayInputStream bais;
|
||||
|
||||
bais = new ByteArrayInputStream(Base64.decode(dest));
|
||||
|
||||
initSAMRawSession(bais, props, recv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new SAM RAW session.
|
||||
*
|
||||
* @param destStream Input stream containing the destination keys
|
||||
* @param props Properties to setup the I2P session
|
||||
* @param recv Object that will receive incoming data
|
||||
*/
|
||||
public SAMRawSession(InputStream destStream, Properties props,
|
||||
SAMRawReceiver recv) throws I2PSessionException {
|
||||
initSAMRawSession(destStream, props, recv);
|
||||
}
|
||||
|
||||
private void initSAMRawSession(InputStream destStream, Properties props,
|
||||
SAMRawReceiver recv) throws I2PSessionException {
|
||||
this.recv = recv;
|
||||
|
||||
_log.debug("SAM RAW session instantiated");
|
||||
|
||||
handler = new SAMRawSessionHandler(destStream, props);
|
||||
Thread t = new I2PThread(handler, "SAMRawSessionHandler");
|
||||
|
||||
t.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send bytes through a SAM RAW session.
|
||||
*
|
||||
* @param data Bytes to be sent
|
||||
*
|
||||
* @return True if the data was sent, false otherwise
|
||||
*/
|
||||
public boolean sendBytes(String dest, byte[] data) throws DataFormatException {
|
||||
Destination d = new Destination();
|
||||
d.fromBase64(dest);
|
||||
|
||||
try {
|
||||
return session.sendMessage(d, data);
|
||||
} catch (I2PSessionException e) {
|
||||
_log.error("I2PSessionException while sending data", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a SAM RAW session.
|
||||
*
|
||||
*/
|
||||
public void close() {
|
||||
handler.stopRunning();
|
||||
}
|
||||
|
||||
/**
|
||||
* SAM RAW session handler, running in its own thread
|
||||
*
|
||||
* @author human
|
||||
*/
|
||||
public class SAMRawSessionHandler implements Runnable, I2PSessionListener {
|
||||
|
||||
private Object runningLock = new Object();
|
||||
private boolean stillRunning = true;
|
||||
|
||||
/**
|
||||
* Create a new SAM RAW session handler
|
||||
*
|
||||
* @param destStream Input stream containing the destination keys
|
||||
* @param props Properties to setup the I2P session
|
||||
*/
|
||||
public SAMRawSessionHandler(InputStream destStream, Properties props) throws I2PSessionException {
|
||||
_log.debug("Instantiating new SAM RAW session handler");
|
||||
|
||||
I2PClient client = I2PClientFactory.createClient();
|
||||
session = client.createSession(destStream, props);
|
||||
|
||||
_log.debug("Connecting I2P session...");
|
||||
session.connect();
|
||||
_log.debug("I2P session connected");
|
||||
|
||||
session.setSessionListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop a SAM RAW session handling thread
|
||||
*
|
||||
*/
|
||||
public void stopRunning() {
|
||||
synchronized (runningLock) {
|
||||
stillRunning = false;
|
||||
runningLock.notify();
|
||||
}
|
||||
}
|
||||
|
||||
public void run() {
|
||||
|
||||
_log.debug("SAM RAW session handler running");
|
||||
|
||||
synchronized (runningLock) {
|
||||
while (stillRunning) {
|
||||
try {
|
||||
runningLock.wait();
|
||||
} catch (InterruptedException ie) {}
|
||||
}
|
||||
_log.debug("Shutting down SAM RAW session handler");
|
||||
|
||||
recv.stopReceiving();
|
||||
|
||||
try {
|
||||
_log.debug("Destroying I2P session...");
|
||||
session.destroySession();
|
||||
_log.debug("I2P session destroyed");
|
||||
} catch (I2PSessionException e) {
|
||||
_log.error("Error destroying I2P session", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void disconnected(I2PSession session) {
|
||||
_log.debug("I2P session disconnected");
|
||||
stopRunning();
|
||||
}
|
||||
|
||||
public void errorOccurred(I2PSession session, String message,
|
||||
Throwable error) {
|
||||
_log.debug("I2P error: " + message, error);
|
||||
stopRunning();
|
||||
}
|
||||
|
||||
|
||||
public void messageAvailable(I2PSession session, int msgId, long size){
|
||||
_log.debug("I2P message available (id: " + msgId
|
||||
+ "; size: " + size + ")");
|
||||
try {
|
||||
byte msg[] = session.receiveMessage(msgId);
|
||||
if (_log.shouldLog(Log.DEBUG)) {
|
||||
_log.debug("Content of message " + msgId + ":\n"
|
||||
+ HexDump.dump(msg));
|
||||
}
|
||||
|
||||
recv.receiveRawBytes(msg);
|
||||
} catch (IOException e) {
|
||||
_log.error("Error forwarding message to receiver", e);
|
||||
stopRunning();
|
||||
} catch (I2PSessionException e) {
|
||||
_log.error("Error fetching I2P message", e);
|
||||
stopRunning();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void reportAbuse(I2PSession session, int severity) {
|
||||
_log.warn("Abuse reported (severity: " + severity + ")");
|
||||
stopRunning();
|
||||
}
|
||||
}
|
||||
}
|
158
apps/sam/java/src/net/i2p/sam/SAMUtils.java
Normal file
158
apps/sam/java/src/net/i2p/sam/SAMUtils.java
Normal file
@ -0,0 +1,158 @@
|
||||
package net.i2p.sam;
|
||||
/*
|
||||
* free (adj.): unencumbered; not under the control of others
|
||||
* Written by human in 2004 and released into the public domain
|
||||
* with no warranty of any kind, either expressed or implied.
|
||||
* It probably won't make your computer catch on fire, or eat
|
||||
* your children, but it might. Use at your own risk.
|
||||
*
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Enumeration;
|
||||
import java.util.Properties;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
import net.i2p.I2PException;
|
||||
import net.i2p.client.I2PClient;
|
||||
import net.i2p.client.I2PClientFactory;
|
||||
import net.i2p.client.naming.NamingService;
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.data.DataFormatException;
|
||||
import net.i2p.data.Destination;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Miscellaneous utility methods used by SAM protocol handlers.
|
||||
*
|
||||
* @author human
|
||||
*/
|
||||
public class SAMUtils {
|
||||
|
||||
private final static Log _log = new Log(SAMUtils.class);
|
||||
|
||||
/**
|
||||
* Generate a random destination key
|
||||
*
|
||||
* @param priv Stream used to write the private key
|
||||
* @param pub Stream used to write the public key (may be null)
|
||||
*/
|
||||
public static void genRandomKey(OutputStream priv, OutputStream pub) {
|
||||
_log.debug("Generating random keys...");
|
||||
try {
|
||||
I2PClient c = I2PClientFactory.createClient();
|
||||
Destination d = c.createDestination(priv);
|
||||
priv.flush();
|
||||
|
||||
if (pub != null) {
|
||||
d.writeBytes(pub);
|
||||
pub.flush();
|
||||
}
|
||||
} catch (I2PException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a base64-encoded dest is valid
|
||||
*
|
||||
* @param dest The base64-encoded destination to be checked
|
||||
*
|
||||
* @return True if the destination is valid, false otherwise
|
||||
*/
|
||||
public static boolean checkDestination(String dest) {
|
||||
try {
|
||||
Destination d = new Destination();
|
||||
d.fromBase64(dest);
|
||||
|
||||
return true;
|
||||
} catch (DataFormatException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolved the specified hostname.
|
||||
*
|
||||
* @param name Hostname to be resolved
|
||||
* @param pubKey A stream to write the Destination public key (may be null)
|
||||
*
|
||||
* @return the Destination for the specified hostname, or null if not found
|
||||
*/
|
||||
public static Destination lookupHost(String name, OutputStream pubKey) {
|
||||
NamingService ns = NamingService.getInstance();
|
||||
Destination dest = ns.lookup(name);
|
||||
|
||||
if ((pubKey != null) && (dest != null)) {
|
||||
try {
|
||||
dest.writeBytes(pubKey);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
} catch (DataFormatException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return dest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse SAM parameters, and put them into a Propetries object
|
||||
*
|
||||
* @param tok A StringTokenizer pointing to the SAM parameters
|
||||
*
|
||||
* @return A Properties object with the parsed SAM parameters
|
||||
*/
|
||||
public static Properties parseParams(StringTokenizer tok) {
|
||||
int pos, ntoks = tok.countTokens();
|
||||
String token, param, value;
|
||||
Properties props = new Properties();
|
||||
|
||||
for (int i = 0; i < ntoks; ++i) {
|
||||
token = tok.nextToken();
|
||||
|
||||
pos = token.indexOf("=");
|
||||
if (pos == -1) {
|
||||
_log.debug("Error in params format");
|
||||
return null;
|
||||
}
|
||||
param = token.substring(0, pos);
|
||||
value = token.substring(pos + 1);
|
||||
|
||||
props.setProperty(param, value);
|
||||
}
|
||||
|
||||
if (_log.shouldLog(Log.DEBUG)) {
|
||||
_log.debug("Parsed properties: " + dumpProperties(props));
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
/* Dump a Properties object in an human-readable form */
|
||||
private static String dumpProperties(Properties props) {
|
||||
Enumeration enum = props.propertyNames();
|
||||
String msg = "";
|
||||
String key, val;
|
||||
boolean firstIter = true;
|
||||
|
||||
while (enum.hasMoreElements()) {
|
||||
key = (String)enum.nextElement();
|
||||
val = props.getProperty(key);
|
||||
|
||||
if (!firstIter) {
|
||||
msg += ";";
|
||||
} else {
|
||||
firstIter = false;
|
||||
}
|
||||
msg += " \"" + key + "\" -> \"" + val + "\"";
|
||||
}
|
||||
|
||||
return msg;
|
||||
}
|
||||
}
|
420
apps/sam/java/src/net/i2p/sam/SAMv1Handler.java
Normal file
420
apps/sam/java/src/net/i2p/sam/SAMv1Handler.java
Normal file
@ -0,0 +1,420 @@
|
||||
package net.i2p.sam;
|
||||
/*
|
||||
* free (adj.): unencumbered; not under the control of others
|
||||
* Written by human in 2004 and released into the public domain
|
||||
* with no warranty of any kind, either expressed or implied.
|
||||
* It probably won't make your computer catch on fire, or eat
|
||||
* your children, but it might. Use at your own risk.
|
||||
*
|
||||
*/
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.EOFException;
|
||||
import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.Socket;
|
||||
import java.util.Enumeration;
|
||||
import java.util.Properties;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
import net.i2p.client.I2PSessionException;
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.data.DataFormatException;
|
||||
import net.i2p.data.Destination;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Class able to handle a SAM version 1 client connections.
|
||||
*
|
||||
* @author human
|
||||
*/
|
||||
public class SAMv1Handler extends SAMHandler implements SAMRawReceiver {
|
||||
|
||||
private final static Log _log = new Log(SAMv1Handler.class);
|
||||
|
||||
private final static int IN_BUFSIZE = 2048;
|
||||
|
||||
private SAMRawSession rawSession = null;
|
||||
private SAMRawSession datagramSession = null;
|
||||
private SAMRawSession streamSession = null;
|
||||
|
||||
/**
|
||||
* Create a new SAM version 1 handler. This constructor expects
|
||||
* that the SAM HELLO message has been still answered (and
|
||||
* stripped) from the socket input stream.
|
||||
*
|
||||
* @param s Socket attached to a SAM client
|
||||
*/
|
||||
public SAMv1Handler(Socket s, int verMajor, int verMinor) throws SAMException{
|
||||
_log.debug("SAM version 1 handler instantiated");
|
||||
|
||||
this.verMajor = verMajor;
|
||||
this.verMinor = verMinor;
|
||||
|
||||
if ((this.verMajor != 1) || (this.verMinor != 0)) {
|
||||
throw new SAMException("BUG! Wrong protocol version!");
|
||||
}
|
||||
|
||||
this.socket = s;
|
||||
this.verMajor = verMajor;
|
||||
this.verMinor = verMinor;
|
||||
}
|
||||
|
||||
public void handle() {
|
||||
String msg, domain, opcode;
|
||||
boolean canContinue = false;
|
||||
ByteArrayOutputStream buf = new ByteArrayOutputStream(IN_BUFSIZE);
|
||||
StringTokenizer tok;
|
||||
|
||||
this.thread.setName("SAMv1Handler");
|
||||
_log.debug("SAM handling started");
|
||||
|
||||
try {
|
||||
InputStream in = socket.getInputStream();
|
||||
int b = -1;
|
||||
|
||||
while (true) {
|
||||
if (shouldStop()) {
|
||||
_log.debug("Stop request found");
|
||||
break;
|
||||
}
|
||||
|
||||
while ((b = in.read()) != -1) {
|
||||
if (b == '\n') {
|
||||
break;
|
||||
}
|
||||
buf.write(b);
|
||||
}
|
||||
if (b == -1) {
|
||||
_log.debug("Connection closed by client");
|
||||
break;
|
||||
}
|
||||
|
||||
msg = buf.toString("ISO-8859-1");
|
||||
if (_log.shouldLog(Log.DEBUG)) {
|
||||
_log.debug("New message received: " + msg);
|
||||
}
|
||||
buf.reset();
|
||||
|
||||
tok = new StringTokenizer(msg, " ");
|
||||
if (tok.countTokens() < 2) {
|
||||
// This is not a correct message, for sure
|
||||
_log.debug("Error in message format");
|
||||
break;
|
||||
}
|
||||
domain = tok.nextToken();
|
||||
opcode = tok.nextToken();
|
||||
|
||||
_log.debug("Parsing (domain: \"" + domain + "\"; opcode: \""
|
||||
+ opcode + "\")");
|
||||
if (domain.equals("RAW")) {
|
||||
canContinue = execRawMessage(opcode, tok);
|
||||
} else if (domain.equals("SESSION")) {
|
||||
canContinue = execSessionMessage(opcode, tok);
|
||||
} else if (domain.equals("DEST")) {
|
||||
canContinue = execDestMessage(opcode, tok);
|
||||
} else if (domain.equals("NAMING")) {
|
||||
canContinue = execNamingMessage(opcode, tok);
|
||||
} else {
|
||||
_log.debug("Unrecognized message domain: \""
|
||||
+ domain + "\"");
|
||||
break;
|
||||
}
|
||||
|
||||
if (!canContinue) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
_log.error("Caught UnsupportedEncodingException ("
|
||||
+ e.getMessage() + ")");
|
||||
} catch (IOException e) {
|
||||
_log.debug("Caught IOException ("
|
||||
+ e.getMessage() + ")");
|
||||
} catch (Exception e) {
|
||||
_log.error("Unexpected exception", e);
|
||||
} finally {
|
||||
_log.debug("Stopping handler");
|
||||
try {
|
||||
this.socket.close();
|
||||
} catch (IOException e) {
|
||||
_log.error("Error closing socket: " + e.getMessage());
|
||||
}
|
||||
if (rawSession != null) {
|
||||
rawSession.close();
|
||||
}
|
||||
if (datagramSession != null) {
|
||||
datagramSession.close();
|
||||
}
|
||||
if (streamSession != null) {
|
||||
streamSession.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Parse and execute a SESSION message */
|
||||
private boolean execSessionMessage(String opcode, StringTokenizer tok) {
|
||||
Properties props = null;
|
||||
|
||||
if (opcode.equals("CREATE")) {
|
||||
|
||||
if ((rawSession != null) || (datagramSession != null)
|
||||
|| (streamSession != null)) {
|
||||
_log.debug("Trying to create a session, but one still exists");
|
||||
return false;
|
||||
}
|
||||
props = SAMUtils.parseParams(tok);
|
||||
if (props == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String dest = props.getProperty("DESTINATION");
|
||||
if (dest == null) {
|
||||
_log.debug("SESSION DESTINATION parameter not specified");
|
||||
return false;
|
||||
}
|
||||
props.remove("DESTINATION");
|
||||
|
||||
String style = props.getProperty("STYLE");
|
||||
if (style == null) {
|
||||
_log.debug("SESSION STYLE parameter not specified");
|
||||
return false;
|
||||
}
|
||||
props.remove("STYLE");
|
||||
|
||||
try {
|
||||
if (style.equals("RAW")) {
|
||||
try {
|
||||
if (dest.equals("TRANSIENT")) {
|
||||
_log.debug("TRANSIENT destination requested");
|
||||
ByteArrayOutputStream priv = new ByteArrayOutputStream();
|
||||
SAMUtils.genRandomKey(priv, null);
|
||||
|
||||
dest = Base64.encode(priv.toByteArray());
|
||||
}
|
||||
rawSession = new SAMRawSession (dest, props, this);
|
||||
writeBytes(("SESSION STATUS RESULT=OK DESTINATION=" + dest + "\n").getBytes("ISO-8859-1"));
|
||||
} catch (DataFormatException e) {
|
||||
_log.debug("Invalid destination specified");
|
||||
writeBytes(("SESSION STATUS RESULT=INVALID_KEY DESTINATION=" + dest + "\n").getBytes("ISO-8859-1"));
|
||||
return true;
|
||||
} catch (I2PSessionException e) {
|
||||
_log.debug("I2P error when instantiating RAW session", e);
|
||||
writeBytes(("SESSION STATUS RESULT=I2P_ERROR DESTINATION=" + dest + "\n").getBytes("ISO-8859-1"));
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
_log.debug("Unrecognized SESSION STYLE: \"" + style + "\"");
|
||||
return false;
|
||||
}
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
_log.error("Caught UnsupportedEncodingException ("
|
||||
+ e.getMessage() + ")");
|
||||
return false;
|
||||
} catch (IOException e) {
|
||||
_log.error("Caught IOException while parsing SESSION message ("
|
||||
+ e.getMessage() + ")");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
_log.debug("Unrecognized SESSION message opcode: \""
|
||||
+ opcode + "\"");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Parse and execute a DEST message*/
|
||||
private boolean execDestMessage(String opcode, StringTokenizer tok) {
|
||||
|
||||
if (opcode.equals("GENERATE")) {
|
||||
if (tok.countTokens() > 0) {
|
||||
_log.debug("Bad format in DEST GENERATE message");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
ByteArrayOutputStream priv = new ByteArrayOutputStream();
|
||||
ByteArrayOutputStream pub = new ByteArrayOutputStream();
|
||||
|
||||
SAMUtils.genRandomKey(priv, pub);
|
||||
writeBytes(("DEST REPLY"
|
||||
+ " PUB="
|
||||
+ Base64.encode(pub.toByteArray())
|
||||
+ " PRIV="
|
||||
+ Base64.encode(priv.toByteArray())
|
||||
+ "\n").getBytes("ISO-8859-1"));
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
_log.error("Caught UnsupportedEncodingException ("
|
||||
+ e.getMessage() + ")");
|
||||
return false;
|
||||
} catch (IOException e) {
|
||||
_log.debug("IOException while executing DEST message", e);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
_log.debug("Unrecognized DEST message opcode: \"" + opcode + "\"");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Parse and execute a NAMING message */
|
||||
private boolean execNamingMessage(String opcode, StringTokenizer tok) {
|
||||
Properties props = null;
|
||||
|
||||
if (opcode.equals("LOOKUP")) {
|
||||
props = SAMUtils.parseParams(tok);
|
||||
if (props == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String name = props.getProperty("NAME");
|
||||
if (name == null) {
|
||||
_log.debug("Name to resolve not specified");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
ByteArrayOutputStream pubKey = new ByteArrayOutputStream();
|
||||
Destination dest = SAMUtils.lookupHost(name, pubKey);
|
||||
|
||||
if (dest == null) {
|
||||
writeBytes("NAMING REPLY RESULT=KEY_NOT_FOUND\n".getBytes("ISP-8859-1"));
|
||||
return true;
|
||||
}
|
||||
|
||||
writeBytes(("NAMING REPLY RESULT=OK NAME=" + name
|
||||
+ " VALUE=" + Base64.encode(pubKey.toByteArray())
|
||||
+ "\n").getBytes("ISO-8859-1"));
|
||||
return true;
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
_log.error("Caught UnsupportedEncodingException ("
|
||||
+ e.getMessage() + ")");
|
||||
return false;
|
||||
} catch (IOException e) {
|
||||
_log.debug("Caught IOException while parsing NAMING message",
|
||||
e);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
_log.debug("Unrecognized NAMING message opcode: \""
|
||||
+ opcode + "\"");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "SAM v1 handler (client: "
|
||||
+ this.socket.getInetAddress().toString() + ":"
|
||||
+ this.socket.getPort() + ")";
|
||||
}
|
||||
|
||||
/* Parse and execute a RAW message */
|
||||
private boolean execRawMessage(String opcode, StringTokenizer tok) {
|
||||
Properties props = null;
|
||||
|
||||
if (rawSession == null) {
|
||||
_log.debug("RAW message received, but no RAW session exists");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (opcode.equals("SEND")) {
|
||||
props = SAMUtils.parseParams(tok);
|
||||
if (props == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String dest = props.getProperty("DESTINATION");
|
||||
if (dest == null) {
|
||||
_log.debug("Destination not specified in RAW SEND message");
|
||||
return false;
|
||||
}
|
||||
|
||||
int size;
|
||||
{
|
||||
String strsize = props.getProperty("SIZE");
|
||||
if (strsize == null) {
|
||||
_log.debug("Size not specified in RAW SEND message");
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
size = Integer.parseInt(strsize);
|
||||
} catch (NumberFormatException e) {
|
||||
_log.debug("Invalid RAW SEND size specified: " + strsize);
|
||||
return false;
|
||||
}
|
||||
if (!checkSize(size)) {
|
||||
_log.debug("Specified size (" + size
|
||||
+ ") is out of protocol limits");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
DataInputStream in = new DataInputStream(socket.getInputStream());
|
||||
byte[] data = new byte[size];
|
||||
|
||||
in.readFully(data);
|
||||
|
||||
if (!rawSession.sendBytes(dest, data)) {
|
||||
_log.error("RAW SEND failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (EOFException e) {
|
||||
_log.debug("Too few bytes with RAW SEND message (expected: "
|
||||
+ size);
|
||||
return false;
|
||||
} catch (IOException e) {
|
||||
_log.debug("Caught IOException while parsing RAW SEND message",
|
||||
e);
|
||||
return false;
|
||||
} catch (DataFormatException e) {
|
||||
_log.debug("Invalid key specified with RAW SEND message",
|
||||
e);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
_log.debug("Unrecognized RAW message opcode: \""
|
||||
+ opcode + "\"");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Check whether a size is inside the limits allowed by this protocol */
|
||||
private boolean checkSize(int size) {
|
||||
return ((size >= 1) && (size <= 32768));
|
||||
}
|
||||
|
||||
// SAMRawReceiver implementation
|
||||
public void receiveRawBytes(byte data[]) throws IOException {
|
||||
if (rawSession == null) {
|
||||
_log.error("BUG! Trying to write raw bytes, but session is null!");
|
||||
throw new NullPointerException("BUG! RAW session is null!");
|
||||
}
|
||||
|
||||
ByteArrayOutputStream msg = new ByteArrayOutputStream();
|
||||
|
||||
msg.write(("RAW RECEIVED SIZE=" + data.length + "\n").getBytes());
|
||||
msg.write(data);
|
||||
|
||||
writeBytes(msg.toByteArray());
|
||||
}
|
||||
|
||||
public void stopReceiving() {
|
||||
_log.debug("stopReceiving() invoked");
|
||||
try {
|
||||
this.socket.close();
|
||||
} catch (IOException e) {
|
||||
_log.error("Error closing socket: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user