diff --git a/cmd/server.go b/cmd/server.go index 4ffeb45..fcf59c2 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -30,7 +30,7 @@ var ( statesaveWorker *worker.Worker srv *server.Server certs *tls.Config - extensions []extension.Extension + extensions extension.Extensions ) // serverCmd represents the serve command @@ -178,8 +178,19 @@ func reload() { } func init() { + extensions = append(extensions, + &extension.Message{}, + extension.IQExtensions{ + &extension.Private{}, + &extension.Ping{}, + &extension.Disco{Database: db}, + &extension.Roster{Database: db}, + &extension.ExtensionDiscovery{GetSpaces: func() []string { + return extensions.Spaces() + }}, + }) + RootCmd.AddCommand(serverCmd) serverCmd.Flags().StringVarP(&configPath, "config", "c", "yaja.conf", "Path to configuration file") - extensions = append(extensions, &extension.Message{}, &extension.Roster{Database: db}) } diff --git a/database/main.go b/database/main.go index ba3892d..c093031 100644 --- a/database/main.go +++ b/database/main.go @@ -66,3 +66,18 @@ func (s *State) Authenticate(jid *model.JID, password string) (bool, error) { } return false, nil } + +func (s *State) GetAccount(jid *model.JID) *model.Account { + logger := log.WithField("database", "get") + + if domain, ok := s.Domains[jid.Domain]; ok { + if acc, ok := domain.Accounts[jid.Local]; ok { + return acc + } else { + logger.Debug("account not found") + } + } else { + logger.Debug("domain not found") + } + return nil +} diff --git a/messages/namespaces.go b/messages/namespaces.go index 2ddc0dd..73cb3f7 100644 --- a/messages/namespaces.go +++ b/messages/namespaces.go @@ -16,4 +16,6 @@ const ( NSIQRegister = "jabber:iq:register" NSFeaturesIQRegister = "http://jabber.org/features/iq-register" + + NSDisco = "http://jabber.org/protocol/disco#info" ) diff --git a/model/account.go b/model/account.go index 46117ce..6652b4e 100644 --- a/model/account.go +++ b/model/account.go @@ -29,10 +29,11 @@ func (d *Domain) UpdateAccount(a *Account) error { } type Account struct { - Local string `json:"-"` - Domain *Domain `json:"-"` - Password string `json:"password"` - Roster map[string]*Buddy `json:"roster"` + Local string `json:"-"` + Domain *Domain `json:"-"` + Password string `json:"password"` + Roster map[string]*Buddy `json:"roster"` + Bookmarks map[string]*Bookmark `json:"bookmarks"` } func NewAccount(jid *JID, password string) *Account { diff --git a/model/buddy.go b/model/buddy.go index e321171..905ac25 100644 --- a/model/buddy.go +++ b/model/buddy.go @@ -15,3 +15,6 @@ type Buddy struct { Subscription int `json:"subscription"` Ask int `json:"ask"` } + +type Bookmark struct { +} diff --git a/server/extension/iq.go b/server/extension/iq.go new file mode 100644 index 0000000..b47535b --- /dev/null +++ b/server/extension/iq.go @@ -0,0 +1,58 @@ +package extension + +import ( + "encoding/xml" + + "github.com/genofire/yaja/messages" + "github.com/genofire/yaja/server/utils" +) + +type IQExtensions []IQExtension + +type IQExtension interface { + Extension + Get(*messages.IQ, *utils.Client) bool + Set(*messages.IQ, *utils.Client) bool +} + +func (iex IQExtensions) Spaces() (result []string) { + for _, extension := range iex { + spaces := extension.Spaces() + result = append(result, spaces...) + } + return result +} + +func (iex IQExtensions) Process(element *xml.StartElement, client *utils.Client) bool { + log := client.Log.WithField("extension", "iq") + + // iq encode + var msg messages.IQ + if err := client.In.DecodeElement(&msg, element); err != nil { + return false + } + + log = log.WithField("id", msg.ID) + + // run every extensions + count := 0 + for _, extension := range iex { + switch msg.Type { + case messages.IQTypeGet: + if extension.Get(&msg, client) { + count++ + } + case messages.IQTypeSet: + if extension.Set(&msg, client) { + count++ + } + } + } + + // not extensions found + if count != 1 { + log.Debug(msg.XMLName.Space, " - ", msg.Type, ": ", string(msg.Body)) + } + + return true +} diff --git a/server/extension/iq_disco.go b/server/extension/iq_disco.go new file mode 100644 index 0000000..21791fa --- /dev/null +++ b/server/extension/iq_disco.go @@ -0,0 +1,72 @@ +package extension + +import ( + "encoding/xml" + + "github.com/genofire/yaja/database" + "github.com/genofire/yaja/messages" + "github.com/genofire/yaja/server/utils" +) + +type Disco struct { + IQExtension + Database *database.State +} + +func (r *Disco) Spaces() []string { return []string{} } + +func (r *Disco) Get(msg *messages.IQ, client *utils.Client) bool { + log := client.Log.WithField("extension", "disco-item").WithField("id", msg.ID) + + // query encode + type query struct { + XMLName xml.Name `xml:"http://jabber.org/protocol/disco#items query"` + Body []byte `xml:",innerxml"` + } + q := &query{} + err := xml.Unmarshal(msg.Body, q) + if err != nil { + return false + } + + // answer query + q.Body = []byte{} + + // build answer body + type item struct { + XMLName xml.Name `xml:"item"` + JID string `xml:"jid,attr"` + } + if acc := r.Database.GetAccount(client.JID); acc != nil { + for jid, _ := range acc.Bookmarks { + itemByte, err := xml.Marshal(&item{ + JID: jid, + }) + if err != nil { + log.Warn(err) + continue + } + q.Body = append(q.Body, itemByte...) + } + } + + // decode query + queryByte, err := xml.Marshal(q) + if err != nil { + log.Warn(err) + return false + } + + // reply + client.Out.Encode(&messages.IQ{ + Type: messages.IQTypeResult, + To: client.JID.String(), + From: client.JID.Domain, + ID: msg.ID, + Body: queryByte, + }) + + log.Debug("send") + + return true +} diff --git a/server/extension/iq_discovery.go b/server/extension/iq_discovery.go new file mode 100644 index 0000000..d79f497 --- /dev/null +++ b/server/extension/iq_discovery.go @@ -0,0 +1,72 @@ +package extension + +import ( + "encoding/xml" + + "github.com/genofire/yaja/messages" + "github.com/genofire/yaja/server/utils" +) + +type ExtensionDiscovery struct { + IQExtension + GetSpaces func() []string +} + +func (ex *ExtensionDiscovery) Spaces() []string { return []string{} } + +func (ex *ExtensionDiscovery) Get(msg *messages.IQ, client *utils.Client) bool { + log := client.Log.WithField("extension", "roster").WithField("id", msg.ID) + + // query encode + type query struct { + XMLName xml.Name `xml:"http://jabber.org/protocol/disco#info query"` + Body []byte `xml:",innerxml"` + } + q := &query{} + err := xml.Unmarshal(msg.Body, q) + if err != nil { + return false + } + + // answer query + q.Body = []byte{} + + // build answer body + type feature struct { + XMLName xml.Name `xml:"feature"` + Var string `xml:"var,attr"` + } + for _, namespace := range ex.GetSpaces() { + if namespace == "" { + continue + } + itemByte, err := xml.Marshal(&feature{ + Var: namespace, + }) + if err != nil { + log.Warn(err) + continue + } + q.Body = append(q.Body, itemByte...) + } + + // decode query + queryByte, err := xml.Marshal(q) + if err != nil { + log.Warn(err) + return false + } + + // replay + client.Out.Encode(&messages.IQ{ + Type: messages.IQTypeResult, + To: client.JID.String(), + From: client.JID.Domain, + ID: msg.ID, + Body: queryByte, + }) + + log.Debug("send") + + return true +} diff --git a/server/extension/iq_ping.go b/server/extension/iq_ping.go new file mode 100644 index 0000000..9d3e86f --- /dev/null +++ b/server/extension/iq_ping.go @@ -0,0 +1,40 @@ +package extension + +import ( + "encoding/xml" + + "github.com/genofire/yaja/messages" + "github.com/genofire/yaja/server/utils" +) + +type Ping struct { + IQExtension +} + +func (p *Ping) Spaces() []string { return []string{"urn:xmpp:ping"} } + +func (p *Ping) Get(msg *messages.IQ, client *utils.Client) bool { + log := client.Log.WithField("extension", "ping").WithField("id", msg.ID) + + // ping encode + type ping struct { + XMLName xml.Name `xml:"urn:xmpp:ping ping"` + } + pq := &ping{} + err := xml.Unmarshal(msg.Body, pq) + if err != nil { + return false + } + + // reply + client.Out.Encode(&messages.IQ{ + Type: messages.IQTypeResult, + To: client.JID.String(), + From: client.JID.Domain, + ID: msg.ID, + }) + + log.Debug("send") + + return true +} diff --git a/server/extension/iq_private.go b/server/extension/iq_private.go new file mode 100644 index 0000000..2be09f7 --- /dev/null +++ b/server/extension/iq_private.go @@ -0,0 +1,52 @@ +package extension + +import ( + "encoding/xml" + + "github.com/genofire/yaja/messages" + "github.com/genofire/yaja/server/utils" +) + +type Private struct { + IQExtension +} + +type privateQuery struct { + XMLName xml.Name `xml:"jabber:iq:private query"` + Body []byte `xml:",innerxml"` +} + +type ioPrivateExtension interface { + Handle(*messages.IQ, *privateQuery, *utils.Client) bool +} + +func (p *Private) Spaces() []string { return []string{"jabber:iq:private"} } + +func (p *Private) Get(msg *messages.IQ, client *utils.Client) bool { + log := client.Log.WithField("extension", "private").WithField("id", msg.ID) + + // query encode + q := &privateQuery{} + err := xml.Unmarshal(msg.Body, q) + if err != nil { + return false + } + + // run every extensions + count := 0 + for _, e := range []ioPrivateExtension{ + &PrivateMetacontact{}, + &PrivateRoster{}, + } { + if e.Handle(msg, q, client) { + count++ + } + } + + // not extensions found + if count != 1 { + log.Debug(msg.XMLName.Space, " - ", msg.Type, ": ", string(q.Body)) + } + + return true +} diff --git a/server/extension/iq_private_metacontacts.go b/server/extension/iq_private_metacontacts.go new file mode 100644 index 0000000..9a621e2 --- /dev/null +++ b/server/extension/iq_private_metacontacts.go @@ -0,0 +1,51 @@ +package extension + +import ( + "encoding/xml" + + "github.com/genofire/yaja/messages" + "github.com/genofire/yaja/server/utils" +) + +type PrivateMetacontact struct { + ioPrivateExtension +} + +func (p *PrivateMetacontact) Handle(msg *messages.IQ, q *privateQuery, client *utils.Client) bool { + log := client.Log.WithField("extension", "private-metacontact").WithField("id", msg.ID) + + // storage encode + type storage struct { + XMLName xml.Name `xml:"storage:metacontacts storage"` + } + s := &storage{} + err := xml.Unmarshal(q.Body, s) + if err != nil { + return false + } + /* + TODO full implement XEP-0209 + https://xmpp.org/extensions/xep-0209.html + */ + + queryByte, err := xml.Marshal(&privateQuery{ + Body: q.Body, + }) + if err != nil { + log.Warn(err) + return true + } + + // reply + client.Out.Encode(&messages.IQ{ + Type: messages.IQTypeResult, + To: client.JID.String(), + From: client.JID.Domain, + ID: msg.ID, + Body: queryByte, + }) + + log.Debug("send") + + return true +} diff --git a/server/extension/iq_private_roster.go b/server/extension/iq_private_roster.go new file mode 100644 index 0000000..7702ebe --- /dev/null +++ b/server/extension/iq_private_roster.go @@ -0,0 +1,55 @@ +package extension + +import ( + "encoding/xml" + + "github.com/genofire/yaja/messages" + "github.com/genofire/yaja/server/utils" +) + +type PrivateRoster struct { + ioPrivateExtension +} + +func (p *PrivateRoster) Handle(msg *messages.IQ, q *privateQuery, client *utils.Client) bool { + log := client.Log.WithField("extension", "private").WithField("id", msg.ID) + + // roster encode + type roster struct { + XMLName xml.Name `xml:"roster:delimiter roster"` + Body []byte `xml:",innerxml"` + } + r := &roster{} + err := xml.Unmarshal(q.Body, r) + if err != nil { + return false + } + + rosterByte, err := xml.Marshal(&roster{ + Body: []byte("::"), + }) + if err != nil { + log.Warn(err) + return true + } + queryByte, err := xml.Marshal(&privateQuery{ + Body: rosterByte, + }) + if err != nil { + log.Warn(err) + return true + } + + // reply + client.Out.Encode(&messages.IQ{ + Type: messages.IQTypeResult, + To: client.JID.String(), + From: client.JID.Domain, + ID: msg.ID, + Body: queryByte, + }) + + log.Debug("send") + + return true +} diff --git a/server/extension/iq_roster.go b/server/extension/iq_roster.go new file mode 100644 index 0000000..03deb6a --- /dev/null +++ b/server/extension/iq_roster.go @@ -0,0 +1,74 @@ +package extension + +import ( + "encoding/xml" + + "github.com/genofire/yaja/database" + "github.com/genofire/yaja/messages" + "github.com/genofire/yaja/server/utils" +) + +type Roster struct { + IQExtension + Database *database.State +} + +func (r *Roster) Spaces() []string { return []string{"jabber:iq:roster"} } + +func (r *Roster) Get(msg *messages.IQ, client *utils.Client) bool { + log := client.Log.WithField("extension", "roster").WithField("id", msg.ID) + + // query encode + type query struct { + XMLName xml.Name `xml:"jabber:iq:roster query"` + Version string `xml:"ver,attr"` + Body []byte `xml:",innerxml"` + } + q := &query{} + err := xml.Unmarshal(msg.Body, q) + if err != nil { + return false + } + + // answer query + q.Body = []byte{} + q.Version = "1" + + // build answer body + type item struct { + XMLName xml.Name `xml:"item"` + JID string `xml:"jid,attr"` + } + if acc := r.Database.GetAccount(client.JID); acc != nil { + for jid, _ := range acc.Roster { + itemByte, err := xml.Marshal(&item{ + JID: jid, + }) + if err != nil { + log.Warn(err) + continue + } + q.Body = append(q.Body, itemByte...) + } + } + + // decode query + queryByte, err := xml.Marshal(q) + if err != nil { + log.Warn(err) + return false + } + + // reply + client.Out.Encode(&messages.IQ{ + Type: messages.IQTypeResult, + To: client.JID.String(), + From: client.JID.Domain, + ID: msg.ID, + Body: queryByte, + }) + + log.Debug("send") + + return true +} diff --git a/server/extension/main.go b/server/extension/main.go index d6e42a1..cbdab2e 100644 --- a/server/extension/main.go +++ b/server/extension/main.go @@ -6,6 +6,33 @@ import ( "github.com/genofire/yaja/server/utils" ) +type Extensions []Extension + type Extension interface { Process(*xml.StartElement, *utils.Client) bool + Spaces() []string +} + +func (ex Extensions) Spaces() (result []string) { + for _, extension := range ex { + result = append(result, extension.Spaces()...) + } + return result +} + +func (ex Extensions) Process(element *xml.StartElement, client *utils.Client) { + log := client.Log.WithField("extension", "all") + + // run every extensions + count := 0 + for _, extension := range ex { + if extension.Process(element, client) { + count++ + } + } + + // not extensions found + if count != 1 { + log.Debug(element) + } } diff --git a/server/extension/message.go b/server/extension/message.go index bc8a7ce..4e35e79 100644 --- a/server/extension/message.go +++ b/server/extension/message.go @@ -10,6 +10,8 @@ type Message struct { Extension } +func (m *Message) Spaces() []string { return []string{} } + func (m *Message) Process(element *xml.StartElement, client *utils.Client) bool { return false } diff --git a/server/extension/roster.go b/server/extension/roster.go deleted file mode 100644 index 593f7eb..0000000 --- a/server/extension/roster.go +++ /dev/null @@ -1,27 +0,0 @@ -package extension - -import ( - "encoding/xml" - - "github.com/genofire/yaja/database" - "github.com/genofire/yaja/messages" - "github.com/genofire/yaja/server/utils" -) - -type Roster struct { - Extension - Database *database.State -} - -func (r *Roster) Process(element *xml.StartElement, client *utils.Client) bool { - var msg messages.IQ - if err := client.In.DecodeElement(&msg, element); err != nil { - client.Log.Warn("is no iq: ", err) - return false - } - if msg.Type != messages.IQTypeGet { - client.Log.Warn("is no get iq") - return false - } - return true -} diff --git a/server/server.go b/server/server.go index 5c5d686..0580770 100644 --- a/server/server.go +++ b/server/server.go @@ -22,7 +22,7 @@ type Server struct { LoggingClient log.Level RegisterEnable bool RegisterDomains []string - Extensions []extension.Extension + Extensions extension.Extensions } func (srv *Server) Start() { @@ -74,7 +74,7 @@ func (srv *Server) handleServer(conn net.Conn) { func (srv *Server) handleClient(conn net.Conn) { log.Info("new client connection:", conn.RemoteAddr()) client := utils.NewClient(conn, srv.LoggingClient) - state := toclient.ConnectionStartup(srv.Database, srv.TLSConfig, srv.TLSManager, srv.DomainRegisterAllowed) + state := toclient.ConnectionStartup(srv.Database, srv.TLSConfig, srv.TLSManager, srv.DomainRegisterAllowed, srv.Extensions) for { state, client = state.Process(client) diff --git a/server/toclient/connect.go b/server/toclient/connect.go index 2be6ec3..f8deaa4 100644 --- a/server/toclient/connect.go +++ b/server/toclient/connect.go @@ -16,8 +16,8 @@ import ( ) // ConnectionStartup return steps through TCP TLS state -func ConnectionStartup(db *database.State, tlsconfig *tls.Config, tlsmgmt *autocert.Manager, registerAllowed utils.DomainRegisterAllowed) state.State { - receiving := &ReceivingClient{} +func ConnectionStartup(db *database.State, tlsconfig *tls.Config, tlsmgmt *autocert.Manager, registerAllowed utils.DomainRegisterAllowed, extensions []extension.Extension) state.State { + receiving := &ReceivingClient{Extensions: extensions} sending := &SendingClient{Next: receiving} authedstream := &AuthedStream{Next: sending} authedstart := &AuthedStart{Next: authedstream} @@ -213,6 +213,8 @@ func (state *AuthedStream) Process(client *utils.Client) (state.State, *utils.Cl client.Log = client.Log.WithField("jid", client.JID.Full()) client.Out.Encode(&messages.IQ{ Type: messages.IQTypeResult, + To: client.JID.String(), + From: client.JID.Domain, ID: msg.ID, Body: []byte(fmt.Sprintf( ` @@ -223,50 +225,3 @@ func (state *AuthedStream) Process(client *utils.Client) (state.State, *utils.Cl return state.Next, client } - -// SendingClient state -type SendingClient struct { - Next state.State -} - -// Process messages -func (state *SendingClient) Process(client *utils.Client) (state.State, *utils.Client) { - client.Log = client.Log.WithField("state", "normal") - client.Log.Debug("sending") - // sending - go func() { - select { - case msg := <-client.Messages: - err := client.Out.Encode(msg) - client.Log.Info(err) - case <-client.OnClose(): - return - } - }() - client.Log.Debug("receiving") - return state.Next, client -} - -// ReceivingClient state -type ReceivingClient struct { - Extensions []extension.Extension -} - -// Process messages -func (state *ReceivingClient) Process(client *utils.Client) (state.State, *utils.Client) { - element, err := client.Read() - if err != nil { - client.Log.Warn("unable to read: ", err) - return nil, client - } - count := 0 - for _, extension := range state.Extensions { - if extension.Process(element, client) { - count++ - } - } - if count != 1 { - client.Log.WithField("extension", count).Debug(element) - } - return state, client -} diff --git a/server/toclient/normal.go b/server/toclient/normal.go new file mode 100644 index 0000000..d971657 --- /dev/null +++ b/server/toclient/normal.go @@ -0,0 +1,46 @@ +package toclient + +import ( + "github.com/genofire/yaja/server/extension" + "github.com/genofire/yaja/server/state" + "github.com/genofire/yaja/server/utils" +) + +// SendingClient state +type SendingClient struct { + Next state.State +} + +// Process messages +func (state *SendingClient) Process(client *utils.Client) (state.State, *utils.Client) { + client.Log = client.Log.WithField("state", "normal") + client.Log.Debug("sending") + // sending + go func() { + select { + case msg := <-client.Messages: + err := client.Out.Encode(msg) + client.Log.Info(err) + case <-client.OnClose(): + return + } + }() + client.Log.Debug("receiving") + return state.Next, client +} + +// ReceivingClient state +type ReceivingClient struct { + Extensions extension.Extensions +} + +// Process messages +func (state *ReceivingClient) Process(client *utils.Client) (state.State, *utils.Client) { + element, err := client.Read() + if err != nil { + client.Log.Warn("unable to read: ", err) + return nil, client + } + state.Extensions.Process(element, client) + return state, client +} diff --git a/server/toclient/register.go b/server/toclient/register.go index 4f381ed..585a6f6 100644 --- a/server/toclient/register.go +++ b/server/toclient/register.go @@ -53,6 +53,8 @@ func (state *RegisterFormRequest) Process(client *utils.Client) (state.State, *u } client.Out.Encode(&messages.IQ{ Type: messages.IQTypeResult, + To: client.JID.String(), + From: client.JID.Domain, ID: msg.ID, Body: []byte(fmt.Sprintf(` Choose a username and password for use with this service. @@ -118,6 +120,8 @@ func (state *RegisterRequest) Process(client *utils.Client) (state.State, *utils if err != nil { client.Out.Encode(&messages.IQ{ Type: messages.IQTypeResult, + To: client.JID.String(), + From: client.JID.Domain, ID: msg.ID, Body: []byte(fmt.Sprintf(` %s @@ -137,6 +141,8 @@ func (state *RegisterRequest) Process(client *utils.Client) (state.State, *utils } client.Out.Encode(&messages.IQ{ Type: messages.IQTypeResult, + To: client.JID.String(), + From: client.JID.Domain, ID: msg.ID, })