forked from Mirrors/gomuks
Use Beeper JWT login
This commit is contained in:
parent
8fcbdddc62
commit
7e25710506
5 changed files with 215 additions and 50 deletions
80
beeper/internal.go
Normal file
80
beeper/internal.go
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
package beeper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"maunium.net/go/mautrix"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cli = &http.Client{Timeout: 30 * time.Second}
|
||||||
|
|
||||||
|
func newRequest(token, method, path string) *http.Request {
|
||||||
|
req := &http.Request{
|
||||||
|
URL: &url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: "api.beeper.com",
|
||||||
|
Path: path,
|
||||||
|
},
|
||||||
|
Method: method,
|
||||||
|
Header: http.Header{
|
||||||
|
"Authorization": {fmt.Sprintf("Bearer %s", token)},
|
||||||
|
"User-Agent": {mautrix.DefaultUserAgent},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if method == http.MethodPut || method == http.MethodPost {
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeContent(into *http.Request, body any) error {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := json.NewEncoder(&buf).Encode(body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to encode request: %w", err)
|
||||||
|
}
|
||||||
|
into.Body = io.NopCloser(&buf)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func doRequest(req *http.Request, reqData, resp any) (err error) {
|
||||||
|
if reqData != nil {
|
||||||
|
err = encodeContent(req, reqData)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r, err := cli.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send request: %w", err)
|
||||||
|
}
|
||||||
|
defer r.Body.Close()
|
||||||
|
if r.StatusCode < 200 || r.StatusCode >= 300 {
|
||||||
|
var body map[string]any
|
||||||
|
_ = json.NewDecoder(r.Body).Decode(&body)
|
||||||
|
if body != nil {
|
||||||
|
retryCount, ok := body["retries"].(float64)
|
||||||
|
if ok && retryCount > 0 && r.StatusCode == 403 && req.URL.Path == "/user/login/response" {
|
||||||
|
return fmt.Errorf("%w (%d retries left)", ErrInvalidLoginCode, int(retryCount))
|
||||||
|
}
|
||||||
|
errorMsg, ok := body["error"].(string)
|
||||||
|
if ok {
|
||||||
|
return fmt.Errorf("server returned error (HTTP %d): %s", r.StatusCode, errorMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("unexpected status code %d", r.StatusCode)
|
||||||
|
}
|
||||||
|
if resp != nil {
|
||||||
|
err = json.NewDecoder(r.Body).Decode(resp)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error decoding response: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
59
beeper/login.go
Normal file
59
beeper/login.go
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
package beeper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RespStartLogin struct {
|
||||||
|
RequestID string `json:"request"`
|
||||||
|
Type []string `json:"type"`
|
||||||
|
Expires time.Time `json:"expires"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReqSendLoginEmail struct {
|
||||||
|
RequestID string `json:"request"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReqSendLoginCode struct {
|
||||||
|
RequestID string `json:"request"`
|
||||||
|
Code string `json:"response"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RespSendLoginCode struct {
|
||||||
|
LoginToken string `json:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrInvalidLoginCode = fmt.Errorf("invalid login code")
|
||||||
|
|
||||||
|
const loginAuth = "BEEPER-PRIVATE-API-PLEASE-DONT-USE"
|
||||||
|
|
||||||
|
func StartLogin() (resp *RespStartLogin, err error) {
|
||||||
|
req := newRequest(loginAuth, http.MethodPost, "/user/login")
|
||||||
|
req.Body = io.NopCloser(bytes.NewReader([]byte("{}")))
|
||||||
|
err = doRequest(req, nil, &resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendLoginEmail(request, email string) error {
|
||||||
|
req := newRequest(loginAuth, http.MethodPost, "/user/login/email")
|
||||||
|
reqData := &ReqSendLoginEmail{
|
||||||
|
RequestID: request,
|
||||||
|
Email: email,
|
||||||
|
}
|
||||||
|
return doRequest(req, reqData, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendLoginCode(request, code string) (resp *RespSendLoginCode, err error) {
|
||||||
|
req := newRequest(loginAuth, http.MethodPost, "/user/login/response")
|
||||||
|
reqData := &ReqSendLoginCode{
|
||||||
|
RequestID: request,
|
||||||
|
Code: code,
|
||||||
|
}
|
||||||
|
err = doRequest(req, reqData, &resp)
|
||||||
|
return
|
||||||
|
}
|
|
@ -50,6 +50,7 @@ type MatrixContainer interface {
|
||||||
Stop()
|
Stop()
|
||||||
|
|
||||||
Login(user, password string) error
|
Login(user, password string) error
|
||||||
|
BeeperLogin(session, code string) error
|
||||||
Logout()
|
Logout()
|
||||||
UIAFallback(authType mautrix.AuthType, sessionID string) error
|
UIAFallback(authType mautrix.AuthType, sessionID string) error
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,7 @@ import (
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
"maunium.net/go/mautrix/pushrules"
|
"maunium.net/go/mautrix/pushrules"
|
||||||
|
|
||||||
|
"maunium.net/go/gomuks/beeper"
|
||||||
"maunium.net/go/gomuks/config"
|
"maunium.net/go/gomuks/config"
|
||||||
"maunium.net/go/gomuks/debug"
|
"maunium.net/go/gomuks/debug"
|
||||||
ifc "maunium.net/go/gomuks/interface"
|
ifc "maunium.net/go/gomuks/interface"
|
||||||
|
@ -190,6 +191,22 @@ func (c *Container) Initialized() bool {
|
||||||
return c.client != nil
|
return c.client != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Container) JWTLogin(token string) error {
|
||||||
|
resp, err := c.client.Login(&mautrix.ReqLogin{
|
||||||
|
Type: "org.matrix.login.jwt",
|
||||||
|
Token: token,
|
||||||
|
InitialDeviceDisplayName: "gomuks",
|
||||||
|
|
||||||
|
StoreCredentials: true,
|
||||||
|
StoreHomeserverURL: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.finishLogin(resp)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Container) PasswordLogin(user, password string) error {
|
func (c *Container) PasswordLogin(user, password string) error {
|
||||||
resp, err := c.client.Login(&mautrix.ReqLogin{
|
resp, err := c.client.Login(&mautrix.ReqLogin{
|
||||||
Type: "m.login.password",
|
Type: "m.login.password",
|
||||||
|
@ -288,6 +305,15 @@ func (c *Container) SingleSignOn() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Container) BeeperLogin(request, code string) error {
|
||||||
|
resp, err := beeper.SendLoginCode(request, code)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JWTLogin(resp.LoginToken)
|
||||||
|
}
|
||||||
|
|
||||||
// Login sends a password login request with the given username and password.
|
// Login sends a password login request with the given username and password.
|
||||||
func (c *Container) Login(user, password string) error {
|
func (c *Container) Login(user, password string) error {
|
||||||
resp, err := c.client.GetLoginFlows()
|
resp, err := c.client.GetLoginFlows()
|
||||||
|
|
|
@ -25,8 +25,8 @@ import (
|
||||||
"go.mau.fi/tcell"
|
"go.mau.fi/tcell"
|
||||||
|
|
||||||
"maunium.net/go/mautrix"
|
"maunium.net/go/mautrix"
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
|
|
||||||
|
"maunium.net/go/gomuks/beeper"
|
||||||
"maunium.net/go/gomuks/config"
|
"maunium.net/go/gomuks/config"
|
||||||
"maunium.net/go/gomuks/debug"
|
"maunium.net/go/gomuks/debug"
|
||||||
ifc "maunium.net/go/gomuks/interface"
|
ifc "maunium.net/go/gomuks/interface"
|
||||||
|
@ -37,19 +37,18 @@ type LoginView struct {
|
||||||
|
|
||||||
container *mauview.Centerer
|
container *mauview.Centerer
|
||||||
|
|
||||||
homeserverLabel *mauview.TextField
|
emailLabel *mauview.TextField
|
||||||
usernameLabel *mauview.TextField
|
codeLabel *mauview.TextField
|
||||||
passwordLabel *mauview.TextField
|
|
||||||
|
|
||||||
homeserver *mauview.InputField
|
email *mauview.InputField
|
||||||
username *mauview.InputField
|
code *mauview.InputField
|
||||||
password *mauview.InputField
|
error *mauview.TextView
|
||||||
error *mauview.TextView
|
|
||||||
|
|
||||||
loginButton *mauview.Button
|
loginButton *mauview.Button
|
||||||
quitButton *mauview.Button
|
quitButton *mauview.Button
|
||||||
|
|
||||||
loading bool
|
loading bool
|
||||||
|
session string
|
||||||
|
|
||||||
matrix ifc.MatrixContainer
|
matrix ifc.MatrixContainer
|
||||||
config *config.Config
|
config *config.Config
|
||||||
|
@ -60,13 +59,11 @@ func (ui *GomuksUI) NewLoginView() mauview.Component {
|
||||||
view := &LoginView{
|
view := &LoginView{
|
||||||
Form: mauview.NewForm(),
|
Form: mauview.NewForm(),
|
||||||
|
|
||||||
usernameLabel: mauview.NewTextField().SetText("Username"),
|
emailLabel: mauview.NewTextField().SetText("Email"),
|
||||||
passwordLabel: mauview.NewTextField().SetText("Password"),
|
codeLabel: mauview.NewTextField().SetText("Code"),
|
||||||
homeserverLabel: mauview.NewTextField().SetText("Homeserver"),
|
|
||||||
|
|
||||||
username: mauview.NewInputField(),
|
email: mauview.NewInputField(),
|
||||||
password: mauview.NewInputField(),
|
code: mauview.NewInputField(),
|
||||||
homeserver: mauview.NewInputField(),
|
|
||||||
|
|
||||||
loginButton: mauview.NewButton("Login"),
|
loginButton: mauview.NewButton("Login"),
|
||||||
quitButton: mauview.NewButton("Quit"),
|
quitButton: mauview.NewButton("Quit"),
|
||||||
|
@ -76,10 +73,8 @@ func (ui *GomuksUI) NewLoginView() mauview.Component {
|
||||||
parent: ui,
|
parent: ui,
|
||||||
}
|
}
|
||||||
|
|
||||||
hs := ui.gmx.Config().HS
|
view.email.SetPlaceholder("example@example.com").SetTextColor(tcell.ColorWhite)
|
||||||
view.homeserver.SetPlaceholder("https://example.com").SetText(hs).SetTextColor(tcell.ColorWhite)
|
view.code.SetPlaceholder("123456").SetTextColor(tcell.ColorWhite)
|
||||||
view.username.SetPlaceholder("@user:example.com").SetText(string(ui.gmx.Config().UserID)).SetTextColor(tcell.ColorWhite)
|
|
||||||
view.password.SetPlaceholder("correct horse battery staple").SetMaskCharacter('*').SetTextColor(tcell.ColorWhite)
|
|
||||||
|
|
||||||
view.quitButton.
|
view.quitButton.
|
||||||
SetOnClick(func() { ui.gmx.Stop(true) }).
|
SetOnClick(func() { ui.gmx.Stop(true) }).
|
||||||
|
@ -93,45 +88,51 @@ func (ui *GomuksUI) NewLoginView() mauview.Component {
|
||||||
SetFocusedForegroundColor(tcell.ColorWhite)
|
SetFocusedForegroundColor(tcell.ColorWhite)
|
||||||
|
|
||||||
view.
|
view.
|
||||||
SetColumns([]int{1, 10, 1, 30, 1}).
|
SetColumns([]int{1, 5, 1, 30, 1}).
|
||||||
SetRows([]int{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1})
|
SetRows([]int{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1})
|
||||||
view.
|
view.
|
||||||
AddFormItem(view.username, 3, 1, 1, 1).
|
AddFormItem(view.email, 3, 1, 1, 1).
|
||||||
AddFormItem(view.password, 3, 3, 1, 1).
|
AddFormItem(view.code, 3, 3, 1, 1).
|
||||||
AddFormItem(view.homeserver, 3, 5, 1, 1).
|
AddFormItem(view.loginButton, 1, 5, 3, 1).
|
||||||
AddFormItem(view.loginButton, 1, 7, 3, 1).
|
AddFormItem(view.quitButton, 1, 7, 3, 1).
|
||||||
AddFormItem(view.quitButton, 1, 9, 3, 1).
|
AddComponent(view.emailLabel, 1, 1, 1, 1).
|
||||||
AddComponent(view.usernameLabel, 1, 1, 1, 1).
|
AddComponent(view.codeLabel, 1, 3, 1, 1)
|
||||||
AddComponent(view.passwordLabel, 1, 3, 1, 1).
|
|
||||||
AddComponent(view.homeserverLabel, 1, 5, 1, 1)
|
|
||||||
view.SetOnFocusChanged(view.focusChanged)
|
view.SetOnFocusChanged(view.focusChanged)
|
||||||
view.FocusNextItem()
|
view.FocusNextItem()
|
||||||
ui.loginView = view
|
ui.loginView = view
|
||||||
|
|
||||||
view.container = mauview.Center(mauview.NewBox(view).SetTitle("Log in to Matrix"), 45, 13)
|
view.container = mauview.Center(mauview.NewBox(view).SetTitle("Log in to Matrix"), 40, 11)
|
||||||
view.container.SetAlwaysFocusChild(true)
|
view.container.SetAlwaysFocusChild(true)
|
||||||
return view.container
|
return view.container
|
||||||
}
|
}
|
||||||
|
|
||||||
func (view *LoginView) resolveWellKnown() {
|
func (view *LoginView) emailAuthFlow() {
|
||||||
_, homeserver, err := id.UserID(view.username.GetText()).Parse()
|
view.Error("")
|
||||||
|
|
||||||
|
resp, err := beeper.StartLogin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
view.code.SetText("")
|
||||||
|
view.Error(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
view.homeserver.SetText("Resolving...")
|
view.code.SetPlaceholder("Talking to Beeper servers…")
|
||||||
resp, err := mautrix.DiscoverClientAPI(homeserver)
|
view.parent.Render()
|
||||||
|
|
||||||
|
err = beeper.SendLoginEmail(resp.RequestID, view.email.GetText())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
view.homeserver.SetText("")
|
view.code.SetText("")
|
||||||
|
view.code.SetPlaceholder("123456")
|
||||||
view.Error(err.Error())
|
view.Error(err.Error())
|
||||||
} else if resp != nil {
|
return
|
||||||
view.homeserver.SetText(resp.Homeserver.BaseURL)
|
|
||||||
view.parent.Render()
|
|
||||||
}
|
}
|
||||||
|
view.session = resp.RequestID
|
||||||
|
view.code.SetPlaceholder("Check your inbox…")
|
||||||
|
view.parent.Render()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (view *LoginView) focusChanged(from, to mauview.Component) {
|
func (view *LoginView) focusChanged(from, to mauview.Component) {
|
||||||
if from == view.username && view.homeserver.GetText() == "" {
|
if from == view.email {
|
||||||
go view.resolveWellKnown()
|
go view.emailAuthFlow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,32 +140,32 @@ func (view *LoginView) Error(err string) {
|
||||||
if len(err) == 0 && view.error != nil {
|
if len(err) == 0 && view.error != nil {
|
||||||
debug.Print("Hiding error")
|
debug.Print("Hiding error")
|
||||||
view.RemoveComponent(view.error)
|
view.RemoveComponent(view.error)
|
||||||
view.container.SetHeight(13)
|
view.container.SetHeight(11)
|
||||||
view.SetRows([]int{1, 1, 1, 1, 1, 1, 1, 1, 1})
|
view.SetRows([]int{1, 1, 1, 1, 1, 1, 1, 1, 1})
|
||||||
view.error = nil
|
view.error = nil
|
||||||
} else if len(err) > 0 {
|
} else if len(err) > 0 {
|
||||||
debug.Print("Showing error", err)
|
debug.Print("Showing error", err)
|
||||||
if view.error == nil {
|
if view.error == nil {
|
||||||
view.error = mauview.NewTextView().SetTextColor(tcell.ColorRed)
|
view.error = mauview.NewTextView().SetTextColor(tcell.ColorRed)
|
||||||
view.AddComponent(view.error, 1, 11, 3, 1)
|
view.AddComponent(view.error, 1, 9, 3, 1)
|
||||||
}
|
}
|
||||||
view.error.SetText(err)
|
view.error.SetText(err)
|
||||||
errorHeight := int(math.Ceil(float64(runewidth.StringWidth(err)) / 41))
|
errorHeight := int(math.Ceil(float64(runewidth.StringWidth(err)) / 41))
|
||||||
view.container.SetHeight(14 + errorHeight)
|
view.container.SetHeight(12 + errorHeight)
|
||||||
view.SetRow(11, errorHeight)
|
view.SetRow(11, errorHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
view.parent.Render()
|
view.parent.Render()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (view *LoginView) actuallyLogin(hs, mxid, password string) {
|
func (view *LoginView) actuallyLogin(session, code string) {
|
||||||
debug.Printf("Logging into %s as %s...", hs, mxid)
|
debug.Printf("Logging into Beeper with code %s...", code)
|
||||||
view.config.HS = hs
|
view.config.HS = "https://matrix.beeper.com"
|
||||||
|
|
||||||
if err := view.matrix.InitClient(false); err != nil {
|
if err := view.matrix.InitClient(false); err != nil {
|
||||||
debug.Print("Init error:", err)
|
debug.Print("Init error:", err)
|
||||||
view.Error(err.Error())
|
view.Error(err.Error())
|
||||||
} else if err = view.matrix.Login(mxid, password); err != nil {
|
} else if err = view.matrix.BeeperLogin(session, code); err != nil {
|
||||||
if httpErr, ok := err.(mautrix.HTTPError); ok {
|
if httpErr, ok := err.(mautrix.HTTPError); ok {
|
||||||
if httpErr.RespError != nil && len(httpErr.RespError.Err) > 0 {
|
if httpErr.RespError != nil && len(httpErr.RespError.Err) > 0 {
|
||||||
view.Error(httpErr.RespError.Err)
|
view.Error(httpErr.RespError.Err)
|
||||||
|
@ -186,11 +187,9 @@ func (view *LoginView) Login() {
|
||||||
if view.loading {
|
if view.loading {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
hs := view.homeserver.GetText()
|
code := view.code.GetText()
|
||||||
mxid := view.username.GetText()
|
|
||||||
password := view.password.GetText()
|
|
||||||
|
|
||||||
view.loading = true
|
view.loading = true
|
||||||
view.loginButton.SetText("Logging in...")
|
view.loginButton.SetText("Logging in...")
|
||||||
go view.actuallyLogin(hs, mxid, password)
|
go view.actuallyLogin(view.session, code)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue