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.