├── .gitignore ├── COPYING ├── Makefile ├── README.rdoc ├── auth.go ├── auth_test.go ├── client.go ├── client_test.go ├── events.go ├── fixture └── message.xml.gt ├── main.go ├── test_util.go ├── util.go ├── xmpp.go └── xmpp_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *swp 2 | _* 3 | *.out 4 | *6 5 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Google Inc 2 | 3 | This software has been developed by Dmitry Ratnikov (ratnikov@google.com) 4 | and has little relation to Google aside for legal reasons. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include $(GOROOT)/src/Make.inc 2 | 3 | TARG=xmpp 4 | GOFILES=auth.go client.go events.go util.go test_util.go xmpp.go 5 | 6 | _xmpp_: 7 | 6g -o _xmpp_.6 $(GOFILES) 8 | 9 | _obj: 10 | mkdir _obj 11 | 12 | xmpp.a: _obj _xmpp_ 13 | gopack grc _obj/xmpp.a _xmpp_.6 14 | 15 | main: xmpp.a 16 | 6g -I_obj/ -o main.6 main.go 17 | 6l -L_obj/ -o main.out main.6 18 | 19 | include $(GOROOT)/src/Make.pkg 20 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | == Overview 2 | 3 | Provides go implementation of xmpp protocol. 4 | 5 | == License and Copyright 6 | 7 | All of the code is copyrighted by Google Inc. and is released under MIT license. 8 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | package xmpp; 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "encoding/base64" 7 | "os" 8 | "regexp" 9 | ) 10 | 11 | const ( 12 | auth_regex = "^([^@]+)@([^@]+)$" 13 | ) 14 | 15 | type Auth struct { 16 | user, domain, password string 17 | } 18 | 19 | func NewAuth(login, password string) (*Auth) { 20 | chunks := regexp.MustCompile(auth_regex).FindStringSubmatch(login) 21 | 22 | if len(chunks) == 0 { 23 | panic(os.NewError(fmt.Sprintf("Authentication identifier has to match %s", auth_regex))) 24 | } 25 | 26 | return &Auth{user: chunks[1], domain: chunks[2], password: password } 27 | } 28 | 29 | func (auth *Auth) Base64() string { 30 | bb := &bytes.Buffer{}; 31 | encoder := base64.NewEncoder(base64.StdEncoding, bb); 32 | 33 | raw := "\x00" + auth.user +"\x00" + auth.password 34 | encoder.Write(bytes.NewBufferString(raw).Bytes()) 35 | encoder.Close() 36 | 37 | return bb.String() 38 | } 39 | -------------------------------------------------------------------------------- /auth_test.go: -------------------------------------------------------------------------------- 1 | package xmpp; 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestNewAuth(t *testing.T) { 8 | if auth := NewAuth("joe@test.com", "secret"); auth != nil { 9 | assertEqual(t, "joe", auth.user, "Should parse the user") 10 | assertEqual(t, "test.com", auth.domain, "Should parse the domain") 11 | assertEqual(t, "secret", auth.password, "Should assign password") 12 | } else { 13 | t.Errorf("Expected to parse %s as valid authentication info, but got nil.") 14 | } 15 | 16 | assertPanic(t, "Should not parse auth information with multiple '@'", func() { 17 | NewAuth("joe@johnson@test.com", "secret") 18 | }) 19 | 20 | assertPanic(t, "Should complain about lack of @", func() { 21 | NewAuth("joe-someone.com", "secret") 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package xmpp; 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "io" 7 | "os" 8 | "net" 9 | ) 10 | 11 | type Client struct { 12 | hostname string 13 | conn io.ReadWriter 14 | listeners listenerList 15 | } 16 | 17 | func NewClient(hostname, user, password string) (client *Client, failure os.Error) { 18 | failure = nil 19 | defer func() { 20 | if err := recover(); err != nil { 21 | client = nil 22 | 23 | var ok bool 24 | if failure, ok = err.(os.Error); !ok { 25 | failure = os.NewError(fmt.Sprintf("Weird error happened: %s\n", failure)) 26 | } 27 | } 28 | }() 29 | 30 | client = &Client{hostname: hostname} 31 | 32 | client.startTls() 33 | client.authenticate(NewAuth(user, password)) 34 | 35 | return client, nil 36 | } 37 | 38 | func (client *Client) OnAny(callback func(string)) { 39 | client.listeners.onAny(callback) 40 | } 41 | 42 | func (client *Client) OnMessage(callback func(Message)) { 43 | client.listeners.onMessage(callback) 44 | } 45 | 46 | func (client *Client) OnChatMessage(callback func(Message)) { 47 | client.onMessageType("chat", callback) 48 | } 49 | 50 | func (client *Client) OnErrorMessage(callback func(Message)) { 51 | client.onMessageType("error", callback) 52 | } 53 | 54 | func (client *Client) OnUnknown(callback func(string)) { 55 | client.listeners.onUnknown(callback) 56 | } 57 | 58 | func (client *Client) Loop() os.Error { 59 | for { 60 | if read, err := client.read(); err != nil { 61 | return err 62 | } else { 63 | 64 | if msg := NewMessage(read); msg != nil { 65 | client.listeners.fireOnMessage(msg) 66 | } else { 67 | client.listeners.fireOnUnknown(read) 68 | } 69 | } 70 | } 71 | 72 | return nil 73 | } 74 | 75 | func (client *Client) startTls() { 76 | var plain_conn net.Conn 77 | var err os.Error 78 | 79 | if plain_conn, err = net.Dial("tcp", "", client.hostname + ":5222"); err != nil { 80 | die("Failed to establish plain connection: %s", err) 81 | } 82 | 83 | write(plain_conn, "") 84 | write(plain_conn, "") 85 | 86 | log("Read: %s (%s)", mustRead(plain_conn)) 87 | 88 | // assuming need to start tls 89 | write(plain_conn, "") 90 | log("Read: %s", mustRead(plain_conn)) 91 | 92 | // assuming the server asked to proceed 93 | if client.conn, err = tls.Dial("tcp", "", client.hostname + ":https", nil); err != nil { 94 | die("Failed to establish tls connection (%s)", err) 95 | } 96 | } 97 | 98 | func (client *Client) authenticate(auth *Auth) { 99 | client.write("") 100 | 101 | // get stream response with id back 102 | client.read() 103 | 104 | // get auth mechanisms... 105 | client.read() 106 | 107 | // assuming we can do plain authentication 108 | client.write("%s", auth.Base64()) 109 | 110 | // get "success" response 111 | client.read() 112 | 113 | // re-start the stream 114 | client.write("") 115 | 116 | client.read() // get stream acknowledgement 117 | client.read() // get session information 118 | 119 | // identify as xmpp-bot1029 120 | client.write("Home") 121 | client.read() // get return as to what we're bound to... or something... 122 | 123 | // anyhow, assuming authentication is complete 124 | } 125 | 126 | func (client *Client) onMessageType(mtype string, callback func(Message)) { 127 | client.listeners.onMessage(func(msg Message) { 128 | if msg.Type() == mtype { 129 | callback(msg) 130 | } else { 131 | // not our type, so doing nothing 132 | } 133 | }) 134 | } 135 | 136 | func (client *Client) read() (string, os.Error) { 137 | return read(client.conn) 138 | } 139 | 140 | func (client *Client) write(format string, args ...interface{}) int { 141 | return write(client.conn, format, args...) 142 | } 143 | 144 | func (client *Client) Send(msg string) { 145 | client.write(msg) 146 | } 147 | 148 | func (client *Client) SendChat(to, msg string) { 149 | client.write("%s", to, msg) 150 | } 151 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package xmpp 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "regexp" 8 | "template" 9 | "testing" 10 | ) 11 | 12 | type testIO struct { 13 | out bytes.Buffer 14 | in bytes.Buffer 15 | } 16 | 17 | func (buf *testIO) Read(p []byte) (int, os.Error) { 18 | return buf.in.Read(p) 19 | } 20 | 21 | func (buf *testIO) Write(p []byte) (int, os.Error) { 22 | return buf.out.Write(p) 23 | } 24 | 25 | // Resets both in and out buffers 26 | func (buf *testIO) Reset() { 27 | buf.in.Reset() 28 | buf.out.Reset() 29 | } 30 | 31 | func (buf *testIO) PushString(str string) { 32 | buf.in.Write(bytes.NewBufferString(str).Bytes()) 33 | } 34 | 35 | func (buf *testIO) PushFixture(fixture string, data interface{}) { 36 | buf.PushString(readFixture(fixture, data)) 37 | } 38 | 39 | func (buf *testIO) PopString() string { 40 | var byte_buf []byte 41 | 42 | buf.out.Read(byte_buf) 43 | 44 | return bytes.NewBuffer(byte_buf).String() 45 | } 46 | 47 | func setupClient() (io *testIO, client *Client) { 48 | io = new(testIO) 49 | client = &Client{conn: io} 50 | 51 | return 52 | } 53 | 54 | type fixtureMessage struct { 55 | Type, To, From, Body, Botid string 56 | } 57 | 58 | func TestLoop(t *testing.T) { 59 | testio, client := setupClient() 60 | 61 | testio.Reset() 62 | 63 | // should return from looping, if testio returns EOF 64 | client.Loop() 65 | 66 | // if we got here, then loop must have existed, and we're good 67 | } 68 | 69 | func TestClientOnAny(t *testing.T) { 70 | testio, client := setupClient() 71 | 72 | var received string 73 | 74 | client.OnAny(func(msg string) { 75 | received = msg 76 | }) 77 | 78 | testio.PushString("") 79 | client.Loop() 80 | 81 | assertMatch(t, "hello world!", received, "Should return the unknown message") 82 | 83 | testio.PushString("hello world!") 84 | client.Loop() 85 | 86 | assertMatch(t, "message.*body.*hello world!.*/body.*/message", received, "Should return the message as well") 87 | } 88 | 89 | func TestClientOnMessage(t *testing.T) { 90 | testio, client := setupClient() 91 | 92 | testio.PushFixture("message", fixtureMessage{ 93 | Type: "any-possible-type", 94 | Body: "Some stuff", 95 | From: "joe", 96 | To: "sam" }) 97 | 98 | var received *Message 99 | 100 | client.OnMessage(func(msg Message) { 101 | received = &msg 102 | }) 103 | 104 | client.Loop() 105 | 106 | should(t, "invoked callback", func() bool { 107 | return received != nil 108 | }) 109 | 110 | assertMatch(t, "any-possible-type", received.Type(), "Should capture its type") 111 | } 112 | 113 | func TestClientOnChatMessage(t *testing.T) { 114 | testio, client := setupClient() 115 | 116 | testio.PushFixture("message", fixtureMessage{ 117 | Type: "chat", 118 | Body: "Hello world!", 119 | From: "joe@example.com", 120 | To: "sam@example.com" }) 121 | 122 | var received *Message 123 | 124 | client.OnChatMessage(func(msg Message) { 125 | received = &msg 126 | }) 127 | 128 | client.Loop() 129 | 130 | should(t, "have invoked onMessage callback", func() bool { 131 | return received != nil 132 | }) 133 | 134 | assertMatch(t, "chat", received.Type(), "Should be a chat message") 135 | assertMatch(t, "Hello world!", received.Body(), "Should include the message") 136 | assertMatch(t, "sam@example.com", received.To(), "Should include To") 137 | assertMatch(t, "joe@example.com", received.From(), "Should include From") 138 | } 139 | 140 | func TestClientOnErrorMessage(t *testing.T) { 141 | testio, client := setupClient() 142 | 143 | testio.PushFixture("message", fixtureMessage{ 144 | Type: "error", 145 | Body: "Something went wrong", 146 | From: "joe", 147 | To: "sam" }) 148 | 149 | testio.PushFixture("message", fixtureMessage{ 150 | Type: "chat", 151 | Body: "Some message" }) 152 | 153 | var received *Message 154 | client.OnErrorMessage(func(msg Message) { 155 | received = &msg 156 | }) 157 | 158 | client.Loop() 159 | 160 | should(t, "have invoked the callback", func() bool { 161 | return received != nil 162 | }) 163 | 164 | assertMatch(t, "went.*wrong", received.Body(), "Should pick up the error only") 165 | } 166 | 167 | func TestClientOnUnknown(t *testing.T) { 168 | testio, client := setupClient() 169 | 170 | var received string 171 | client.OnUnknown(func(msg string) { 172 | received = msg 173 | }) 174 | 175 | testio.PushString("hello world!") 176 | client.Loop() 177 | 178 | should(t, "invoke the unknown listener", func() bool { 179 | match, _ := regexp.MatchString("hello world!", received) 180 | 181 | return match 182 | }) 183 | } 184 | 185 | func readFixture(filename string, data interface{}) string { 186 | buf := bytes.NewBufferString("") 187 | 188 | template.MustParseFile(fmt.Sprintf("fixture/%s.xml.gt", filename), nil).Execute(buf, data) 189 | 190 | return buf.String() 191 | } 192 | -------------------------------------------------------------------------------- /events.go: -------------------------------------------------------------------------------- 1 | package xmpp 2 | 3 | type Listener interface { 4 | } 5 | 6 | type MessageListener struct { 7 | callback func(message Message) 8 | } 9 | 10 | type UnknownListener struct { 11 | callback func(raw string) 12 | } 13 | 14 | type AnyListener struct { 15 | callback func(raw string) 16 | } 17 | 18 | type listenerList struct { 19 | listeners []Listener 20 | } 21 | 22 | func (list *listenerList) Subscribe(listener Listener) { 23 | list.listeners = append(list.listeners, listener) 24 | } 25 | 26 | func (list *listenerList) onAny(callback func(string)) { 27 | list.Subscribe(AnyListener{ callback : callback }) 28 | } 29 | 30 | func (list *listenerList) onMessage(callback func(Message)) { 31 | list.Subscribe(MessageListener{ callback: callback }) 32 | } 33 | 34 | func (list *listenerList) onUnknown(callback func(string)) { 35 | list.Subscribe(UnknownListener{ callback : callback }) 36 | } 37 | 38 | func (list *listenerList) fireOnMessage(msg *Message) { 39 | list.eachListener(func(l Listener) { 40 | fireIfAny(l, msg.Raw()) 41 | 42 | if msg_l, ok := l.(MessageListener); ok { 43 | msg_l.callback(*msg) 44 | } 45 | }) 46 | } 47 | 48 | func (list *listenerList) fireOnUnknown(msg string) { 49 | list.eachListener(func(l Listener) { 50 | fireIfAny(l, msg) 51 | 52 | if unknown_l, ok := l.(UnknownListener); ok { 53 | unknown_l.callback(msg) 54 | } 55 | }) 56 | } 57 | 58 | func fireIfAny(l Listener, msg string) { 59 | if any_l, ok := l.(AnyListener); ok { 60 | any_l.callback(msg) 61 | } else { 62 | // not any listener, so nothing to do 63 | } 64 | } 65 | 66 | func (list *listenerList) eachListener(callback func(Listener)) { 67 | for i := range list.listeners { 68 | callback(list.listeners[i]) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /fixture/message.xml.gt: -------------------------------------------------------------------------------- 1 | 2 | 3 | {Body} 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "xmpp" 6 | ) 7 | 8 | func main() { 9 | client, err := xmpp.NewClient("talk.google.com", "xmpp.chatterbox@gmail.com", "XXX") 10 | 11 | if err != nil { 12 | fmt.Printf("Failed due to: %s\n", err) 13 | } else { 14 | client.OnAny(func(msg string) { 15 | log(msg) 16 | }) 17 | 18 | client.OnMessage(func(msg xmpp.Message) { 19 | log("Got a message from %s to %s: %s\n", msg.From(), msg.To(), msg.Body()) 20 | }) 21 | 22 | client.SendChat("ratnikov@gmail.com", "Hello world!") 23 | 24 | client.Loop() 25 | } 26 | } 27 | 28 | func log(format string, args ...interface{}) { 29 | fmt.Printf("MAIN LOG: " + format + "\n", args...) 30 | } 31 | -------------------------------------------------------------------------------- /test_util.go: -------------------------------------------------------------------------------- 1 | package xmpp 2 | 3 | import ( 4 | "testing" 5 | "regexp" 6 | ) 7 | 8 | func should(t *testing.T, message string, checker func() bool) { 9 | if !checker() { 10 | t.Fatalf("Should " + message) 11 | } 12 | } 13 | 14 | func assertEqual(t *testing.T, expected, actual interface{}, message string) { 15 | if expected != actual { 16 | t.Fatalf("Expected <%s> but got <%s>", expected, actual) 17 | } 18 | } 19 | 20 | func assertMatch(t *testing.T, regex_str, str, message string) { 21 | regex := regexp.MustCompile(regex_str) 22 | 23 | if !regex.MatchString(str) { 24 | t.Fatalf("%s: Expected <%s> to match <%s>", message, str, regex) 25 | } else { 26 | // all good 27 | } 28 | } 29 | 30 | func assertPanic(t *testing.T, message string, f func()) { 31 | defer func() { 32 | if err := recover(); err == nil { 33 | t.Fatalf(message) 34 | } 35 | }() 36 | 37 | f() 38 | } 39 | 40 | func fail(t *testing.T, message string, args ...interface{}) { 41 | t.Fatalf(message, args...) 42 | } 43 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package xmpp 2 | 3 | 4 | import ( 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "os" 9 | ) 10 | 11 | func read(in io.Reader) (string, os.Error) { 12 | buf := make([]byte, 2048) 13 | 14 | _, err := in.Read(buf) 15 | 16 | return bytes.NewBuffer(buf).String(), err 17 | } 18 | 19 | func mustRead(in io.Reader) (out string) { 20 | var err os.Error 21 | if out, err = read(in); err != nil { 22 | die("Failed to read (%s)", err) 23 | } 24 | 25 | return 26 | } 27 | 28 | func write(out io.Writer, format string, args ...interface{}) int { 29 | buf := bytes.NewBufferString(fmt.Sprintf(format + "\n", args...)) 30 | 31 | n, err := out.Write(buf.Bytes()) 32 | 33 | if err != nil { 34 | die("Failed to write to %s (%s)", out, err) 35 | } 36 | 37 | return n 38 | } 39 | 40 | func die(format string, args ...interface{}) { 41 | panic(os.NewError(fmt.Sprintf(format, args...))) 42 | } 43 | 44 | func log(format string, args ...interface{}) { 45 | fmt.Printf("LOG: %s\n", fmt.Sprintf(format, args... )) 46 | } 47 | -------------------------------------------------------------------------------- /xmpp.go: -------------------------------------------------------------------------------- 1 | package xmpp 2 | 3 | import ( 4 | "os" 5 | "regexp" 6 | ) 7 | 8 | type Message struct { 9 | mtype, raw, to, from, body string 10 | } 11 | 12 | func NewMessage(raw string) *Message { 13 | var err os.Error 14 | msg := new(Message) 15 | 16 | msg.raw = raw 17 | 18 | if msg.mtype, err = parse(raw, "type=\"([^\"]+)\""); err != nil { 19 | return nil // failed to parse what type it is 20 | } 21 | 22 | if msg.to, err = parse(raw, "to=\"([^\"]+)\""); err != nil { 23 | return nil // failed to parse to 24 | } 25 | 26 | if msg.from, err = parse(raw, "from=\"([^\"]+)\""); err != nil { 27 | return nil // failed to parse from 28 | } 29 | 30 | if msg.body, err = parse(raw, "(.*)"); err != nil { 31 | return nil // failed to parse body 32 | } 33 | 34 | return msg 35 | } 36 | 37 | func (m *Message) Raw() string { 38 | return m.raw 39 | } 40 | 41 | func (m *Message) Type() string { 42 | return m.mtype 43 | } 44 | 45 | func (m *Message) From() (from string) { 46 | return m.from 47 | } 48 | 49 | func (m *Message) To() string { 50 | return m.to 51 | } 52 | 53 | func (m *Message) Body() string { 54 | return m.body 55 | } 56 | 57 | func parse(raw, regex string) (out string, err os.Error) { 58 | if match := regexp.MustCompile(regex).FindStringSubmatch(raw); len(match) > 0 { 59 | out = match[1] 60 | } else { 61 | err = os.NewError("Failed to parse message") 62 | } 63 | 64 | return 65 | } 66 | -------------------------------------------------------------------------------- /xmpp_test.go: -------------------------------------------------------------------------------- 1 | package xmpp 2 | 3 | import "testing" 4 | 5 | func TestBadXmppMessage(t *testing.T) { 6 | should(t, "create a nil message", func() bool { 7 | return NewMessage("no good info") == nil 8 | }) 9 | } 10 | 11 | func TestXmppMessage(t *testing.T) { 12 | msg := NewMessage("Hello world") 13 | 14 | should(t, "parse 'type'", func() bool { 15 | return msg.Type() == "special" 16 | }) 17 | 18 | should(t, "parse 'from'", func() bool { 19 | return msg.From() == "me" 20 | }) 21 | 22 | should(t, "parse 'to'", func() bool { 23 | return msg.To() == "you" 24 | }) 25 | 26 | should(t, "parse body", func() bool { return msg.Body() == "Hello world" }) 27 | } 28 | --------------------------------------------------------------------------------