diff --git a/.gitignore b/.gitignore index 024e953..661053d 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ .glide/ tmp +yaja*.conf +!yaja*example.conf diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..3a1eb30 --- /dev/null +++ b/client/client.go @@ -0,0 +1,275 @@ +package client + +import ( + "crypto/md5" + "crypto/rand" + "crypto/tls" + "encoding/base64" + "encoding/xml" + "errors" + "fmt" + "math/big" + "net" + "strings" + + "dev.sum7.eu/genofire/yaja/messages" + "dev.sum7.eu/genofire/yaja/model" + "dev.sum7.eu/genofire/yaja/server/utils" +) + +// Client holds XMPP connection opitons +type Client struct { + conn net.Conn // connection to server + Out *xml.Encoder + In *xml.Decoder + + JID *model.JID +} + +func NewClient(jid model.JID, password string) (*Client, error) { + conn, err := net.Dial("tcp", jid.Domain+":5222") + if err != nil { + return nil, err + } + client := &Client{ + conn: conn, + In: xml.NewDecoder(conn), + Out: xml.NewEncoder(conn), + + JID: &jid, + } + + if err = client.connect(password); err != nil { + client.Close() + return nil, err + } + return client, nil +} + +// Close closes the XMPP connection +func (c *Client) Close() error { + if c.conn != (*tls.Conn)(nil) { + return c.conn.Close() + } + return nil +} + +func (client *Client) Read() (*xml.StartElement, error) { + for { + nextToken, err := client.In.Token() + if err != nil { + return nil, err + } + switch nextToken.(type) { + case xml.StartElement: + element := nextToken.(xml.StartElement) + return &element, nil + } + } +} +func (client *Client) ReadElement(p interface{}) error { + element, err := client.Read() + if err != nil { + return err + } + return client.In.DecodeElement(p, element) +} + +func (client *Client) init() error { + // XMPP-Connection + _, err := fmt.Fprintf(client.conn, "\n"+ + "\n", + model.XMLEscape(client.JID.Domain), messages.NSClient, messages.NSStream) + if err != nil { + return err + } + element, err := client.Read() + if err != nil { + return err + } + if element.Name.Space != messages.NSStream || element.Name.Local != "stream" { + return errors.New("is not stream") + } + return nil +} +func (client *Client) connect(password string) error { + if err := client.init(); err != nil { + return err + } + var f messages.StreamFeatures + if err := client.ReadElement(&f); err != nil { + return err + } + if err := client.Out.Encode(&messages.TLSStartTLS{}); err != nil { + return err + } + + var p messages.TLSProceed + if err := client.ReadElement(&p); err != nil { + return err + } + // Change tcp to tls + tlsconn := tls.Client(client.conn, &tls.Config{ + ServerName: client.JID.Domain, + }) + client.conn = tlsconn + client.In = xml.NewDecoder(client.conn) + client.Out = xml.NewEncoder(client.conn) + + if err := tlsconn.Handshake(); err != nil { + return err + } + if err := tlsconn.VerifyHostname(client.JID.Domain); err != nil { + return err + } + if err := client.init(); err != nil { + return err + } + //auth: + if err := client.ReadElement(&f); err != nil { + return err + } + mechanism := "" + for _, m := range f.Mechanisms.Mechanism { + if m == "PLAIN" { + mechanism = m + // Plain authentication: send base64-encoded \x00 user \x00 password. + raw := "\x00" + client.JID.Local + "\x00" + password + enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw))) + base64.StdEncoding.Encode(enc, []byte(raw)) + fmt.Fprintf(client.conn, "%s\n", messages.NSSASL, enc) + break + } + if m == "DIGEST-MD5" { + mechanism = m + // Digest-MD5 authentication + fmt.Fprintf(client.conn, "\n", messages.NSSASL) + var ch string + if err := client.ReadElement(&ch); err != nil { + return err + } + b, err := base64.StdEncoding.DecodeString(string(ch)) + if err != nil { + return err + } + tokens := map[string]string{} + for _, token := range strings.Split(string(b), ",") { + kv := strings.SplitN(strings.TrimSpace(token), "=", 2) + if len(kv) == 2 { + if kv[1][0] == '"' && kv[1][len(kv[1])-1] == '"' { + kv[1] = kv[1][1 : len(kv[1])-1] + } + tokens[kv[0]] = kv[1] + } + } + realm, _ := tokens["realm"] + nonce, _ := tokens["nonce"] + qop, _ := tokens["qop"] + charset, _ := tokens["charset"] + cnonceStr := cnonce() + digestURI := "xmpp/" + client.JID.Domain + nonceCount := fmt.Sprintf("%08x", 1) + digest := saslDigestResponse(client.JID.Local, realm, password, nonce, cnonceStr, "AUTHENTICATE", digestURI, nonceCount) + message := "username=\"" + client.JID.Local + "\", realm=\"" + realm + "\", nonce=\"" + nonce + "\", cnonce=\"" + cnonceStr + + "\", nc=" + nonceCount + ", qop=" + qop + ", digest-uri=\"" + digestURI + "\", response=" + digest + ", charset=" + charset + + fmt.Fprintf(client.conn, "%s\n", messages.NSSASL, base64.StdEncoding.EncodeToString([]byte(message))) + + err = client.ReadElement(&ch) + if err != nil { + return err + } + _, err = base64.StdEncoding.DecodeString(ch) + if err != nil { + return err + } + fmt.Fprintf(client.conn, "\n", messages.NSSASL) + break + } + } + if mechanism == "" { + return fmt.Errorf("PLAIN authentication is not an option: %v", f.Mechanisms.Mechanism) + } + element, err := client.Read() + if err != nil { + return err + } + if element.Name.Local != "success" { + return errors.New("auth failed: " + element.Name.Local) + } + + err = client.init() + if err != nil { + return err + } + if err := client.ReadElement(&f); err != nil { + return err + } + // bind to resource + var msg string + if client.JID.Resource == "" { + msg = fmt.Sprintf("", messages.NSBind) + } else { + msg = fmt.Sprintf( + ` + %s + `, + messages.NSBind, client.JID.Resource) + } + client.Out.Encode(&messages.IQClient{ + Type: messages.IQTypeSet, + To: client.JID.Domain, + From: client.JID.Full(), + ID: utils.CreateCookieString(), + Body: []byte(msg), + }) + + var iq messages.IQClient + if err := client.ReadElement(&iq); err != nil { + return err + } + if &iq.Bind == nil { + return errors.New(" result missing ") + } + if iq.Bind.JID != nil { + client.JID.Local = iq.Bind.JID.Local + client.JID.Domain = iq.Bind.JID.Domain + client.JID.Resource = iq.Bind.JID.Resource + } else { + return errors.New(string(iq.Body)) + } + // set status + client.Out.Encode(&messages.PresenceClient{Show: "online", Status: "yaja client"}) + + return nil +} + +func saslDigestResponse(username, realm, passwd, nonce, cnonceStr, authenticate, digestURI, nonceCountStr string) string { + h := func(text string) []byte { + h := md5.New() + h.Write([]byte(text)) + return h.Sum(nil) + } + hex := func(bytes []byte) string { + return fmt.Sprintf("%x", bytes) + } + kd := func(secret, data string) []byte { + return h(secret + ":" + data) + } + + a1 := string(h(username+":"+realm+":"+passwd)) + ":" + nonce + ":" + cnonceStr + a2 := authenticate + ":" + digestURI + response := hex(kd(hex(h(a1)), nonce+":"+nonceCountStr+":"+cnonceStr+":auth:"+hex(h(a2)))) + return response +} + +func cnonce() string { + randSize := big.NewInt(0) + randSize.Lsh(big.NewInt(1), 64) + cn, err := rand.Int(rand.Reader, randSize) + if err != nil { + return "" + } + return fmt.Sprintf("%016x", cn) +} diff --git a/cmd/daemon.go b/cmd/daemon.go new file mode 100644 index 0000000..0ef0a60 --- /dev/null +++ b/cmd/daemon.go @@ -0,0 +1,18 @@ +package cmd + +import ( + "dev.sum7.eu/genofire/yaja/daemon" + "github.com/spf13/cobra" +) + +// DaemonCMD represents the serve command +var DaemonCMD = &cobra.Command{ + Use: "daemon", + Short: "daemon of yaja", +} + +func init() { + DaemonCMD.AddCommand(daemon.ServerCMD) + DaemonCMD.AddCommand(daemon.TesterCMD) + RootCMD.AddCommand(DaemonCMD) +} diff --git a/cmd/root.go b/cmd/root.go index 65f2f1e..9e0d72a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -11,7 +11,7 @@ var ( ) // RootCmd represents the base command when called without any subcommands -var RootCmd = &cobra.Command{ +var RootCMD = &cobra.Command{ Use: "yaja", Short: "Yet another jabber server", Long: `A small standalone jabber server, for easy deployment`, @@ -20,7 +20,7 @@ var RootCmd = &cobra.Command{ // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { - if err := RootCmd.Execute(); err != nil { + if err := RootCMD.Execute(); err != nil { log.Panicln(err) } } diff --git a/daemon/config.go b/daemon/config.go new file mode 100644 index 0000000..df9193f --- /dev/null +++ b/daemon/config.go @@ -0,0 +1,3 @@ +package daemon + +var configPath string diff --git a/cmd/server.go b/daemon/server.go similarity index 74% rename from cmd/server.go rename to daemon/server.go index 8a32883..3c4c825 100644 --- a/cmd/server.go +++ b/daemon/server.go @@ -1,4 +1,4 @@ -package cmd +package daemon import ( "crypto/tls" @@ -10,8 +10,8 @@ import ( "golang.org/x/crypto/acme/autocert" + serverDaemon "dev.sum7.eu/genofire/yaja/daemon/server" "dev.sum7.eu/genofire/yaja/database" - "dev.sum7.eu/genofire/yaja/model/config" "dev.sum7.eu/genofire/yaja/server/extension" "dev.sum7.eu/genofire/golang-lib/file" @@ -22,10 +22,8 @@ import ( "github.com/spf13/cobra" ) -var configPath string - var ( - configData = &config.Config{} + serverConfig = &serverDaemon.Config{} db = &database.State{} statesaveWorker *worker.Worker srv *server.Server @@ -34,35 +32,35 @@ var ( extensionsServer extension.Extensions ) -// serverCmd represents the serve command -var serverCmd = &cobra.Command{ +// ServerCMD represents the serve command +var ServerCMD = &cobra.Command{ Use: "server", - Short: "Runs the yaja server", - Example: "yaja serve -c /etc/yaja.conf", + Short: "runs xmpp server", + Example: "yaja daemon server -c /etc/yaja.conf", Run: func(cmd *cobra.Command, args []string) { - if err := file.ReadTOML(configPath, configData); err != nil { + if err := file.ReadTOML(configPath, serverConfig); err != nil { log.Fatal("unable to load config file:", err) } - log.SetLevel(configData.Logging.Level) + log.SetLevel(serverConfig.Logging.Level) - if err := file.ReadJSON(configData.StatePath, db); err != nil { + if err := file.ReadJSON(serverConfig.StatePath, db); err != nil { log.Warn("unable to load state file:", err) } statesaveWorker = worker.NewWorker(time.Minute, func() { - file.SaveJSON(configData.StatePath, db) - log.Info("save state to:", configData.StatePath) + file.SaveJSON(serverConfig.StatePath, db) + log.Info("save state to:", serverConfig.StatePath) }) m := autocert.Manager{ - Cache: autocert.DirCache(configData.TLSDir), + Cache: autocert.DirCache(serverConfig.TLSDir), Prompt: autocert.AcceptTOS, } // https server to handle acme (by letsencrypt) - for _, addr := range configData.Address.Webserver { + for _, addr := range serverConfig.Address.Webserver { hs := &http.Server{ Addr: addr, TLSConfig: &tls.Config{GetCertificate: m.GetCertificate}, @@ -77,12 +75,12 @@ var serverCmd = &cobra.Command{ srv = &server.Server{ TLSManager: &m, Database: db, - ClientAddr: configData.Address.Client, - ServerAddr: configData.Address.Server, - LoggingClient: configData.Logging.LevelClient, - LoggingServer: configData.Logging.LevelServer, - RegisterEnable: configData.Register.Enable, - RegisterDomains: configData.Register.Domains, + ClientAddr: serverConfig.Address.Client, + ServerAddr: serverConfig.Address.Server, + LoggingClient: serverConfig.Logging.LevelClient, + LoggingServer: serverConfig.Logging.LevelServer, + RegisterEnable: serverConfig.Register.Enable, + RegisterDomains: serverConfig.Register.Domains, ExtensionsServer: extensionsServer, ExtensionsClient: extensionsClient, } @@ -117,12 +115,12 @@ func quit() { srv.Close() statesaveWorker.Close() - file.SaveJSON(configData.StatePath, db) + file.SaveJSON(serverConfig.StatePath, db) } func reload() { log.Info("start reloading...") - var configNewData *config.Config + var configNewData *serverDaemon.Config if err := file.ReadTOML(configPath, configNewData); err != nil { log.Warn("unable to load config file:", err) @@ -136,7 +134,7 @@ func reload() { //TODO fetch changing address (to set restart) - if configNewData.StatePath != configData.StatePath { + if configNewData.StatePath != serverConfig.StatePath { statesaveWorker.Close() statesaveWorker := worker.NewWorker(time.Minute, func() { file.SaveJSON(configNewData.StatePath, db) @@ -147,10 +145,10 @@ func reload() { restartServer := false - if configNewData.TLSDir != configData.TLSDir { + if configNewData.TLSDir != serverConfig.TLSDir { m := autocert.Manager{ - Cache: autocert.DirCache(configData.TLSDir), + Cache: autocert.DirCache(serverConfig.TLSDir), Prompt: autocert.AcceptTOS, } @@ -176,7 +174,7 @@ func reload() { srv = newServer } - configData = configNewData + serverConfig = configNewData log.Info("reloaded") } @@ -200,7 +198,6 @@ func init() { &extension.IQPing{}, }) - RootCmd.AddCommand(serverCmd) - serverCmd.Flags().StringVarP(&configPath, "config", "c", "yaja.conf", "Path to configuration file") + ServerCMD.Flags().StringVarP(&configPath, "config", "c", "yaja-server.conf", "Path to configuration file") } diff --git a/model/config/struct.go b/daemon/server/config.go similarity index 97% rename from model/config/struct.go rename to daemon/server/config.go index 872ccef..6dc45ba 100644 --- a/model/config/struct.go +++ b/daemon/server/config.go @@ -1,4 +1,4 @@ -package config +package server import ( log "github.com/sirupsen/logrus" diff --git a/model/config/testdata/config_panic.conf b/daemon/server/testdata/config_panic.conf similarity index 100% rename from model/config/testdata/config_panic.conf rename to daemon/server/testdata/config_panic.conf diff --git a/daemon/tester.go b/daemon/tester.go new file mode 100644 index 0000000..72946a2 --- /dev/null +++ b/daemon/tester.go @@ -0,0 +1,117 @@ +package daemon + +import ( + "crypto/tls" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "golang.org/x/crypto/acme/autocert" + + "dev.sum7.eu/genofire/golang-lib/file" + "dev.sum7.eu/genofire/golang-lib/worker" + "dev.sum7.eu/genofire/yaja/client" + "dev.sum7.eu/genofire/yaja/daemon/tester" + "dev.sum7.eu/genofire/yaja/messages" + log "github.com/sirupsen/logrus" + + "github.com/spf13/cobra" +) + +var configTester = &tester.Config{} + +// TesterCMD represents the serve command +var TesterCMD = &cobra.Command{ + Use: "tester", + Short: "runs xmpp tester server", + Example: "yaja daemon tester -c /etc/yaja.conf", + Run: func(cmd *cobra.Command, args []string) { + + if err := file.ReadTOML(configPath, configTester); err != nil { + log.Fatal("unable to load config file:", err) + } + + log.SetLevel(configTester.Logging) + + if err := file.ReadJSON(configTester.StatePath, db); err != nil { + log.Warn("unable to load state file:", err) + } + + statesaveWorker = worker.NewWorker(time.Minute, func() { + file.SaveJSON(configTester.StatePath, db) + log.Info("save state to:", configTester.StatePath) + }) + + // https server to handle acme (by letsencrypt) + hs := &http.Server{ + Addr: configTester.Webserver, + } + if configTester.TLSDir != "" { + m := autocert.Manager{ + Cache: autocert.DirCache(configTester.TLSDir), + Prompt: autocert.AcceptTOS, + } + hs.TLSConfig = &tls.Config{GetCertificate: m.GetCertificate} + go func(hs *http.Server) { + if err := hs.ListenAndServeTLS("", ""); err != http.ErrServerClosed { + log.Errorf("webserver with addr %s: %s", hs.Addr, err) + } + }(hs) + } else { + go func(hs *http.Server) { + if err := hs.ListenAndServe(); err != http.ErrServerClosed { + log.Errorf("webserver with addr %s: %s", hs.Addr, err) + } + }(hs) + } + + mainClient, err := client.NewClient(configTester.Client.JID, configTester.Client.Password) + if err != nil { + log.Fatal("unable to connect with main jabber client: ", err) + } + + for _, admin := range configTester.Admins { + mainClient.Out.Encode(&messages.MessageClient{ + From: mainClient.JID.Full(), + To: admin.Full(), + Type: "chat", + Body: "yaja tester starts", + }) + } + + go statesaveWorker.Start() + + log.Infoln("yaja tester started ") + + // Wait for INT/TERM + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT) + for sig := range sigs { + log.Infoln("received", sig) + switch sig { + case syscall.SIGTERM: + log.Panic("terminated") + os.Exit(0) + case syscall.SIGQUIT: + quitTester() + case syscall.SIGHUP: + quitTester() + } + } + + }, +} + +func quitTester() { + srv.Close() + statesaveWorker.Close() + + file.SaveJSON(configTester.StatePath, db) +} + +func init() { + TesterCMD.Flags().StringVarP(&configPath, "config", "c", "yaja-tester.conf", "Path to configuration file") + +} diff --git a/daemon/tester/config.go b/daemon/tester/config.go new file mode 100644 index 0000000..235c0dc --- /dev/null +++ b/daemon/tester/config.go @@ -0,0 +1,18 @@ +package tester + +import ( + "dev.sum7.eu/genofire/yaja/model" + log "github.com/sirupsen/logrus" +) + +type Config struct { + TLSDir string `toml:"tlsdir"` + StatePath string `toml:"state_path"` + Logging log.Level `toml:"logging"` + Webserver string `toml:"webserver"` + Admins []model.JID `toml:"admins"` + Client struct { + JID model.JID `toml:"jid"` + Password string `toml:"password"` + } `toml:"client"` +} diff --git a/messages/connection.go b/messages/connection.go new file mode 100644 index 0000000..1702a34 --- /dev/null +++ b/messages/connection.go @@ -0,0 +1,43 @@ +package messages + +import ( + "encoding/xml" + + "dev.sum7.eu/genofire/yaja/model" +) + +// RFC 3920 C.1 Streams name space +type StreamFeatures struct { + XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"` + StartTLS *TLSStartTLS + Mechanisms SASLMechanisms + Bind Bind + Session bool +} + +type StreamError struct { + XMLName xml.Name `xml:"http://etherx.jabber.org/streams error"` + Any xml.Name + Text string +} + +// RFC 3920 C.3 TLS name space +type TLSStartTLS struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls starttls"` + Required *string `xml:"required"` +} + +type TLSFailure struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls failure"` +} + +type TLSProceed struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls proceed"` +} + +// RFC 3920 C.5 Resource binding name space +type Bind struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"` + Resource string `xml:"resource"` + JID *model.JID `xml:"jid"` +} diff --git a/messages/error.go b/messages/error.go index 6acc884..c250282 100644 --- a/messages/error.go +++ b/messages/error.go @@ -2,8 +2,8 @@ package messages import "encoding/xml" -// Error element -type Error struct { +// ErrorClient element +type ErrorClient struct { XMLName xml.Name `xml:"jabber:client error"` Code string `xml:"code,attr"` Type string `xml:"type,attr"` diff --git a/messages/iq.go b/messages/iq.go index cf278b0..4703731 100644 --- a/messages/iq.go +++ b/messages/iq.go @@ -12,14 +12,14 @@ const ( ) // IQ element - info/query -type IQ struct { - XMLName xml.Name `xml:"jabber:client iq"` - From string `xml:"from,attr"` - ID string `xml:"id,attr"` - To string `xml:"to,attr"` - Type IQType `xml:"type,attr"` - Error *Error `xml:"error"` - //Bind bindBind `xml:"bind"` - Body []byte `xml:",innerxml"` +type IQClient struct { + XMLName xml.Name `xml:"jabber:client iq"` + From string `xml:"from,attr"` + ID string `xml:"id,attr"` + To string `xml:"to,attr"` + Type IQType `xml:"type,attr"` + Error *ErrorClient `xml:"error"` + Bind Bind `xml:"bind"` + Body []byte `xml:",innerxml"` // RosterRequest - better detection of iq's } diff --git a/messages/presence.go b/messages/presence.go index f77c90f..23d0cf7 100644 --- a/messages/presence.go +++ b/messages/presence.go @@ -14,8 +14,8 @@ const ( PresenceTypeError PresenceType = "error" ) -// Presence element -type Presence struct { +// PresenceClient element +type PresenceClient struct { XMLName xml.Name `xml:"jabber:client presence"` From string `xml:"from,attr,omitempty"` ID string `xml:"id,attr,omitempty"` @@ -27,6 +27,19 @@ type Presence struct { Status string `xml:"status,omitempty"` // sb []clientText Priority string `xml:"priority,omitempty"` // Caps *ClientCaps `xml:"c"` - Error *Error `xml:"error"` + Error *ErrorClient `xml:"error"` // Delay Delay `xml:"delay"` } + +// MessageClient element +type MessageClient struct { + XMLName xml.Name `xml:"jabber:client message"` + From string `xml:"from,attr,omitempty"` + ID string `xml:"id,attr,omitempty"` + To string `xml:"to,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` + Lang string `xml:"lang,attr,omitempty"` + Subject string `xml:"subject"` + Body string `xml:"body"` + Thread string `xml:"thread"` +} diff --git a/messages/sasl.go b/messages/sasl.go index 294bda1..6e0fc1d 100644 --- a/messages/sasl.go +++ b/messages/sasl.go @@ -8,3 +8,9 @@ type SASLAuth struct { Mechanism string `xml:"mechanism,attr"` Body string `xml:",chardata"` } + +// RFC 3920 C.4 SASL name space +type SASLMechanisms struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl mechanisms"` + Mechanism []string `xml:"mechanism"` +} diff --git a/model/jid.go b/model/jid.go index 7fc5b5c..4a5f025 100644 --- a/model/jid.go +++ b/model/jid.go @@ -52,13 +52,13 @@ func (jid *JID) Full() string { return jid.Bare() } -//MarshalTOML to bytearray -func (jid JID) MarshalTOML() ([]byte, error) { +//MarshalText to bytearray +func (jid JID) MarshalText() ([]byte, error) { return []byte(jid.Full()), nil } -// UnmarshalTOML from bytearray -func (jid *JID) UnmarshalTOML(data []byte) (err error) { +// UnmarshalText from bytearray +func (jid *JID) UnmarshalText(data []byte) (err error) { newJID := NewJID(string(data)) if newJID == nil { return errors.New("not a valid jid") diff --git a/model/jid_test.go b/model/jid_test.go index 52ea722..1315f3d 100644 --- a/model/jid_test.go +++ b/model/jid_test.go @@ -137,14 +137,14 @@ func TestMarshal(t *testing.T) { assert := assert.New(t) jid := &JID{} - err := jid.UnmarshalTOML([]byte("juliet@example.com/foo")) + err := jid.UnmarshalText([]byte("juliet@example.com/foo")) assert.NoError(err) assert.Equal(jid.Local, "juliet") assert.Equal(jid.Domain, "example.com") assert.Equal(jid.Resource, "foo") - err = jid.UnmarshalTOML([]byte("juliet@example.com/ foo")) + err = jid.UnmarshalText([]byte("juliet@example.com/ foo")) assert.Error(err) @@ -153,7 +153,7 @@ func TestMarshal(t *testing.T) { Domain: "example.com", Resource: "bar", } - jidString, err := jid.MarshalTOML() + jidString, err := jid.MarshalText() assert.NoError(err) assert.Equal("romeo@example.com/bar", string(jidString)) } diff --git a/model/xml.go b/model/xml.go new file mode 100644 index 0000000..fa3a556 --- /dev/null +++ b/model/xml.go @@ -0,0 +1,13 @@ +package model + +import ( + "bytes" + "encoding/xml" +) + +func XMLEscape(s string) string { + var b bytes.Buffer + xml.Escape(&b, []byte(s)) + + return b.String() +} diff --git a/server/extension/iq.go b/server/extension/iq.go index 13b83df..d74b54e 100644 --- a/server/extension/iq.go +++ b/server/extension/iq.go @@ -11,8 +11,8 @@ type IQExtensions []IQExtension type IQExtension interface { Extension - Get(*messages.IQ, *utils.Client) bool - Set(*messages.IQ, *utils.Client) bool + Get(*messages.IQClient, *utils.Client) bool + Set(*messages.IQClient, *utils.Client) bool } func (iex IQExtensions) Spaces() (result []string) { @@ -27,7 +27,7 @@ func (iex IQExtensions) Process(element *xml.StartElement, client *utils.Client) log := client.Log.WithField("extension", "iq") // iq encode - var msg messages.IQ + var msg messages.IQClient if err := client.In.DecodeElement(&msg, element); err != nil { return false } diff --git a/server/extension/iq_disco.go b/server/extension/iq_disco.go index ef74689..45e0e05 100644 --- a/server/extension/iq_disco.go +++ b/server/extension/iq_disco.go @@ -15,7 +15,7 @@ type IQDisco struct { func (ex *IQDisco) Spaces() []string { return []string{"http://jabber.org/protocol/disco#items"} } -func (ex *IQDisco) Get(msg *messages.IQ, client *utils.Client) bool { +func (ex *IQDisco) Get(msg *messages.IQClient, client *utils.Client) bool { log := client.Log.WithField("extension", "disco-item").WithField("id", msg.ID) // query encode @@ -57,7 +57,7 @@ func (ex *IQDisco) Get(msg *messages.IQ, client *utils.Client) bool { } // reply - client.Messages <- &messages.IQ{ + client.Messages <- &messages.IQClient{ Type: messages.IQTypeResult, To: client.JID.String(), From: client.JID.Domain, diff --git a/server/extension/iq_discovery.go b/server/extension/iq_discovery.go index b3fefc4..c2c0288 100644 --- a/server/extension/iq_discovery.go +++ b/server/extension/iq_discovery.go @@ -14,7 +14,7 @@ type IQExtensionDiscovery struct { func (ex *IQExtensionDiscovery) Spaces() []string { return []string{} } -func (ex *IQExtensionDiscovery) Get(msg *messages.IQ, client *utils.Client) bool { +func (ex *IQExtensionDiscovery) Get(msg *messages.IQClient, client *utils.Client) bool { log := client.Log.WithField("extension", "roster").WithField("id", msg.ID) // query encode @@ -57,7 +57,7 @@ func (ex *IQExtensionDiscovery) Get(msg *messages.IQ, client *utils.Client) bool } // replay - client.Messages <- &messages.IQ{ + client.Messages <- &messages.IQClient{ Type: messages.IQTypeResult, To: client.JID.String(), From: client.JID.Domain, diff --git a/server/extension/iq_last.go b/server/extension/iq_last.go index 8df4891..b06c5c8 100644 --- a/server/extension/iq_last.go +++ b/server/extension/iq_last.go @@ -15,7 +15,7 @@ type IQLast struct { func (ex *IQLast) Spaces() []string { return []string{"jabber:iq:last"} } -func (ex *IQLast) Get(msg *messages.IQ, client *utils.Client) bool { +func (ex *IQLast) Get(msg *messages.IQClient, client *utils.Client) bool { log := client.Log.WithField("extension", "last").WithField("id", msg.ID) // query encode @@ -45,7 +45,7 @@ func (ex *IQLast) Get(msg *messages.IQ, client *utils.Client) bool { } // reply - client.Messages <- &messages.IQ{ + client.Messages <- &messages.IQClient{ Type: messages.IQTypeResult, To: client.JID.String(), From: client.JID.Domain, diff --git a/server/extension/iq_ping.go b/server/extension/iq_ping.go index 6e64ae5..0f65819 100644 --- a/server/extension/iq_ping.go +++ b/server/extension/iq_ping.go @@ -13,7 +13,7 @@ type IQPing struct { func (ex *IQPing) Spaces() []string { return []string{"urn:xmpp:ping"} } -func (ex *IQPing) Get(msg *messages.IQ, client *utils.Client) bool { +func (ex *IQPing) Get(msg *messages.IQClient, client *utils.Client) bool { log := client.Log.WithField("extension", "ping").WithField("id", msg.ID) // ping encode @@ -26,7 +26,7 @@ func (ex *IQPing) Get(msg *messages.IQ, client *utils.Client) bool { } // reply - client.Messages <- &messages.IQ{ + client.Messages <- &messages.IQClient{ Type: messages.IQTypeResult, To: client.JID.String(), From: client.JID.Domain, diff --git a/server/extension/iq_private.go b/server/extension/iq_private.go index bb82d27..52eb8c5 100644 --- a/server/extension/iq_private.go +++ b/server/extension/iq_private.go @@ -17,12 +17,12 @@ type iqPrivateQuery struct { } type iqPrivateExtension interface { - Handle(*messages.IQ, *iqPrivateQuery, *utils.Client) bool + Handle(*messages.IQClient, *iqPrivateQuery, *utils.Client) bool } func (ex *IQPrivate) Spaces() []string { return []string{"jabber:iq:private"} } -func (ex *IQPrivate) Get(msg *messages.IQ, client *utils.Client) bool { +func (ex *IQPrivate) Get(msg *messages.IQClient, client *utils.Client) bool { log := client.Log.WithField("extension", "private").WithField("id", msg.ID) // query encode diff --git a/server/extension/iq_private_bookmarks.go b/server/extension/iq_private_bookmarks.go index bcaa653..2fd55e5 100644 --- a/server/extension/iq_private_bookmarks.go +++ b/server/extension/iq_private_bookmarks.go @@ -11,7 +11,7 @@ type IQPrivateBookmark struct { iqPrivateExtension } -func (ex *IQPrivateBookmark) Handle(msg *messages.IQ, q *iqPrivateQuery, client *utils.Client) bool { +func (ex *IQPrivateBookmark) Handle(msg *messages.IQClient, q *iqPrivateQuery, client *utils.Client) bool { log := client.Log.WithField("extension", "private").WithField("id", msg.ID) // storage encode @@ -35,7 +35,7 @@ func (ex *IQPrivateBookmark) Handle(msg *messages.IQ, q *iqPrivateQuery, client } // reply - client.Messages <- &messages.IQ{ + client.Messages <- &messages.IQClient{ Type: messages.IQTypeResult, To: client.JID.String(), From: client.JID.Domain, diff --git a/server/extension/iq_private_metacontacts.go b/server/extension/iq_private_metacontacts.go index bea4fb7..5819cd9 100644 --- a/server/extension/iq_private_metacontacts.go +++ b/server/extension/iq_private_metacontacts.go @@ -11,7 +11,7 @@ type IQPrivateMetacontact struct { iqPrivateExtension } -func (ex *IQPrivateMetacontact) Handle(msg *messages.IQ, q *iqPrivateQuery, client *utils.Client) bool { +func (ex *IQPrivateMetacontact) Handle(msg *messages.IQClient, q *iqPrivateQuery, client *utils.Client) bool { log := client.Log.WithField("extension", "private-metacontact").WithField("id", msg.ID) // storage encode @@ -36,7 +36,7 @@ func (ex *IQPrivateMetacontact) Handle(msg *messages.IQ, q *iqPrivateQuery, clie } // reply - client.Messages <- &messages.IQ{ + client.Messages <- &messages.IQClient{ Type: messages.IQTypeResult, To: client.JID.String(), From: client.JID.Domain, diff --git a/server/extension/iq_private_roster.go b/server/extension/iq_private_roster.go index 539ef1c..61c67d3 100644 --- a/server/extension/iq_private_roster.go +++ b/server/extension/iq_private_roster.go @@ -11,7 +11,7 @@ type IQPrivateRoster struct { iqPrivateExtension } -func (ex *IQPrivateRoster) Handle(msg *messages.IQ, q *iqPrivateQuery, client *utils.Client) bool { +func (ex *IQPrivateRoster) Handle(msg *messages.IQClient, q *iqPrivateQuery, client *utils.Client) bool { log := client.Log.WithField("extension", "private").WithField("id", msg.ID) // roster encode @@ -40,7 +40,7 @@ func (ex *IQPrivateRoster) Handle(msg *messages.IQ, q *iqPrivateQuery, client *u } // reply - client.Messages <- &messages.IQ{ + client.Messages <- &messages.IQClient{ Type: messages.IQTypeResult, To: client.JID.String(), From: client.JID.Domain, diff --git a/server/extension/iq_roster.go b/server/extension/iq_roster.go index 73b2109..c335669 100644 --- a/server/extension/iq_roster.go +++ b/server/extension/iq_roster.go @@ -15,7 +15,7 @@ type IQRoster struct { func (ex *IQRoster) Spaces() []string { return []string{"jabber:iq:roster"} } -func (ex *IQRoster) Get(msg *messages.IQ, client *utils.Client) bool { +func (ex *IQRoster) Get(msg *messages.IQClient, client *utils.Client) bool { log := client.Log.WithField("extension", "roster").WithField("id", msg.ID) // query encode @@ -59,7 +59,7 @@ func (ex *IQRoster) Get(msg *messages.IQ, client *utils.Client) bool { } // reply - client.Messages <- &messages.IQ{ + client.Messages <- &messages.IQClient{ Type: messages.IQTypeResult, To: client.JID.String(), From: client.JID.Domain, diff --git a/server/extension/presence.go b/server/extension/presence.go index 4799fee..7addacd 100644 --- a/server/extension/presence.go +++ b/server/extension/presence.go @@ -19,11 +19,11 @@ func (p *Presence) Process(element *xml.StartElement, client *utils.Client) bool log := client.Log.WithField("extension", "presence") // iq encode - var msg messages.Presence + var msg messages.PresenceClient if err := client.In.DecodeElement(&msg, element); err != nil { return false } - client.Messages <- &messages.Presence{ + client.Messages <- &messages.PresenceClient{ ID: msg.ID, } log.Debug("send") diff --git a/server/toclient/connect.go b/server/toclient/connect.go index 088db2d..eecf3d7 100644 --- a/server/toclient/connect.go +++ b/server/toclient/connect.go @@ -199,7 +199,7 @@ func (state *AuthedStream) Process() state.State { state.Client.Log.Warn("unable to read: ", err) return nil } - var msg messages.IQ + var msg messages.IQClient if err = state.Client.In.DecodeElement(&msg, element); err != nil { state.Client.Log.Warn("is no iq: ", err) return nil @@ -212,11 +212,8 @@ func (state *AuthedStream) Process() state.State { state.Client.Log.Warn("iq with error: ", msg.Error.Code) return nil } - type query struct { - XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"` - Resource string `xml:"resource"` - } - q := &query{} + + var q messages.Bind err = xml.Unmarshal(msg.Body, q) if err != nil { state.Client.Log.Warn("is no iq bind: ", err) @@ -228,7 +225,7 @@ func (state *AuthedStream) Process() state.State { state.Client.JID.Resource = q.Resource } state.Client.Log = state.Client.Log.WithField("jid", state.Client.JID.Full()) - state.Client.Out.Encode(&messages.IQ{ + state.Client.Out.Encode(&messages.IQClient{ Type: messages.IQTypeResult, To: state.Client.JID.String(), From: state.Client.JID.Domain, diff --git a/server/toclient/register.go b/server/toclient/register.go index bedfdec..54ca034 100644 --- a/server/toclient/register.go +++ b/server/toclient/register.go @@ -29,7 +29,7 @@ func (state *RegisterFormRequest) Process() state.State { return nil } - var msg messages.IQ + var msg messages.IQClient if err := state.Client.In.DecodeElement(&msg, state.element); err != nil { state.Client.Log.Warn("is no iq: ", err) return state @@ -52,7 +52,7 @@ func (state *RegisterFormRequest) Process() state.State { state.Client.Log.Warn("is no iq register: ", err) return nil } - state.Client.Out.Encode(&messages.IQ{ + state.Client.Out.Encode(&messages.IQClient{ Type: messages.IQTypeResult, To: state.Client.JID.String(), From: state.Client.JID.Domain, @@ -90,7 +90,7 @@ func (state *RegisterRequest) Process() state.State { state.Client.Log.Warn("unable to read: ", err) return nil } - var msg messages.IQ + var msg messages.IQClient if err = state.Client.In.DecodeElement(&msg, element); err != nil { state.Client.Log.Warn("is no iq: ", err) return state @@ -120,7 +120,7 @@ func (state *RegisterRequest) Process() state.State { account := model.NewAccount(state.Client.JID, q.Password) err = state.database.AddAccount(account) if err != nil { - state.Client.Out.Encode(&messages.IQ{ + state.Client.Out.Encode(&messages.IQClient{ Type: messages.IQTypeResult, To: state.Client.JID.String(), From: state.Client.JID.Domain, @@ -129,7 +129,7 @@ func (state *RegisterRequest) Process() state.State { %s %s `, messages.NSIQRegister, q.Username, q.Password)), - Error: &messages.Error{ + Error: &messages.ErrorClient{ Code: "409", Type: "cancel", Any: xml.Name{ @@ -141,7 +141,7 @@ func (state *RegisterRequest) Process() state.State { state.Client.Log.Warn("database error: ", err) return state } - state.Client.Out.Encode(&messages.IQ{ + state.Client.Out.Encode(&messages.IQClient{ Type: messages.IQTypeResult, To: state.Client.JID.String(), From: state.Client.JID.Domain, diff --git a/config_example.conf b/yaja-server_example.conf similarity index 100% rename from config_example.conf rename to yaja-server_example.conf diff --git a/yaja-tester_example.conf b/yaja-tester_example.conf new file mode 100644 index 0000000..62cec8b --- /dev/null +++ b/yaja-tester_example.conf @@ -0,0 +1,11 @@ +tlsdir = "tmp/ssl" +state_path = "tmp/yaja-tester.json" +logging = 5 + +webserver = ":https" + +admins = ["a.admin@chat.sum7.eu"] + +[client] +jid = "bot@chat.sum7.eu" +password = "test"