Use Beeper JWT login

This commit is contained in:
FIGBERT 2023-08-23 22:55:57 -07:00
parent 8fcbdddc62
commit 7e25710506
No known key found for this signature in database
GPG key ID: 67F1598D607A844B
5 changed files with 215 additions and 50 deletions

80
beeper/internal.go Normal file
View 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
View 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
}

View file

@ -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

View file

@ -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()

View file

@ -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)
}