diff --git a/apps/sam/java/src/net/i2p/sam/SAMBridge.java b/apps/sam/java/src/net/i2p/sam/SAMBridge.java
new file mode 100644
index 0000000000000000000000000000000000000000..7f0678101c2a9578e20463f5dcae81f9285ff0ff
--- /dev/null
+++ b/apps/sam/java/src/net/i2p/sam/SAMBridge.java
@@ -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) {}
+	}
+    }
+}
diff --git a/apps/sam/java/src/net/i2p/sam/SAMException.java b/apps/sam/java/src/net/i2p/sam/SAMException.java
new file mode 100644
index 0000000000000000000000000000000000000000..e51e35ea4fcf8aad07dd23727022e4c1a606a932
--- /dev/null
+++ b/apps/sam/java/src/net/i2p/sam/SAMException.java
@@ -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);
+    }
+}
diff --git a/apps/sam/java/src/net/i2p/sam/SAMHandler.java b/apps/sam/java/src/net/i2p/sam/SAMHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..d5d4453cc45b03109c637a02e36aaabcb2bd724b
--- /dev/null
+++ b/apps/sam/java/src/net/i2p/sam/SAMHandler.java
@@ -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();
+    }
+}
diff --git a/apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java b/apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..c598a0d501fda2ef606ed62d47d3e3ea86e60e00
--- /dev/null
+++ b/apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java
@@ -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;
+	}
+    }
+}
diff --git a/apps/sam/java/src/net/i2p/sam/SAMRawReceiver.java b/apps/sam/java/src/net/i2p/sam/SAMRawReceiver.java
new file mode 100644
index 0000000000000000000000000000000000000000..95a0e9df7e89ca1d1e19166fbac46d065e530757
--- /dev/null
+++ b/apps/sam/java/src/net/i2p/sam/SAMRawReceiver.java
@@ -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();
+}
diff --git a/apps/sam/java/src/net/i2p/sam/SAMRawSession.java b/apps/sam/java/src/net/i2p/sam/SAMRawSession.java
new file mode 100644
index 0000000000000000000000000000000000000000..1078ba6bb9a096ec913db38f189c1f432d88cd1e
--- /dev/null
+++ b/apps/sam/java/src/net/i2p/sam/SAMRawSession.java
@@ -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();
+	}
+    }
+}
diff --git a/apps/sam/java/src/net/i2p/sam/SAMUtils.java b/apps/sam/java/src/net/i2p/sam/SAMUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..c22d0cec0bd3d924e7fee1a3826335723b9e2b51
--- /dev/null
+++ b/apps/sam/java/src/net/i2p/sam/SAMUtils.java
@@ -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;
+    }
+}
diff --git a/apps/sam/java/src/net/i2p/sam/SAMv1Handler.java b/apps/sam/java/src/net/i2p/sam/SAMv1Handler.java
new file mode 100644
index 0000000000000000000000000000000000000000..bc02b5054e1f1296634fab171e527912119e2c4f
--- /dev/null
+++ b/apps/sam/java/src/net/i2p/sam/SAMv1Handler.java
@@ -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());
+	}
+    }
+}