diff --git a/src/xmpp/ad-hoc.go b/src/xmpp/ad-hoc.go new file mode 100644 index 0000000..95e1f00 --- /dev/null +++ b/src/xmpp/ad-hoc.go @@ -0,0 +1,69 @@ +package xmpp + +import ( + "encoding/xml" +) + +const ( + NodeAdHocCommand = "http://jabber.org/protocol/commands" + + ActionAdHocExecute = "execute" + ActionAdHocNext = "next" + ActionAdHocCancel = "cancel" + + StatusAdHocExecute = "executing" + StatusAdHocCompleted = "completed" + StatusAdHocCanceled = "canceled" + + TypeAdHocForm = "form" + TypeAdHocResult = "result" + TypeAdHocSubmit = "submit" + + TypeAdHocListSingle = "list-single" + TypeAdHocListMulti = "list-multi" + + TypeAdHocNoteInfo = "info" + TypeAdHocNoteWarning = "warn" + TypeAdHocNoteError = "error" + + TypeAdHocFieldListMulti = "list-multi" + TypeAdHocFieldListSingle = "list-single" + TypeAdHocFieldTextSingle = "text-single" + TypeAdHocFieldJidSingle = "jid-single" + TypeAdHocFieldTextPrivate = "text-private" +) + +type AdHocCommand struct { + XMLName xml.Name `xml:"http://jabber.org/protocol/commands command"` + Node string `xml:"node,attr"` + Action string `xml:"action,attr"` + SessionID string `xml:"sessionid,attr"` + Status string `xml:"status,attr"` + XForm AdHocXForm `xml:"x"` + Note AdHocNote `xml:"note,omitempty"` +} + +type AdHocXForm struct { + XMLName xml.Name `xml:"jabber:x:data x"` + Type string `xml:"type,attr"` + Title string `xml:"title"` + Instructions string `xml:"instructions"` + Fields []AdHocField `xml:"field"` +} + +type AdHocField struct { + Var string `xml:"var,attr"` + Label string `xml:"label,attr"` + Type string `xml:"type,attr"` + Options []AdHocFieldOption `xml:"option"` + Value string `xml:"value,omitempty"` +} + +type AdHocFieldOption struct { + Value string `xml:"value"` +} + +type AdHocNote struct { + Type string `xml:"type,attr"` + Value string `xml:",innerxml"` +} diff --git a/src/xmpp/chatStateNotification.go b/src/xmpp/chatStateNotification.go new file mode 100644 index 0000000..e2b80b0 --- /dev/null +++ b/src/xmpp/chatStateNotification.go @@ -0,0 +1,27 @@ +package xmpp + +import ( + "encoding/xml" +) + +const ( + NSChatStatesNotification = "http://jabber.org/protocol/chatstates" +) + +// XEP-0085: Chat States Notification + +type Active struct { + XMLName xml.Name `xml:"http://jabber.org/protocol/chatstates active"` +} +type Composing struct { + XMLName xml.Name `xml:"http://jabber.org/protocol/chatstates composing"` +} +type Paused struct { + XMLName xml.Name `xml:"http://jabber.org/protocol/chatstates paused"` +} +type Inactive struct { + XMLName xml.Name `xml:"http://jabber.org/protocol/chatstates inactive"` +} +type Gone struct { + XMLName xml.Name `xml:"http://jabber.org/protocol/chatstates gone"` +} diff --git a/src/xmpp/client.go b/src/xmpp/client.go index 34941d8..a53d920 100644 --- a/src/xmpp/client.go +++ b/src/xmpp/client.go @@ -115,7 +115,7 @@ func startTLS(stream *Stream, config *ClientConfig) error { return err } - tlsConfig := tls.Config{InsecureSkipVerify: config.InsecureSkipVerify} + tlsConfig := tls.Config{InsecureSkipVerify: config.InsecureSkipVerify, ServerName: stream.config.ConnectionDomain} return stream.UpgradeTLS(&tlsConfig) } diff --git a/src/xmpp/disco.go b/src/xmpp/disco.go index d9dea7f..32de1f7 100644 --- a/src/xmpp/disco.go +++ b/src/xmpp/disco.go @@ -6,8 +6,8 @@ import ( ) const ( - nsDiscoInfo = "http://jabber.org/protocol/disco#info" - nsDiscoItems = "http://jabber.org/protocol/disco#items" + NSDiscoInfo = "http://jabber.org/protocol/disco#info" + NSDiscoItems = "http://jabber.org/protocol/disco#items" ) // Service Discovery (XEP-0030) protocol. "Wraps" XMPP instance to provide a @@ -19,6 +19,7 @@ type Disco struct { // Iq get/result payload for "info" requests. type DiscoInfo struct { XMLName xml.Name `xml:"http://jabber.org/protocol/disco#info query"` + Node string `xml:"node,attr"` Identity []DiscoIdentity `xml:"identity"` Feature []DiscoFeature `xml:"feature"` } @@ -38,6 +39,7 @@ type DiscoFeature struct { // Iq get/result payload for "items" requests. type DiscoItems struct { XMLName xml.Name `xml:"http://jabber.org/protocol/disco#items query"` + Node string `xml:"node,attr"` Item []DiscoItem `xml:"item"` } @@ -49,13 +51,13 @@ type DiscoItem struct { } // Request information about the service identified by 'to'. -func (disco *Disco) Info(to string, from string) (*DiscoInfo, error) { +func (disco *Disco) Info(to, from string) (*DiscoInfo, error) { if from == "" { from = disco.XMPP.JID.Full() } - req := &Iq{Id: UUID4(), Type: "get", To: to, From: from} + req := &Iq{Id: UUID4(), Type: IQTypeGet, To: to, From: from} req.PayloadEncode(&DiscoInfo{}) resp, err := disco.XMPP.SendRecv(req) @@ -72,14 +74,14 @@ func (disco *Disco) Info(to string, from string) (*DiscoInfo, error) { } // Request items in the service identified by 'to'. -func (disco *Disco) Items(to string, from string) (*DiscoItems, error) { +func (disco *Disco) Items(to, from, node string) (*DiscoItems, error) { if from == "" { from = disco.XMPP.JID.Full() } - req := &Iq{Id: UUID4(), Type: "get", To: to, From: from} - req.PayloadEncode(&DiscoItems{}) + req := &Iq{Id: UUID4(), Type: IQTypeGet, To: to, From: from} + req.PayloadEncode(&DiscoItems{Node: node}) resp, err := disco.XMPP.SendRecv(req) if err != nil { @@ -94,7 +96,7 @@ func (disco *Disco) Items(to string, from string) (*DiscoItems, error) { return items, err } -var discoNamespacePrefix = strings.Split(nsDiscoInfo, "#")[0] +var discoNamespacePrefix = strings.Split(NSDiscoInfo, "#")[0] // Matcher instance to match stanzas with a disco payload. var DiscoPayloadMatcher = MatcherFunc( diff --git a/src/xmpp/dns.go b/src/xmpp/dns.go index 4b1d136..78397a4 100644 --- a/src/xmpp/dns.go +++ b/src/xmpp/dns.go @@ -3,6 +3,7 @@ package xmpp import ( "fmt" "net" + "strings" ) const ( @@ -27,7 +28,8 @@ func HomeServerAddrs(jid JID) (addr []string, err error) { // Build list of "host:port" strings. for _, a := range addrs { - addr = append(addr, fmt.Sprintf("%s:%d", a.Target, a.Port)) + target := strings.TrimRight(a.Target, ".") + addr = append(addr, fmt.Sprintf("%s:%d", target, a.Port)) } return } diff --git a/src/xmpp/httpAuth.go b/src/xmpp/httpAuth.go new file mode 100644 index 0000000..0f9b21f --- /dev/null +++ b/src/xmpp/httpAuth.go @@ -0,0 +1,17 @@ +package xmpp + +import ( + "encoding/xml" +) + +const ( + NSHTTPAuth = "http://jabber.org/protocol/http-auth" +) + +// XEP-0070: Verifying HTTP Requests via XMPP +type Confirm struct { + XMLName xml.Name `xml:"http://jabber.org/protocol/http-auth confirm"` + Id string `xml:"id,attr"` + Method string `xml:"method,attr"` + URL string `xml:"url,attr"` +} diff --git a/src/xmpp/ping.go b/src/xmpp/ping.go new file mode 100644 index 0000000..a5f3180 --- /dev/null +++ b/src/xmpp/ping.go @@ -0,0 +1,13 @@ +package xmpp + +import ( + "encoding/xml" +) + +const ( + NSPing = "urn:xmpp:ping" +) + +type Ping struct { + XMLName xml.Name `xml:"urn:xmpp:ping ping"` +} diff --git a/src/xmpp/register.go b/src/xmpp/register.go new file mode 100644 index 0000000..6018d2d --- /dev/null +++ b/src/xmpp/register.go @@ -0,0 +1,29 @@ +package xmpp + +import ( + "encoding/xml" +) + +const ( + NSRegister = "jabber:iq:register" +) + +// XEP-0077: In-Band Registration + +type RegisterQuery struct { + XMLName xml.Name `xml:"jabber:iq:register query"` + Instructions string `xml:"instructions"` + Username string `xml:"username"` + Password string `xml:"password"` + XForm AdHocXForm `xml:"x"` + Registered *RegisterRegistered `xmp:"registered"` + Remove *RegisterRemove `xmp:"remove"` +} + +type RegisterRegistered struct { + XMLName xml.Name `xml:"registered"` +} + +type RegisterRemove struct { + XMLName xml.Name `xml:"remove"` +} diff --git a/src/xmpp/remoteRosterManager.go b/src/xmpp/remoteRosterManager.go new file mode 100644 index 0000000..4764680 --- /dev/null +++ b/src/xmpp/remoteRosterManager.go @@ -0,0 +1,21 @@ +package xmpp + +import ( + "encoding/xml" +) + +const ( + NSRemoteRosterManager = "urn:xmpp:tmp:roster-management:0" + + RemoteRosterManagerTypeRequest = "request" + RemoteRosterManagerTypeAllowed = "allowed" + RemoteRosterManagerTypeRejected = "rejected" +) + +// XEP-0321: Remote Roster Manager + +type RemoteRosterManagerQuery struct { + XMLName xml.Name `xml:"urn:xmpp:tmp:roster-management:0 query"` + Reason string `xml:"reason,attr,omitempty"` + Type string `xml:"type,attr"` +} diff --git a/src/xmpp/roster.go b/src/xmpp/roster.go new file mode 100644 index 0000000..8b9f126 --- /dev/null +++ b/src/xmpp/roster.go @@ -0,0 +1,26 @@ +package xmpp + +import ( + "encoding/xml" +) + +const ( + NSRoster = "jabber:iq:roster" + + RosterSubscriptionBoth = "both" + RosterSubscriptionFrom = "from" + RosterSubscriptionTo = "to" + RosterSubscriptionRemove = "remove" +) + +type RosterQuery struct { + XMLName xml.Name `xml:"jabber:iq:roster query"` + Items []RosterItem `xml:"item"` +} + +type RosterItem struct { + JID string `xml:"jid,attr"` + Name string `xml:"name,attr,omitempty"` + Subscription string `xml:"subscription,attr"` + Groupes []string `xml:"group"` +} diff --git a/src/xmpp/softwareVersion.go b/src/xmpp/softwareVersion.go new file mode 100644 index 0000000..a2fc7d5 --- /dev/null +++ b/src/xmpp/softwareVersion.go @@ -0,0 +1,17 @@ +package xmpp + +import ( + "encoding/xml" +) + +const ( + NSJabberClient = "jabber:iq:version" +) + +// XEP-0092 Software Version +type SoftwareVersion struct { + XMLName xml.Name `xml:"jabber:iq:version query"` + Name string `xml:"name,omitempty"` + Version string `xml:"version,omitempty"` + OS string `xml:"os,omitempty"` +} diff --git a/src/xmpp/stanza.go b/src/xmpp/stanza.go index 584793d..4d48bf1 100644 --- a/src/xmpp/stanza.go +++ b/src/xmpp/stanza.go @@ -6,6 +6,17 @@ import ( "fmt" ) +const ( + IQTypeGet = "get" + IQTypeSet = "set" + IQTypeResult = "result" + IQTypeError = "error" + + MessageTypeNormal = "normal" + MessageTypeChat = "chat" + MessageTypeError = "error" +) + // XMPP stanza. type Iq struct { XMLName xml.Name `xml:"iq"` @@ -56,13 +67,29 @@ func (iq *Iq) Response(type_ string) *Iq { // XMPP stanza. type Message struct { - XMLName xml.Name `xml:"message"` - Id string `xml:"id,attr,omitempty"` - Type string `xml:"type,attr,omitempty"` - To string `xml:"to,attr,omitempty"` - From string `xml:"from,attr,omitempty"` - Subject string `xml:"subject,omitempty"` - Body string `xml:"body,omitempty"` + XMLName xml.Name `xml:"message"` + Id string `xml:"id,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` + To string `xml:"to,attr,omitempty"` + From string `xml:"from,attr,omitempty"` + Subject string `xml:"subject,omitempty"` + Body []MessageBody `xml:"body,omitempty"` + Thread string `xml:"thread,omitempty"` + Error *Error `xml:"error"` + Lang string `xml:"xml:lang,attr,omitempty"` + + Confirm *Confirm `xml:"confirm"` // XEP-0070 + + Active *Active `xml:"active"` // XEP-0085 + Composing *Composing `xml:"composing"` // XEP-0085 + Paused *Paused `xml:"paused"` // XEP-0085 + Inactive *Inactive `xml:"inactive"` // XEP-0085 + Gone *Gone `xml:"gone"` // XEP-0085 +} + +type MessageBody struct { + Lang string `xml:"xml:lang,attr,omitempty"` + Value string `xml:",chardata"` } // XMPP stanza. @@ -72,12 +99,17 @@ type Presence struct { Type string `xml:"type,attr,omitempty"` To string `xml:"to,attr,omitempty"` From string `xml:"from,attr,omitempty"` + Show string `xml:"show"` // away, chat, dnd, xa + Status string `xml:"status"` // sb []clientText + Photo string `xml:"photo,omitempty"` // Avatar + Nick string `xml:"nick,omitempty"` // Nickname } // XMPP . May occur as a top-level stanza or embedded in another // stanza, e.g. an . type Error struct { XMLName xml.Name `xml:"error"` + Code string `xml:"code,attr,omitempty"` Type string `xml:"type,attr"` Payload string `xml:",innerxml"` } @@ -116,6 +148,12 @@ func NewError(errorType string, condition ErrorCondition, text string) *Error { return &Error{Type: errorType, Payload: string(buf.Bytes())} } +func NewErrorWithCode(code, errorType string, condition ErrorCondition, text string) *Error { + err := NewError(errorType, condition, text) + err.Code = code + return err +} + // Return the error text from the payload, or "" if not present. func (e Error) Text() string { dec := xml.NewDecoder(bytes.NewBufferString(e.Payload)) @@ -151,5 +189,11 @@ type ErrorCondition xml.Name // Stanza errors. var ( - FeatureNotImplemented = ErrorCondition{nsErrorStanzas, "feature-not-implemented"} + ErrorFeatureNotImplemented = ErrorCondition{nsErrorStanzas, "feature-not-implemented"} + ErrorRemoteServerNotFound = ErrorCondition{nsErrorStanzas, "remote-server-not-found"} + ErrorServiceUnavailable = ErrorCondition{nsErrorStanzas, "service-unavailable"} + ErrorNotAuthorized = ErrorCondition{nsErrorStanzas, "not-authorized"} + ErrorConflict = ErrorCondition{nsErrorStanzas, "conflict"} + ErrorNotAcceptable = ErrorCondition{nsErrorStanzas, "not-acceptable"} + ErrorForbidden = ErrorCondition{nsErrorStanzas, "forbidden"} ) diff --git a/src/xmpp/stream.go b/src/xmpp/stream.go index 0919ed3..d2dc777 100644 --- a/src/xmpp/stream.go +++ b/src/xmpp/stream.go @@ -7,6 +7,7 @@ import ( "io" "log" "net" + "strings" ) // Stream configuration. @@ -16,6 +17,9 @@ type StreamConfig struct { // are either sent to the server or delivered to the application. It also // causes incoming stanzas to be XML-parsed a second time. LogStanzas bool + + // The dommain connection for certificate validation. + ConnectionDomain string } type Stream struct { @@ -42,6 +46,9 @@ func NewStream(addr string, config *StreamConfig) (*Stream, error) { } stream := &Stream{conn: conn, dec: xml.NewDecoder(conn), config: config} + if config.ConnectionDomain == "" { + config.ConnectionDomain = strings.SplitN(addr, ":", 2)[0] + } if err := stream.send([]byte("")); err != nil { return nil, err @@ -163,6 +170,13 @@ func nextStartElement(dec *xml.Decoder) (*xml.StartElement, error) { } switch e := t.(type) { case xml.StartElement: + for i, _ := range e.Attr { + // Replace URL namespace to xml in order to avoid error on Unmarshal + // It's quite ugly, but working for now + if e.Attr[i].Name.Space == "http://www.w3.org/XML/1998/namespace" { + e.Attr[i].Name.Space = "xml" + } + } return &e, nil case xml.EndElement: log.Printf("EOF due to %s\n", e.Name) diff --git a/src/xmpp/uuid.go b/src/xmpp/uuid.go index 14d866f..a18e661 100644 --- a/src/xmpp/uuid.go +++ b/src/xmpp/uuid.go @@ -5,6 +5,10 @@ import ( "fmt" ) +const ( + dictionary = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" +) + // Generate a UUID4. func UUID4() string { uuid := make([]byte, 16) @@ -15,3 +19,14 @@ func UUID4() string { uuid[8] = (uuid[8] &^ 0x40) | 0x80 return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:]) } + +func SessionID() string { + var bytes = make([]byte, 15) + if _, err := rand.Read(bytes); err != nil { + panic(err) + } + for k, v := range bytes { + bytes[k] = dictionary[v%byte(len(dictionary))] + } + return string(bytes) +} diff --git a/src/xmpp/vcard.go b/src/xmpp/vcard.go new file mode 100644 index 0000000..0fe9175 --- /dev/null +++ b/src/xmpp/vcard.go @@ -0,0 +1,15 @@ +package xmpp + +import ( + "encoding/xml" +) + +const ( + NSVCardTemp = "vcard-temp" +) + +// XEP-0054 vCard +type VCard struct { + XMLName xml.Name `xml:"vcard-temp vCard"` + // TODO Must complete truct +} diff --git a/src/xmpp/xmpp.go b/src/xmpp/xmpp.go index 3137897..0da7d81 100644 --- a/src/xmpp/xmpp.go +++ b/src/xmpp/xmpp.go @@ -169,12 +169,17 @@ func (x *XMPP) sender() { // Close the stream. Note: relies on common element name for all types of // XMPP connection. - x.stream.SendEnd(&xml.EndElement{xml.Name{"stream", "stream"}}) + log.Println("Close XMPP stream") + x.Close() } func (x *XMPP) receiver() { - defer close(x.In) + defer func() { + log.Println("Close XMPP receiver") + x.Close() + close(x.In) + }() for { start, err := x.stream.Next() @@ -194,12 +199,12 @@ func (x *XMPP) receiver() { case "presence": v = &Presence{} default: - log.Fatal("Unexected element: %T %v", start, start) + log.Println("Error. Unexected element: %T %v", start, start) } err = x.stream.Decode(v, start) if err != nil { - log.Fatal(err) + log.Println("Error. Failed to decode element. ", err) } filtered := false @@ -216,4 +221,9 @@ func (x *XMPP) receiver() { } } +func (x *XMPP) Close() { + log.Println("Close XMPP") + x.stream.SendEnd(&xml.EndElement{xml.Name{"stream", "stream"}}) +} + // BUG(matt): Filter channels are not closed when the stream is closed.