diff --git a/client.go b/client.go new file mode 100644 index 0000000..52e8680 --- /dev/null +++ b/client.go @@ -0,0 +1,60 @@ +package goSam + +import ( + "bufio" + "fmt" + "io" +) + +type Client struct { + samConn io.ReadWriteCloser + + fromSam *bufio.Reader + toSam *bufio.Writer +} + +func NewClient(samConn io.ReadWriteCloser) (*Client, error) { + c := &Client{ + samConn: samConn, + fromSam: bufio.NewReader(samConn), + toSam: bufio.NewWriter(samConn), + } + return c, nil +} + +func (c *Client) Hello() (err error) { + if _, err = c.toSam.WriteString("HELLO VERSION MIN=3.0 MAX=3.0\n"); err != nil { + return err + } + + if err = c.toSam.Flush(); err != nil { + return err + } + + for { + line, err := c.fromSam.ReadString('\n') + if err != nil { + return err + } + + reply, err := parseReply(line) + if err != nil { + return err + } + + if reply.Topic != "HELLO" { + return fmt.Errorf("Unknown Reply: %+v\n", reply) + } + + if reply.Pairs["RESULT"] != "OK" { + return fmt.Errorf("Handshake did not succeed") + } + + break + } + return nil +} + +func (c *Client) Close() error { + return c.samConn.Close() +} diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..e8c062d --- /dev/null +++ b/client_test.go @@ -0,0 +1,41 @@ +package goSam + +import ( + "net" + "testing" +) + +var ( + client *Client +) + +func setup() { + var err error + + // these tests expect a running SAM brige on this address + conn, err := net.Dial("tcp", "localhost:7656") + if err != nil { + panic(err) + } + + client, err = NewClient(conn) + if err != nil { + panic(err) + } +} + +func teardown() { + client.Close() +} + +func TestClientHello(t *testing.T) { + var err error + + setup() + defer teardown() + + err = client.Hello() + if err != nil { + t.Errorf("client.Hello() should not throw an error.\n%s\n", err) + } +} diff --git a/naming.go b/naming.go new file mode 100644 index 0000000..a271d75 --- /dev/null +++ b/naming.go @@ -0,0 +1,76 @@ +package goSam + +import ( + "fmt" +) + +type Result int + +const ( + ResultOk Result = iota //Operation completed successfully + ResultCantReachPeer //The peer exists, but cannot be reached + ResultDuplicatedDest //The specified Destination is already in use + ResultI2PError //A generic I2P error (e.g. I2CP disconnection, etc.) + ResultInvalidKey //The specified key is not valid (bad format, etc.) + ResultKeyNotFound //The naming system can't resolve the given name + ResultPeerNotFound //The peer cannot be found on the network + ResultTimeout // Timeout while waiting for an event (e.g. peer answer) +) + +type ReplyError struct { + Result Result + Reply *Reply +} + +func (r ReplyError) Error() string { + return fmt.Sprintf("ReplyError: Result:%d - Reply:%+v", r.Reply) +} + +func (c *Client) Lookup(name string) (addr string, err error) { + msg := fmt.Sprintf("NAMING LOOKUP NAME=%s\n", name) + if _, err = c.toSam.WriteString(msg); err != nil { + return + } + + if err = c.toSam.Flush(); err != nil { + return + } + + var ( + line string + r *Reply + ) + for { + line, err = c.fromSam.ReadString('\n') + if err != nil { + return + } + + r, err = parseReply(line) + if err != nil { + break + } + + if r.Topic != "NAMING" || r.Type != "REPLY" { + err = fmt.Errorf("Unknown Reply: %+v\n", r) + break + } + + switch r.Pairs["RESULT"] { + case "OK": + addr = r.Pairs["VALUE"] + return + case "KEY_NOT_FOUND": + err = ReplyError{ResultKeyNotFound, r} + } + + if r.Pairs["NAME"] != name { + err = fmt.Errorf("i2p Replyied with: %+v\n", r) + break + } + + break + } + + return +} diff --git a/naming_test.go b/naming_test.go new file mode 100644 index 0000000..0b426bf --- /dev/null +++ b/naming_test.go @@ -0,0 +1,46 @@ +package goSam + +import ( + "fmt" + "testing" +) + +func TestClientLookupInvalid(t *testing.T) { + var err error + + setup() + defer teardown() + + client.Hello() + + addr, err := client.Lookup("abci2p") + if addr != "" || err == nil { + t.Error("client.Lookup() should throw an error.") + } + + repErr, ok := err.(ReplyError) + if ok && repErr.Result != ResultKeyNotFound { + t.Error("client.Lookup() should throw an ResultKeyNotFound error. Got:%v\n", repErr) + } +} + +func ExampleClient_Lookup() { + var err error + + setup() + defer teardown() + + client.Hello() + + _, err = client.Lookup("zzz.i2p") + if err != nil { + fmt.Printf("client.Lookup() should not throw an error.\n%s\n", err) + } + + fmt.Println("Address of zzz.i2p:") + // Addresses change all the time + // fmt.Println(addr) + + // Output: + //Address of zzz.i2p: +} diff --git a/replyParser.go b/replyParser.go new file mode 100644 index 0000000..86f42dd --- /dev/null +++ b/replyParser.go @@ -0,0 +1,36 @@ +package goSam + +import ( + "fmt" + "strings" +) + +type Reply struct { + Topic string + Type string + + Pairs map[string]string +} + +func parseReply(line string) (r *Reply, err error) { + parts := strings.Split(line, " ") + if len(parts) < 3 { + return nil, fmt.Errorf("Malformed Reply.\n%s\n", line) + } + + r = &Reply{ + Topic: parts[0], + Type: parts[1], + Pairs: make(map[string]string, len(parts)-2), + } + + for _, v := range parts[2:] { + kvPair := strings.Split(v, "=") + if len(kvPair) != 2 { + return nil, fmt.Errorf("Malformed key-value-pair.\n%s\n", kvPair) + } + r.Pairs[kvPair[0]] = kvPair[1] + } + + return +} diff --git a/replyParser_test.go b/replyParser_test.go new file mode 100644 index 0000000..eea2309 --- /dev/null +++ b/replyParser_test.go @@ -0,0 +1,91 @@ +package goSam + +import ( + "testing" +) + +func TestParseInvalidReply(t *testing.T) { + str := "asd asd=" + + r, err := parseReply(str) + if err == nil { + t.Fatalf("Should throw an error.r:%v\n", r) + } +} + +func TestParseReplyNAMING(t *testing.T) { + str := "NAMING REPLY RESULT=OK NAME=zzz.i2p VALUE=GKapJ8koUcBj~jmQzHsTYxDg2tpfWj0xjQTzd8BhfC9c3OS5fwPBNajgF-eOD6eCjFTqTlorlh7Hnd8kXj1qblUGXT-tDoR9~YV8dmXl51cJn9MVTRrEqRWSJVXbUUz9t5Po6Xa247Vr0sJn27R4KoKP8QVj1GuH6dB3b6wTPbOamC3dkO18vkQkfZWUdRMDXk0d8AdjB0E0864nOT~J9Fpnd2pQE5uoFT6P0DqtQR2jsFvf9ME61aqLvKPPWpkgdn4z6Zkm-NJOcDz2Nv8Si7hli94E9SghMYRsdjU-knObKvxiagn84FIwcOpepxuG~kFXdD5NfsH0v6Uri3usE3uSzpWS0EHmrlfoLr5uGGd9ZHwwCIcgfOATaPRMUEQxiK9q48PS0V3EXXO4-YLT0vIfk4xO~XqZpn8~PW1kFe2mQMHd7oO89yCk-3yizRG3UyFtI7-mO~eCI6-m1spYoigStgoupnC3G85gJkqEjMm49gUjbhfWKWI-6NwTj0ZnAAAA" + + reply, err := parseReply(str) + if err != nil { + t.Fatalf("parseReply should not throw an error!\n%s", err) + } + + if reply.Topic != "NAMING" { + t.Fatalf("Wrong Topic. Got %s expected NAMING", reply.Topic) + } + + if reply.Type != "REPLY" { + t.Fatalf("Wrong Type. Got %s expected REPLY", reply.Type) + } + + if len(reply.Pairs) != 3 { + t.Fatalf("Wrong ammount of Pairs. Got %d expected 3", len(reply.Pairs)) + } + + for k, v := range reply.Pairs { + switch k { + case "RESULT": + if v != "OK" { + t.Fatalf("Wrong Result. Got %s expected OK", v) + } + case "NAME": + if v != "zzz.i2p" { + t.Fatalf("Wrong Name. Got %s expected OK", v) + } + case "VALUE": + expect := "GKapJ8koUcBj~jmQzHsTYxDg2tpfWj0xjQTzd8BhfC9c3OS5fwPBNajgF-eOD6eCjFTqTlorlh7Hnd8kXj1qblUGXT-tDoR9~YV8dmXl51cJn9MVTRrEqRWSJVXbUUz9t5Po6Xa247Vr0sJn27R4KoKP8QVj1GuH6dB3b6wTPbOamC3dkO18vkQkfZWUdRMDXk0d8AdjB0E0864nOT~J9Fpnd2pQE5uoFT6P0DqtQR2jsFvf9ME61aqLvKPPWpkgdn4z6Zkm-NJOcDz2Nv8Si7hli94E9SghMYRsdjU-knObKvxiagn84FIwcOpepxuG~kFXdD5NfsH0v6Uri3usE3uSzpWS0EHmrlfoLr5uGGd9ZHwwCIcgfOATaPRMUEQxiK9q48PS0V3EXXO4-YLT0vIfk4xO~XqZpn8~PW1kFe2mQMHd7oO89yCk-3yizRG3UyFtI7-mO~eCI6-m1spYoigStgoupnC3G85gJkqEjMm49gUjbhfWKWI-6NwTj0ZnAAAA" + if v != expect { + t.Fatalf("Wrong Value.\nGot:%s\nExpected:%s", v, expect) + } + default: + t.Fatalf("Unknown kvPair %s=%s", k, v) + } + } +} + +func TestParseReplyHELLO(t *testing.T) { + str := "HELLO REPLY RESULT=OK VERSION=3.0" + + reply, err := parseReply(str) + if err != nil { + t.Fatalf("parseReply should not throw an error!\n%s", err) + } + + if reply.Topic != "HELLO" { + t.Fatalf("Wrong Topic. Got %s expected HELLO", reply.Topic) + } + + if reply.Type != "REPLY" { + t.Fatalf("Wrong Type. Got %s expected REPLY", reply.Type) + } + + if len(reply.Pairs) != 2 { + t.Fatalf("Wrong ammount of Pairs. Got %d expected 3", len(reply.Pairs)) + } + + for k, v := range reply.Pairs { + switch k { + case "RESULT": + if v != "OK" { + t.Fatalf("Wrong Result. Got %s expected OK", v) + } + case "VERSION": + if v != "3.0" { + t.Fatalf("Wrong Name. Got %s expected OK", v) + } + default: + t.Fatalf("Unknown kvPair %s=%s", k, v) + } + } +}