In-Band Registration (XEP-0077)

This commit is contained in:
Bohdan Horbeshko 2023-08-28 10:16:57 -04:00
parent 8ba7596ab5
commit 20994e2995
5 changed files with 304 additions and 36 deletions

View file

@ -15,7 +15,8 @@ import (
) )
const notEnoughArguments string = "Not enough arguments" const notEnoughArguments string = "Not enough arguments"
const telegramNotInitialized string = "Telegram connection is not initialized yet" const TelegramNotInitialized string = "Telegram connection is not initialized yet"
const TelegramAuthDone string = "Authorization is done already"
const notOnline string = "Not online" const notOnline string = "Not online"
var permissionsAdmin = client.ChatAdministratorRights{ var permissionsAdmin = client.ChatAdministratorRights{
@ -244,34 +245,22 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string
} }
if cmd == "login" { if cmd == "login" {
wasSessionLoginEmpty := c.Session.Login == "" err := c.TryLogin(resource, args[0])
c.Session.Login = args[0]
if wasSessionLoginEmpty && c.authorizer == nil {
go func() {
err := c.Connect(resource)
if err != nil { if err != nil {
log.Error(errors.Wrap(err, "TDlib connection failure")) return err.Error()
}
}()
// a quirk for authorizer to become ready. If it's still not,
// nothing bad: the command just needs to be resent again
time.Sleep(1e5)
}
} }
c.authorizer.PhoneNumber <- args[0]
} else {
if c.authorizer == nil { if c.authorizer == nil {
return telegramNotInitialized return TelegramNotInitialized
} }
if c.authorizer.isClosed { if c.authorizer.isClosed {
return "Authorization is done already" return TelegramAuthDone
} }
switch cmd { switch cmd {
// sign in
case "login":
c.authorizer.PhoneNumber <- args[0]
// check auth code // check auth code
case "code": case "code":
c.authorizer.Code <- args[0] c.authorizer.Code <- args[0]
@ -279,6 +268,7 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) string
case "password": case "password":
c.authorizer.Password <- args[0] c.authorizer.Password <- args[0]
} }
}
// sign out // sign out
case "logout": case "logout":
if !c.Online() { if !c.Online() {

View file

@ -3,6 +3,7 @@ package telegram
import ( import (
"github.com/pkg/errors" "github.com/pkg/errors"
"strconv" "strconv"
"time"
"dev.narayana.im/narayana/telegabber/xmpp/gateway" "dev.narayana.im/narayana/telegabber/xmpp/gateway"
@ -154,14 +155,49 @@ func (c *Client) Connect(resource string) error {
log.Errorf("Could not retrieve chats: %v", err) log.Errorf("Could not retrieve chats: %v", err)
} }
gateway.SendPresence(c.xmpp, c.jid, gateway.SPType("subscribe")) gateway.SubscribeToTransport(c.xmpp, c.jid)
gateway.SendPresence(c.xmpp, c.jid, gateway.SPType("subscribed"))
gateway.SendPresence(c.xmpp, c.jid, gateway.SPStatus("Logged in as: "+c.Session.Login)) gateway.SendPresence(c.xmpp, c.jid, gateway.SPStatus("Logged in as: "+c.Session.Login))
}() }()
return nil return nil
} }
func (c *Client) TryLogin(resource string, login string) error {
wasSessionLoginEmpty := c.Session.Login == ""
c.Session.Login = login
if wasSessionLoginEmpty && c.authorizer == nil {
go func() {
err := c.Connect(resource)
if err != nil {
log.Error(errors.Wrap(err, "TDlib connection failure"))
}
}()
// a quirk for authorizer to become ready. If it's still not,
// nothing bad: just re-login again
time.Sleep(1e5)
}
if c.authorizer == nil {
return errors.New(TelegramNotInitialized)
}
if c.authorizer.isClosed {
return errors.New(TelegramAuthDone)
}
return nil
}
func (c *Client) SetPhoneNumber(login string) error {
if c.authorizer == nil || c.authorizer.isClosed {
return errors.New("Authorization not needed")
}
c.authorizer.PhoneNumber <- login
return nil
}
// Disconnect drops TDlib connection and // Disconnect drops TDlib connection and
// returns the flag indicating if disconnecting is permitted // returns the flag indicating if disconnecting is permitted
func (c *Client) Disconnect(resource string, quit bool) bool { func (c *Client) Disconnect(resource string, quit bool) bool {

View file

@ -193,6 +193,26 @@ type Replace struct {
Id string `xml:"id,attr"` Id string `xml:"id,attr"`
} }
// QueryRegister is from XEP-0077
type QueryRegister struct {
XMLName xml.Name `xml:"jabber:iq:register query"`
Instructions string `xml:"instructions"`
Username string `xml:"username"`
Registered *QueryRegisterRegistered `xml:"registered"`
Remove *QueryRegisterRemove `xml:"remove"`
ResultSet *stanza.ResultSet `xml:"set,omitempty"`
}
// QueryRegisterRegistered is a child element from XEP-0077
type QueryRegisterRegistered struct {
XMLName xml.Name `xml:"registered"`
}
// QueryRegisterRemove is a child element from XEP-0077
type QueryRegisterRemove struct {
XMLName xml.Name `xml:"remove"`
}
// Namespace is a namespace! // Namespace is a namespace!
func (c PresenceNickExtension) Namespace() string { func (c PresenceNickExtension) Namespace() string {
return c.XMLName.Space return c.XMLName.Space
@ -248,6 +268,16 @@ func (c Replace) Namespace() string {
return c.XMLName.Space return c.XMLName.Space
} }
// Namespace is a namespace!
func (c QueryRegister) Namespace() string {
return c.XMLName.Space
}
// GetSet getsets!
func (c QueryRegister) GetSet() *stanza.ResultSet {
return c.ResultSet
}
// Name is a packet name // Name is a packet name
func (ClientMessage) Name() string { func (ClientMessage) Name() string {
return "message" return "message"
@ -326,4 +356,10 @@ func init() {
"urn:xmpp:message-correct:0", "urn:xmpp:message-correct:0",
"replace", "replace",
}, Replace{}) }, Replace{})
// register query
stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{
"jabber:iq:register",
"query",
}, QueryRegister{})
} }

View file

@ -360,6 +360,12 @@ func ResumableSend(component *xmpp.Component, packet stanza.Packet) error {
return err return err
} }
// SubscribeToTransport ensures a two-way subscription to the transport
func SubscribeToTransport(component *xmpp.Component, jid string) {
SendPresence(component, jid, SPType("subscribe"))
SendPresence(component, jid, SPType("subscribed"))
}
// SplitJID tokenizes a JID string to bare JID and resource // SplitJID tokenizes a JID string to bare JID and resource
func SplitJID(from string) (string, string, bool) { func SplitJID(from string) (string, string, bool) {
fromJid, err := stanza.NewJid(from) fromJid, err := stanza.NewJid(from)

View file

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"encoding/base64" "encoding/base64"
"encoding/xml" "encoding/xml"
"fmt"
"github.com/pkg/errors" "github.com/pkg/errors"
"io" "io"
"strconv" "strconv"
@ -57,6 +58,22 @@ func HandleIq(s xmpp.Sender, p stanza.Packet) {
go handleGetDiscoInfo(s, iq) go handleGetDiscoInfo(s, iq)
return return
} }
_, ok = iq.Payload.(*stanza.DiscoItems)
if ok {
go handleGetDiscoItems(s, iq)
return
}
_, ok = iq.Payload.(*extensions.QueryRegister)
if ok {
go handleGetQueryRegister(s, iq)
return
}
} else if iq.Type == "set" {
query, ok := iq.Payload.(*extensions.QueryRegister)
if ok {
go handleSetQueryRegister(s, iq, query)
return
}
} }
} }
@ -91,8 +108,7 @@ func HandleMessage(s xmpp.Sender, p stanza.Packet) {
session, ok := sessions[bare] session, ok := sessions[bare]
if !ok { if !ok {
if msg.To == gatewayJid { if msg.To == gatewayJid {
gateway.SendPresence(component, msg.From, gateway.SPType("subscribe")) gateway.SubscribeToTransport(component, msg.From)
gateway.SendPresence(component, msg.From, gateway.SPType("subscribed"))
} else { } else {
log.Error("Message from stranger") log.Error("Message from stranger")
} }
@ -444,6 +460,7 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ) {
disco.AddIdentity("", "account", "registered") disco.AddIdentity("", "account", "registered")
} else { } else {
disco.AddIdentity("Telegram Gateway", "gateway", "telegram") disco.AddIdentity("Telegram Gateway", "gateway", "telegram")
disco.AddFeatures("jabber:iq:register")
} }
answer.Payload = disco answer.Payload = disco
@ -458,6 +475,189 @@ func handleGetDiscoInfo(s xmpp.Sender, iq *stanza.IQ) {
_ = gateway.ResumableSend(component, answer) _ = gateway.ResumableSend(component, answer)
} }
func handleGetDiscoItems(s xmpp.Sender, iq *stanza.IQ) {
answer, err := stanza.NewIQ(stanza.Attrs{
Type: stanza.IQTypeResult,
From: iq.To,
To: iq.From,
Id: iq.Id,
Lang: "en",
})
if err != nil {
log.Errorf("Failed to create answer IQ: %v", err)
return
}
answer.Payload = answer.DiscoItems()
component, ok := s.(*xmpp.Component)
if !ok {
log.Error("Not a component")
return
}
_ = gateway.ResumableSend(component, answer)
}
func handleGetQueryRegister(s xmpp.Sender, iq *stanza.IQ) {
component, ok := s.(*xmpp.Component)
if !ok {
log.Error("Not a component")
return
}
answer, err := stanza.NewIQ(stanza.Attrs{
Type: stanza.IQTypeResult,
From: iq.To,
To: iq.From,
Id: iq.Id,
Lang: "en",
})
if err != nil {
log.Errorf("Failed to create answer IQ: %v", err)
return
}
var login string
bare, _, ok := gateway.SplitJID(iq.From)
if ok {
session, ok := sessions[bare]
if ok {
login = session.Session.Login
}
}
var query stanza.IQPayload
if login == "" {
query = extensions.QueryRegister{
Instructions: fmt.Sprintf("Authorization in Telegram is a multi-step process, so please accept %v to your contacts and follow further instructions (provide the authentication code there, etc.).\nFor now, please provide your login.", iq.To),
}
} else {
query = extensions.QueryRegister{
Instructions: "Already logged in",
Username: login,
Registered: &extensions.QueryRegisterRegistered{},
}
}
answer.Payload = query
log.Debugf("%#v", query)
_ = gateway.ResumableSend(component, answer)
if login == "" {
gateway.SubscribeToTransport(component, iq.From)
}
}
func handleSetQueryRegister(s xmpp.Sender, iq *stanza.IQ, query *extensions.QueryRegister) {
component, ok := s.(*xmpp.Component)
if !ok {
log.Error("Not a component")
return
}
answer, err := stanza.NewIQ(stanza.Attrs{
Type: stanza.IQTypeResult,
From: iq.To,
To: iq.From,
Id: iq.Id,
Lang: "en",
})
if err != nil {
log.Errorf("Failed to create answer IQ: %v", err)
return
}
defer gateway.ResumableSend(component, answer)
if query.Remove != nil {
iqAnswerSetError(answer, query, 405)
return
}
var login string
var session *telegram.Client
bare, resource, ok := gateway.SplitJID(iq.From)
if ok {
session, ok = sessions[bare]
if ok {
login = session.Session.Login
}
}
if login == "" {
if !ok {
session, ok = getTelegramInstance(bare, &persistence.Session{}, component)
if !ok {
iqAnswerSetError(answer, query, 500)
return
}
}
err := session.TryLogin(resource, query.Username)
if err != nil {
if err.Error() == telegram.TelegramAuthDone {
iqAnswerSetError(answer, query, 406)
} else {
iqAnswerSetError(answer, query, 500)
}
return
}
err = session.SetPhoneNumber(query.Username)
if err != nil {
iqAnswerSetError(answer, query, 500)
return
}
// everything okay, the response should be empty with no payload/error at this point
gateway.SubscribeToTransport(component, iq.From)
} else {
iqAnswerSetError(answer, query, 406)
}
}
func iqAnswerSetError(answer *stanza.IQ, payload *extensions.QueryRegister, code int) {
answer.Type = stanza.IQTypeError
answer.Payload = *payload
switch code {
case 400:
answer.Error = &stanza.Err{
Code: code,
Type: stanza.ErrorTypeModify,
Reason: "bad-request",
}
case 405:
answer.Error = &stanza.Err{
Code: code,
Type: stanza.ErrorTypeCancel,
Reason: "not-allowed",
Text: "Logging out is dangerous. If you are sure you would be able to receive the authentication code again, issue the /logout command to the transport",
}
case 406:
answer.Error = &stanza.Err{
Code: code,
Type: stanza.ErrorTypeModify,
Reason: "not-acceptable",
Text: "Phone number already provided, chat with the transport for further instruction",
}
case 500:
answer.Error = &stanza.Err{
Code: code,
Type: stanza.ErrorTypeWait,
Reason: "internal-server-error",
}
default:
log.Error("Unknown error code, falling back with empty reason")
answer.Error = &stanza.Err{
Code: code,
Type: stanza.ErrorTypeCancel,
Reason: "undefined-condition",
}
}
}
func toToID(to string) (int64, bool) { func toToID(to string) (int64, bool) {
toParts := strings.Split(to, "@") toParts := strings.Split(to, "@")
if len(toParts) < 2 { if len(toParts) < 2 {