From 05fe4da3f960f10f7ae2435f12d68a3ee2824aa0 Mon Sep 17 00:00:00 2001 From: hankhill19580 <hankhill19580@gmail.com> Date: Sun, 23 Jun 2019 22:56:40 +0000 Subject: [PATCH] SAM for beginngers blog --- .../blog/2019/06/23/sam-library-basics.rst | 605 ++++++++++++++++++ 1 file changed, 605 insertions(+) create mode 100644 i2p2www/blog/2019/06/23/sam-library-basics.rst diff --git a/i2p2www/blog/2019/06/23/sam-library-basics.rst b/i2p2www/blog/2019/06/23/sam-library-basics.rst new file mode 100644 index 000000000..77e3ba532 --- /dev/null +++ b/i2p2www/blog/2019/06/23/sam-library-basics.rst @@ -0,0 +1,605 @@ +============================================================= +{% trans -%}So You Want To Write A SAM Library{%- endtrans %} +============================================================= + +.. meta:: + :author: idk + :date: 2019-06-23 + :excerpt: {% trans %}Beginners guide to writing a SAM library!{% endtrans %} + +*Or, talking to*\ `i2p <https://geti2p.net>`__\ *for people who aren't really used to reading specs* + +{% trans -%} +One of the best features of I2P, in my opinion, is it's SAM API, which can be +used to build a bridge between I2P and your application or language of choice. +Currently, dozens of SAM libraries exist for a variety of languages, including: +{%- endtrans %} + +- `i2psam, for c++ <https://github.com/i2p/i2psam>`__ +- `libsam3, for C <https://github.com/i2p/libsam3>`__ +- `txi2p for Python <https://github.com/str4d/txi2p>`__ +- `i2plib for Python <https://github.com/l-n-s/i2plib>`__ +- `i2p.socket for Python <https://github.com/majestrate/i2p.socket>`__ +- `leaflet for Python <https://github.com/MuxZeroNet/leaflet>`__ +- `gosam, for Go <https://github.com/eyedeekay/gosam>`__ +- `sam3 for Go <https://github.com/eyedeekay/sam3>`__ +- `node-i2p for nodejs <https://github.com/redhog/node-i2p>`__ +- `haskell-network-anonymous-i2p <https://github.com/solatis/haskell-network-anonymous-i2p>`__ +- `i2pdotnet for .Net languages <https://github.com/SamuelFisher/i2pdotnet>`__ +- `rust-i2p <https://github.com/stallmanifold/rust-i2p>`__ +- `and i2p.rb for ruby <https://github.com/dryruby/i2p.rb>`__ + +{% trans -%} +If you're using any of these languages, you may be able to port your application +to I2P already, using an existing library. That's not what this tutorial is +about, though. This tutorial is about what to do if you want to create a SAM +library in a new language. In this tutorial, I will implement a new SAM library +in Java. I chose Java because there isn't a Java library that connects you to +SAM yet, because of Java's use in Android, and because it's a language almost +everybody has at least a *little* experience with, so hopefully you can +translate it into a language of your choice. +{%- endtrans %} + +{% trans -%}Creating your library{%- endtrans %} +------------------------------------------------ + +{% trans -%} +How you set up your own library will vary depending on the language you wish +to use. For this example library, we'll be using java so we can create a library +like this: +{%- endtrans %} + +.. code:: sh + + mkdir jsam + cd jsam + gradle init --type java-library + +{% trans -%} +Or, if you are using gradle 5 or greater: +{%- endtrans %} + +.. code:: sh + + gradle init --type java-library --project-name jsam + +{% trans -%}Setting up the Library{%- endtrans %} +------------------------------------------------- + +{% trans -%} +There are a few pieces of data that almost any SAM library should probably +manage. It will at least need to store the address of the SAM Bridge you intend +to use and the signature type you wish to use. +{%- endtrans %} + +{% trans -%}Storing the SAM address{%- endtrans %} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +{% trans -%} +I prefer to store the SAM address as a String and an Integer, and re-combine +them in a function at runtime. +{%- endtrans %} + +.. code:: java + + public String SAMHost = "127.0.0.1"; + public int SAMPort = 7656; + public String SAMAddress(){ + return SAMHost + ":" + SAMPort; + } + +{% trans -%}Storing the Signature Type{%- endtrans %} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +{% trans -%} +The valid signature types for an I2P Tunnel are DSA_SHA1, ECDSA_SHA256_P256, +ECDSA_SHA384_P384, ECDSA_SHA512_P521, EdDSA_SHA512_Ed25519, but it is +strongly recommended that you use EdDSA_SHA512_Ed25519 as a default if you +implement at least SAM 3.1. In java, the 'enum' datastructure lends itself to +this task, as it is intended to contain a group of constants. Add the enum, and +an instance of the enum, to your java class definition. +{%- endtrans %} + +.. code:: java + + enum SIGNATURE_TYPE { + DSA_SHA1, + ECDSA_SHA256_P256, + ECDSA_SHA384_P384, + ECDSA_SHA512_P521, + EdDSA_SHA512_Ed25519; + } + public SIGNATURE_TYPE SigType = SIGNATURE_TYPE.EdDSA_SHA512_Ed25519; + +{% trans -%}Retrieving the signature type:{%- endtrans %} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +{% trans -%} +That takes care of reliably storing the signature type in use by the SAM +connection, but you've still got to retrieve it as a string to communicate it +to the bridge. +{%- endtrans %} + +.. code:: java + + public String SignatureType() { + switch (SigType) { + case DSA_SHA1: + return "SIGNATURE_TYPE=DSA_SHA1"; + case ECDSA_SHA256_P256: + return "SIGNATURE_TYPE=ECDSA_SHA256_P256"; + case ECDSA_SHA384_P384: + return "SIGNATURE_TYPE=ECDSA_SHA384_P384"; + case ECDSA_SHA512_P521: + return "SIGNATURE_TYPE=ECDSA_SHA512_P521"; + case EdDSA_SHA512_Ed25519: + return "SIGNATURE_TYPE=EdDSA_SHA512_Ed25519"; + } + return ""; + } + +{% trans -%} +It's important to test things, so let's write some tests: +{%- endtrans %} + +.. code:: java + + @Test public void testValidDefaultSAMAddress() { + Jsam classUnderTest = new Jsam(); + assertEquals("127.0.0.1:7656", classUnderTest.SAMAddress()); + } + @Test public void testValidDefaultSignatureType() { + Jsam classUnderTest = new Jsam(); + assertEquals("EdDSA_SHA512_Ed25519", classUnderTest.SignatureType()); + } + +{% trans -%} +Once that's done, begin creating your constructor. Note that we've given our +library defaults which will be useful in default situations on all existing I2P +routers so far. +{%- endtrans %} + +.. code:: java + + public Jsam(String host, int port, SIGNATURE_TYPE sig) { + SAMHost = host; + SAMPort = port; + SigType = sig; + } + +{% trans -%}Establishing a SAM Connection{%- endtrans %} +-------------------------------------------------------- + +{% trans -%} +Finally, the good part. Interaction with the SAM bridge is done by sending a +"command" to the address of the SAM bridge, and you can parse the result of the +command as a set of string-based key-value pairs. So bearing that in mind, let's +estabish a read-write connection to the SAM Address we defined before, then +write a "CommandSAM" Function and a reply parser. +{%- endtrans %} + +{% trans -%}Connecting to the SAM Port{%- endtrans %} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +{% trans -%} +We're communicating with SAM via a Socket, so in order to connect to, read from, +and write to the socket, you'll need to create the following private variables +in the Jsam class: +{%- endtrans %} + +.. code:: java + + private Socket socket; + private PrintWriter writer; + private BufferedReader reader; + +{% trans -%} +You will also want to instantiate those variables in your Constructors by +creating a function to do so. +{%- endtrans %} + +.. code:: java + + public Jsam(String host, int port, SIGNATURE_TYPE sig) { + SAMHost = host; + SAMPort = port; + SigType = sig; + startConnection(); + } + public void startConnection() { + try { + socket = new Socket(SAMHost, SAMPort); + writer = new PrintWriter(socket.getOutputStream(), true); + reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); + } catch (Exception e) { + //omitted for brevity + } + } + +{% trans -%}Sending a Command to SAM{%- endtrans %} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +{% trans -%} +Now you're all set up to finally start talking to SAM. In order to keep things +nicely organized, let's create a function which sends a single command to SAM, +terminated by a newline, and which returns a Reply object, which we will create +in the next step: +{%- endtrans %} + +.. code:: java + + public Reply CommandSAM(String args) { + writer.println(args + "\n"); + try { + String repl = reader.readLine(); + return new Reply(repl); + } catch (Exception e) { + //omitted for brevity + } + } + +{% trans -%} +Note that we are using the writer and reader we created from the socket in the +previous step as our inputs and outputs to the socket. When we get a reply from +the reader, we pass the string to the Reply constructor, which parses it and +returns the Reply object. +{%- endtrans %} + +.. _parsing-a-reply-and-creating-a-reply-object: + +{% trans -%}Parsing a reply and creating a Reply object.{%- endtrans %} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +{% trans -%} +In order to more easily handle replies, we'll use a Reply object to +automatically parse the results we get from the SAM bridge. A reply has at least +a topic, a type, and a result, as well as an arbitrary number of key-value +pairs. +{%- endtrans %} + +.. code:: java + + public class Reply { + String topic; + String type; + REPLY_TYPES result; + Map<String, String> replyMap = new HashMap<String, String>(); + +{% trans -%} +As you can see, we will be storing the "result" as an enum, REPLY_TYPES. This +enum contains all the possible reply results which the SAM bridge might respond +with. +{%- endtrans %} + +.. code:: java + + enum REPLY_TYPES { + OK, + CANT_REACH_PEER, + DUPLICATED_ID, + DUPLICATED_DEST, + I2P_ERROR, + INVALID_KEY, + KEY_NOT_FOUND, + PEER_NOT_FOUND, + TIMEOUT; + public static REPLY_TYPES set(String type) { + String temp = type.trim(); + switch (temp) { + case "RESULT=OK": + return OK; + case "RESULT=CANT_REACH_PEER": + return CANT_REACH_PEER; + case "RESULT=DUPLICATED_ID": + return DUPLICATED_ID; + case "RESULT=DUPLICATED_DEST": + return DUPLICATED_DEST; + case "RESULT=I2P_ERROR": + return I2P_ERROR; + case "RESULT=INVALID_KEY": + return INVALID_KEY; + case "RESULT=KEY_NOT_FOUND": + return KEY_NOT_FOUND; + case "RESULT=PEER_NOT_FOUND": + return PEER_NOT_FOUND; + case "RESULT=TIMEOUT": + return TIMEOUT; + } + return I2P_ERROR; + } + public static String get(REPLY_TYPES type) { + switch (type) { + case OK: + return "RESULT=OK"; + case CANT_REACH_PEER: + return "RESULT=CANT_REACH_PEER"; + case DUPLICATED_ID: + return "RESULT=DUPLICATED_ID"; + case DUPLICATED_DEST: + return "RESULT=DUPLICATED_DEST"; + case I2P_ERROR: + return "RESULT=I2P_ERROR"; + case INVALID_KEY: + return "RESULT=INVALID_KEY"; + case KEY_NOT_FOUND: + return "RESULT=KEY_NOT_FOUND"; + case PEER_NOT_FOUND: + return "RESULT=PEER_NOT_FOUND"; + case TIMEOUT: + return "RESULT=TIMEOUT"; + } + return "RESULT=I2P_ERROR"; + } + }; + +{% trans -%} +Now let's create our constructor, which takes the reply string recieved from the +socket as a parameter, parses it, and uses the information to set up the reply +object. The reply is space-delimited, with key-value pairs joined by an equal +sign and terminated by a newline. +{%- endtrans %} + +.. code:: java + + public Reply(String reply) { + String trimmed = reply.trim(); + String[] replyvalues = reply.split(" "); + if (replyvalues.length < 2) { + //omitted for brevity + } + topic = replyvalues[0]; + type = replyvalues[1]; + result = REPLY_TYPES.set(replyvalues[2]); + + String[] replyLast = Arrays.copyOfRange(replyvalues, 2, replyvalues.length); + for (int x = 0; x < replyLast.length; x++) { + String[] kv = replyLast[x].split("=", 2); + if (kv.length != 2) { + + } + replyMap.put(kv[0], kv[1]); + } + } + +{% trans -%} +Lastly, for the sake of convenience, let's give the reply object a toString() +function which returns a string representation of the Reply object. +{%- endtrans %} + +.. code:: java + + public String toString() { + return topic + " " + type + " " + REPLY_TYPES.get(result) + " " + replyMap.toString(); + } + } + +{% trans -%}Saying "HELLO" to SAM{%- endtrans %} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +{% trans -%} +Now we're ready to establish communication with SAM by sending a "Hello" +message. If you're writing a new SAM library, you should probably target at +least SAM 3.1, since it's available in both I2P and i2pd and introduces support +for the SIGNATURE_TYPE parameter. +{%- endtrans %} + +.. code:: java + + public boolean HelloSAM() { + Reply repl = CommandSAM("HELLO VERSION MIN=3.0 MAX=3.1 \n"); + if (repl.result == Reply.REPLY_TYPES.OK) { + return true; + } + System.out.println(repl.String()); + return false; + } + +{% trans -%} +As you can see, we use the CommandSAM function we created before to send the +newline-terminated command ``HELLO VERSION MIN=3.0 MAX=3.1 \n``. This tells +SAM that you want to start communicating with the API, and that you know how +to speak SAM version 3.0 and 3.1. The router, in turn, will respond with +like ``HELLO REPLY RESULT=OK VERSION=3.1`` which is a string you can pass to +the Reply constructor to get a valid Reply object. From now on, we can use our +CommandSAM function and Reply object to deal with all our communication across +the SAM bridge. +{%- endtrans %} + +{% trans -%} +Finally, let's add a test for our "HelloSAM" function. +{%- endtrans %} + +.. code:: java + + @Test public void testHelloSAM() { + Jsam classUnderTest = new Jsam(); + assertTrue("HelloSAM should return 'true' in the presence of an alive SAM bridge", classUnderTest.HelloSAM()); + } + + +Creating a "Session" for your application +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +{% trans -%} +Now that you've negotiated your connection to SAM and agreed on a SAM version +you both speak, you can set up peer-to-peer connections for your application +to connect to other i2p applications. You do this by sending a "SESSION CREATE" +command to the SAM Bridge. To do that, we'll use a CreateSession function that +accepts a session ID and a destination type parameter. +{%- endtrans %} + +.. code:: java + + public String CreateSession(String id, String destination ) { + if (destination == "") { + destination = "TRANSIENT"; + } + Reply repl = CommandSAM("SESSION CREATE STYLE=STREAM ID=" + ID + " DESTINATION=" + destination); + if (repl.result == Reply.REPLY_TYPES.OK) { + return id; + } + return ""; + } + +{% trans -%} +That was easy, right? All we had to do was adapt the pattern we used in our +HelloSAM function to the ``SESSION CREATE`` command. A good reply from the +bridge will still return OK, and in that case we return the ID of the newly +created SAM connection. Otherwise, we return an empty string because that's an +invalid ID anyway and it failed, so it's easy to check. Let's see if this +function works by writing a test for it: +{%- endtrans %} + +.. code:: java + + @Test public void testCreateSession() { + Jsam classUnderTest = new Jsam(); + assertTrue("HelloSAM should return 'true' in the presence of an alive SAM bridge", classUnderTest.HelloSAM()); + assertEquals("test", classUnderTest.CreateSession("test", "")); + } + +{% trans -%} +Note that in this test, we *must* call HelloSAM first to establish communication +with SAM before starting our session. If not, the bridge will reply with an +error and the test will fail. +{%- endtrans %} + +.. _looking-up-hosts-by-name-or-b32: + +Looking up Hosts by name or .b32 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +{% trans -%} +Now you have your session established and your local destination, and need to +decide what you want to do with them. Your session can now be commanded to +connect to a remote service over I2P, or to wait for incoming connections to +respond to. However, before you can connect to a remote destination, you may +need to obtain the base64 of the destination, which is what the API expects. In +order to do this, we'll create a LookupName function, which will return the +base64 in a usable form. +{%- endtrans %} + +.. code:: java + + public String LookupName(String name) { + String cmd = "NAMING LOOKUP NAME=" + name + "\n"; + Reply repl = CommandSAM(cmd); + if (repl.result == Reply.REPLY_TYPES.OK) { + System.out.println(repl.replyMap.get("VALUE")); + return repl.replyMap.get("VALUE"); + } + return ""; + } + +{% trans -%} +Again, this is almost the same as our HelloSAM and CreateSession functions, +with one difference. Since we're looking for the VALUE specifically and the NAME +field will be the same as the ``name`` argument, it simply returns the base64 +string of the destination requested. +{%- endtrans %} + +{% trans -%} +Now that we have our LookupName function, let's test it: +{%- endtrans %} + +.. code:: java + + @Test public void testLookupName() { + Jsam classUnderTest = new Jsam(); + assertTrue("HelloSAM should return 'true' in the presence of an alive SAM bridge", classUnderTest.HelloSAM()); + assertEquals("8ZAW~KzGFMUEj0pdchy6GQOOZbuzbqpWtiApEj8LHy2~O~58XKxRrA43cA23a9oDpNZDqWhRWEtehSnX5NoCwJcXWWdO1ksKEUim6cQLP-VpQyuZTIIqwSADwgoe6ikxZG0NGvy5FijgxF4EW9zg39nhUNKRejYNHhOBZKIX38qYyXoB8XCVJybKg89aMMPsCT884F0CLBKbHeYhpYGmhE4YW~aV21c5pebivvxeJPWuTBAOmYxAIgJE3fFU-fucQn9YyGUFa8F3t-0Vco-9qVNSEWfgrdXOdKT6orr3sfssiKo3ybRWdTpxycZ6wB4qHWgTSU5A-gOA3ACTCMZBsASN3W5cz6GRZCspQ0HNu~R~nJ8V06Mmw~iVYOu5lDvipmG6-dJky6XRxCedczxMM1GWFoieQ8Ysfuxq-j8keEtaYmyUQme6TcviCEvQsxyVirr~dTC-F8aZ~y2AlG5IJz5KD02nO6TRkI2fgjHhv9OZ9nskh-I2jxAzFP6Is1kyAAAA", classUnderTest.LookupName("i2p-projekt.i2p")); + } + +Sending and Recieving Information +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +{% trans -%} +At last, we are going to establish a connection to another service with our new +library. This part confused me a bit at first, but the most astute Java +developers were probably wondering why we didn't extend the socket class +instead of creating a Socket variable inside of the Jsam class. That's because +until now, we've been communicating with the "Control Socket" and we need to +create a new socket to do the actual communication. So we've waited to extend +the the Socket class with the Jsam class until now: +{%- endtrans %} + +.. code:: java + + public class Jsam extends Socket { + +{% trans -%} +Also, let's alter our startConnection function so that we can use it to switch +over from the control socket to the socket we'll be using in our application. It +will now take a Socket argument. +{%- endtrans %} + +.. code:: java + + public void startConnection(Socket socket) { + try { + socket.connect(new InetSocketAddress(SAMHost, SAMPort), 600 ); + } catch (Exception e) { + System.out.println(e); + } + try { + writer = new PrintWriter(socket.getOutputStream(), true); + } catch (Exception e) { + System.out.println(e); + } + try { + reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); + } catch (Exception e) { + System.out.println(e); + } + } + +{% trans -%} +This allows us to quickly and easily open a new socket to communicate over, +perform the "Hello SAM" handshake over again, and connect the stream. +{%- endtrans %} + +.. code:: java + + public String ConnectSession(String id, String destination) { + startConnection(this); + HelloSAM(); + if (destination.endsWith(".i2p")) { + destination = LookupName(destination); + } + String cmd = "STREAM CONNECT ID=" + id + " DESTINATION=" + destination + " SILENT=false"; + Reply repl = CommandSAM(cmd); + if (repl.result == Reply.REPLY_TYPES.OK) { + System.out.println(repl.String()); + return id; + } + System.out.println(repl.String()); + return ""; + } + +{% trans -%} +And now you have a new Socket for communicating over SAM! Let's do the same +thing for Accepting remote connections: +{%- endtrans %} + +.. code:: java + + public String AcceptSession(String id) { + startConnection(this); + HelloSAM(); + String cmd = "STREAM ACCEPT ID=" + id + " SILENT=false"; + Reply repl = CommandSAM(cmd); + if (repl.result == Reply.REPLY_TYPES.OK) { + System.out.println(repl.String()); + return id; + } + System.out.println(repl.String()); + return ""; + } + +{% trans -%} +There you have it. That's how you build a SAM library, step-by-step. In the +future, I will cross-reference this with the working version of the library, +Jsam, and the SAM v3 specification but for now I've got to get some other stuff +done. +{%- endtrans %} + -- GitLab