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()
|
||||
|
||||
Login(user, password string) error
|
||||
BeeperLogin(session, code string) error
|
||||
Logout()
|
||||
UIAFallback(authType mautrix.AuthType, sessionID string) error
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ import (
|
|||
"maunium.net/go/mautrix/id"
|
||||
"maunium.net/go/mautrix/pushrules"
|
||||
|
||||
"maunium.net/go/gomuks/beeper"
|
||||
"maunium.net/go/gomuks/config"
|
||||
"maunium.net/go/gomuks/debug"
|
||||
ifc "maunium.net/go/gomuks/interface"
|
||||
|
@ -190,6 +191,22 @@ func (c *Container) Initialized() bool {
|
|||
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 {
|
||||
resp, err := c.client.Login(&mautrix.ReqLogin{
|
||||
Type: "m.login.password",
|
||||
|
@ -288,6 +305,15 @@ func (c *Container) SingleSignOn() error {
|
|||
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.
|
||||
func (c *Container) Login(user, password string) error {
|
||||
resp, err := c.client.GetLoginFlows()
|
||||
|
|
|
@ -25,8 +25,8 @@ import (
|
|||
"go.mau.fi/tcell"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
"maunium.net/go/gomuks/beeper"
|
||||
"maunium.net/go/gomuks/config"
|
||||
"maunium.net/go/gomuks/debug"
|
||||
ifc "maunium.net/go/gomuks/interface"
|
||||
|
@ -37,19 +37,18 @@ type LoginView struct {
|
|||
|
||||
container *mauview.Centerer
|
||||
|
||||
homeserverLabel *mauview.TextField
|
||||
usernameLabel *mauview.TextField
|
||||
passwordLabel *mauview.TextField
|
||||
emailLabel *mauview.TextField
|
||||
codeLabel *mauview.TextField
|
||||
|
||||
homeserver *mauview.InputField
|
||||
username *mauview.InputField
|
||||
password *mauview.InputField
|
||||
error *mauview.TextView
|
||||
email *mauview.InputField
|
||||
code *mauview.InputField
|
||||
error *mauview.TextView
|
||||
|
||||
loginButton *mauview.Button
|
||||
quitButton *mauview.Button
|
||||
|
||||
loading bool
|
||||
session string
|
||||
|
||||
matrix ifc.MatrixContainer
|
||||
config *config.Config
|
||||
|
@ -60,13 +59,11 @@ func (ui *GomuksUI) NewLoginView() mauview.Component {
|
|||
view := &LoginView{
|
||||
Form: mauview.NewForm(),
|
||||
|
||||
usernameLabel: mauview.NewTextField().SetText("Username"),
|
||||
passwordLabel: mauview.NewTextField().SetText("Password"),
|
||||
homeserverLabel: mauview.NewTextField().SetText("Homeserver"),
|
||||
emailLabel: mauview.NewTextField().SetText("Email"),
|
||||
codeLabel: mauview.NewTextField().SetText("Code"),
|
||||
|
||||
username: mauview.NewInputField(),
|
||||
password: mauview.NewInputField(),
|
||||
homeserver: mauview.NewInputField(),
|
||||
email: mauview.NewInputField(),
|
||||
code: mauview.NewInputField(),
|
||||
|
||||
loginButton: mauview.NewButton("Login"),
|
||||
quitButton: mauview.NewButton("Quit"),
|
||||
|
@ -76,10 +73,8 @@ func (ui *GomuksUI) NewLoginView() mauview.Component {
|
|||
parent: ui,
|
||||
}
|
||||
|
||||
hs := ui.gmx.Config().HS
|
||||
view.homeserver.SetPlaceholder("https://example.com").SetText(hs).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.email.SetPlaceholder("example@example.com").SetTextColor(tcell.ColorWhite)
|
||||
view.code.SetPlaceholder("123456").SetTextColor(tcell.ColorWhite)
|
||||
|
||||
view.quitButton.
|
||||
SetOnClick(func() { ui.gmx.Stop(true) }).
|
||||
|
@ -93,45 +88,51 @@ func (ui *GomuksUI) NewLoginView() mauview.Component {
|
|||
SetFocusedForegroundColor(tcell.ColorWhite)
|
||||
|
||||
view.
|
||||
SetColumns([]int{1, 10, 1, 30, 1}).
|
||||
SetRows([]int{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1})
|
||||
SetColumns([]int{1, 5, 1, 30, 1}).
|
||||
SetRows([]int{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1})
|
||||
view.
|
||||
AddFormItem(view.username, 3, 1, 1, 1).
|
||||
AddFormItem(view.password, 3, 3, 1, 1).
|
||||
AddFormItem(view.homeserver, 3, 5, 1, 1).
|
||||
AddFormItem(view.loginButton, 1, 7, 3, 1).
|
||||
AddFormItem(view.quitButton, 1, 9, 3, 1).
|
||||
AddComponent(view.usernameLabel, 1, 1, 1, 1).
|
||||
AddComponent(view.passwordLabel, 1, 3, 1, 1).
|
||||
AddComponent(view.homeserverLabel, 1, 5, 1, 1)
|
||||
AddFormItem(view.email, 3, 1, 1, 1).
|
||||
AddFormItem(view.code, 3, 3, 1, 1).
|
||||
AddFormItem(view.loginButton, 1, 5, 3, 1).
|
||||
AddFormItem(view.quitButton, 1, 7, 3, 1).
|
||||
AddComponent(view.emailLabel, 1, 1, 1, 1).
|
||||
AddComponent(view.codeLabel, 1, 3, 1, 1)
|
||||
view.SetOnFocusChanged(view.focusChanged)
|
||||
view.FocusNextItem()
|
||||
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)
|
||||
return view.container
|
||||
}
|
||||
|
||||
func (view *LoginView) resolveWellKnown() {
|
||||
_, homeserver, err := id.UserID(view.username.GetText()).Parse()
|
||||
func (view *LoginView) emailAuthFlow() {
|
||||
view.Error("")
|
||||
|
||||
resp, err := beeper.StartLogin()
|
||||
if err != nil {
|
||||
view.code.SetText("")
|
||||
view.Error(err.Error())
|
||||
return
|
||||
}
|
||||
view.homeserver.SetText("Resolving...")
|
||||
resp, err := mautrix.DiscoverClientAPI(homeserver)
|
||||
view.code.SetPlaceholder("Talking to Beeper servers…")
|
||||
view.parent.Render()
|
||||
|
||||
err = beeper.SendLoginEmail(resp.RequestID, view.email.GetText())
|
||||
if err != nil {
|
||||
view.homeserver.SetText("")
|
||||
view.code.SetText("")
|
||||
view.code.SetPlaceholder("123456")
|
||||
view.Error(err.Error())
|
||||
} else if resp != nil {
|
||||
view.homeserver.SetText(resp.Homeserver.BaseURL)
|
||||
view.parent.Render()
|
||||
return
|
||||
}
|
||||
view.session = resp.RequestID
|
||||
view.code.SetPlaceholder("Check your inbox…")
|
||||
view.parent.Render()
|
||||
}
|
||||
|
||||
func (view *LoginView) focusChanged(from, to mauview.Component) {
|
||||
if from == view.username && view.homeserver.GetText() == "" {
|
||||
go view.resolveWellKnown()
|
||||
if from == view.email {
|
||||
go view.emailAuthFlow()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -139,32 +140,32 @@ func (view *LoginView) Error(err string) {
|
|||
if len(err) == 0 && view.error != nil {
|
||||
debug.Print("Hiding 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.error = nil
|
||||
} else if len(err) > 0 {
|
||||
debug.Print("Showing error", err)
|
||||
if view.error == nil {
|
||||
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)
|
||||
errorHeight := int(math.Ceil(float64(runewidth.StringWidth(err)) / 41))
|
||||
view.container.SetHeight(14 + errorHeight)
|
||||
view.container.SetHeight(12 + errorHeight)
|
||||
view.SetRow(11, errorHeight)
|
||||
}
|
||||
|
||||
view.parent.Render()
|
||||
}
|
||||
|
||||
func (view *LoginView) actuallyLogin(hs, mxid, password string) {
|
||||
debug.Printf("Logging into %s as %s...", hs, mxid)
|
||||
view.config.HS = hs
|
||||
func (view *LoginView) actuallyLogin(session, code string) {
|
||||
debug.Printf("Logging into Beeper with code %s...", code)
|
||||
view.config.HS = "https://matrix.beeper.com"
|
||||
|
||||
if err := view.matrix.InitClient(false); err != nil {
|
||||
debug.Print("Init error:", err)
|
||||
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.RespError != nil && len(httpErr.RespError.Err) > 0 {
|
||||
view.Error(httpErr.RespError.Err)
|
||||
|
@ -186,11 +187,9 @@ func (view *LoginView) Login() {
|
|||
if view.loading {
|
||||
return
|
||||
}
|
||||
hs := view.homeserver.GetText()
|
||||
mxid := view.username.GetText()
|
||||
password := view.password.GetText()
|
||||
code := view.code.GetText()
|
||||
|
||||
view.loading = true
|
||||
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