From bbf53fb28bba2591ba38c373da5f65a170f0e793 Mon Sep 17 00:00:00 2001 From: Sumner Evans Date: Tue, 27 Aug 2024 15:32:07 -0600 Subject: [PATCH] provisioning: implement legacy endpoints Signed-off-by: Sumner Evans --- cmd/mautrix-telegram/legacyprovisioning.go | 182 +++++++++++++++++++++ cmd/mautrix-telegram/main.go | 11 +- go.mod | 2 +- go.sum | 4 +- pkg/connector/connector.go | 6 - pkg/connector/login.go | 28 ++-- 6 files changed, 209 insertions(+), 24 deletions(-) create mode 100644 cmd/mautrix-telegram/legacyprovisioning.go diff --git a/cmd/mautrix-telegram/legacyprovisioning.go b/cmd/mautrix-telegram/legacyprovisioning.go new file mode 100644 index 00000000..9ae1e77c --- /dev/null +++ b/cmd/mautrix-telegram/legacyprovisioning.go @@ -0,0 +1,182 @@ +// mautrix-telegram - A Matrix-Telegram puppeting bridge. +// Copyright (C) 2024 Sumner Evans +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "sync" + + "github.com/rs/zerolog" + "go.mau.fi/util/exhttp" + "maunium.net/go/mautrix/bridgev2" + "maunium.net/go/mautrix/id" + + "go.mau.fi/mautrix-telegram/pkg/connector" +) + +type response struct { + Username id.UserID `json:"username,omitempty"` + State string `json:"state,omitempty"` + Message string `json:"message,omitempty"` + Error string `json:"error,omitempty"` + ErrCode string `json:"errcode,omitempty"` +} + +func (r response) WithState(state string) response { + r.State = state + return r +} + +func (r response) WithMessage(message string) response { + r.Message = message + return r +} + +func (r response) WithError(errCode, error string) response { + r.ErrCode = errCode + r.Error = error + return r +} + +type legacyLogin struct { + Process bridgev2.LoginProcess + NextStep *bridgev2.LoginStep +} + +var inflightLegacyLoginsLock sync.RWMutex +var inflightLegacyLogins = map[id.UserID]*legacyLogin{} + +func legacyProvLoginRequestCode(w http.ResponseWriter, r *http.Request) { + log := zerolog.Ctx(r.Context()).With().Str("prov_step", "request_code").Logger() + ctx := log.WithContext(r.Context()) + + user := m.Matrix.Provisioning.GetUser(r) + resp := response{Username: user.MXID, State: "request"} + + legacyProvRequestCodeReq := map[string]string{} + if err := json.NewDecoder(r.Body).Decode(&legacyProvRequestCodeReq); err != nil { + exhttp.WriteJSONResponse(w, http.StatusBadRequest, resp.WithError("request_body_invalid", "Request body is invalid")) + } else if phone, ok := legacyProvRequestCodeReq["phone"]; !ok || phone == "" { + exhttp.WriteJSONResponse(w, http.StatusBadRequest, resp.WithError("phone_missing", "Phone number missing")) + } else if loginProcess, err := c.CreateLogin(ctx, user, connector.LoginFlowIDPhone); err != nil { + exhttp.WriteJSONResponse(w, http.StatusInternalServerError, resp.WithError("create_login_failed", fmt.Sprintf("Failed to create a phone number login process: %s", err.Error()))) + } else if firstStep, err := loginProcess.Start(ctx); err != nil { + exhttp.WriteJSONResponse(w, http.StatusInternalServerError, resp.WithError("start_login_failed", fmt.Sprintf("Failed to start login process: %s", err.Error()))) + } else if firstStep.StepID != connector.LoginStepIDPhoneNumber { + exhttp.WriteJSONResponse(w, http.StatusInternalServerError, resp.WithError("unexpected_step", fmt.Sprintf("Unexpected first step %s", firstStep.StepID))) + } else if nextStep, err := loginProcess.(bridgev2.LoginProcessUserInput).SubmitUserInput(ctx, map[string]string{connector.LoginStepIDPhoneNumber: phone}); err != nil { + exhttp.WriteJSONResponse(w, http.StatusBadRequest, resp.WithError("request_code_failed", fmt.Sprintf("Failed to request code: %s", err.Error()))) + } else if nextStep.StepID != connector.LoginStepIDCode { + exhttp.WriteJSONResponse(w, http.StatusInternalServerError, resp.WithError("unexpected_step", fmt.Sprintf("Unexpected step %s", nextStep.StepID))) + } else { + inflightLegacyLoginsLock.Lock() + inflightLegacyLogins[user.MXID] = &legacyLogin{ + Process: loginProcess, + NextStep: nextStep, + } + inflightLegacyLoginsLock.Unlock() + exhttp.WriteJSONResponse(w, http.StatusOK, resp. + WithState("code"). + WithMessage("Code requested successfully. Check your SMS or Telegram app and enter the code below."), + ) + } +} + +func legacyProvLoginSendCode(w http.ResponseWriter, r *http.Request) { + log := zerolog.Ctx(r.Context()).With().Str("prov_step", "send_code").Logger() + ctx := log.WithContext(r.Context()) + + user := m.Matrix.Provisioning.GetUser(r) + resp := response{Username: user.MXID, State: "code"} + + legacyProvSendCodeReq := map[string]string{} + if inflightLogin, ok := inflightLegacyLogins[user.MXID]; !ok { + exhttp.WriteJSONResponse(w, http.StatusBadRequest, resp.WithError("no_login", "No login process in progress")) + } else if inflightLogin.NextStep.StepID != connector.LoginStepIDCode { + exhttp.WriteJSONResponse(w, http.StatusBadRequest, resp.WithError("unexpected_step", fmt.Sprintf("Unexpected step %s", inflightLogin.NextStep.StepID))) + } else if err := json.NewDecoder(r.Body).Decode(&legacyProvSendCodeReq); err != nil { + exhttp.WriteJSONResponse(w, http.StatusBadRequest, resp.WithError("request_body_invalid", "Request body is invalid")) + } else if code, ok := legacyProvSendCodeReq["code"]; !ok || code == "" { + exhttp.WriteJSONResponse(w, http.StatusBadRequest, resp.WithError("phone_code_missing", "You must provide the code from your phone.")) + } else if nextStep, err := inflightLogin.Process.(bridgev2.LoginProcessUserInput).SubmitUserInput(ctx, map[string]string{connector.LoginStepIDCode: code}); err != nil { + exhttp.WriteJSONResponse(w, http.StatusBadRequest, resp.WithError("send_code_failed", fmt.Sprintf("Failed to send code: %s", err.Error()))) + } else if nextStep.StepID == connector.LoginStepIDPassword { + inflightLegacyLoginsLock.Lock() + defer inflightLegacyLoginsLock.Unlock() + inflightLegacyLogins[user.MXID] = &legacyLogin{ + Process: inflightLogin.Process, + NextStep: nextStep, + } + exhttp.WriteJSONResponse(w, http.StatusAccepted, resp. + WithState("password"). + WithMessage("Code accepted, but you have 2-factor authentication enabled. Please enter your password."), + ) + return // Don't delete the inflight login yet, we need to submit the password. + } else if nextStep.StepID == connector.LoginStepIDComplete { + exhttp.WriteJSONResponse(w, http.StatusOK, resp.WithState("logged-in")) + } else { + exhttp.WriteJSONResponse(w, http.StatusInternalServerError, resp.WithError("unexpected_step", fmt.Sprintf("Unexpected step %s", nextStep.StepID))) + } + + // If we got here, then there was an error, or the login is complete. + // Delete the in-flight login. + inflightLegacyLoginsLock.Lock() + delete(inflightLegacyLogins, user.MXID) + inflightLegacyLoginsLock.Unlock() +} + +func legacyProvLoginSendPassword(w http.ResponseWriter, r *http.Request) { + log := zerolog.Ctx(r.Context()).With().Str("prov_step", "send_password").Logger() + ctx := log.WithContext(r.Context()) + + user := m.Matrix.Provisioning.GetUser(r) + resp := response{Username: user.MXID, State: "password"} + + legacyProvSendPasswordReq := map[string]string{} + if inflightLogin, ok := inflightLegacyLogins[user.MXID]; !ok { + exhttp.WriteJSONResponse(w, http.StatusBadRequest, resp.WithError("no_login", "No login process in progress")) + } else if inflightLogin.NextStep.StepID != connector.LoginStepIDPassword { + exhttp.WriteJSONResponse(w, http.StatusBadRequest, resp.WithError("unexpected_step", fmt.Sprintf("Unexpected step %s", inflightLogin.NextStep.StepID))) + } else if err := json.NewDecoder(r.Body).Decode(&legacyProvSendPasswordReq); err != nil { + exhttp.WriteJSONResponse(w, http.StatusBadRequest, resp.WithError("request_body_invalid", "Request body is invalid")) + } else if password, ok := legacyProvSendPasswordReq["password"]; !ok || password == "" { + exhttp.WriteJSONResponse(w, http.StatusBadRequest, resp.WithError("password_missing", "You must provide your password.")) + } else if nextStep, err := inflightLogin.Process.(bridgev2.LoginProcessUserInput).SubmitUserInput(ctx, map[string]string{connector.LoginStepIDPassword: password}); err != nil { + exhttp.WriteJSONResponse(w, http.StatusBadRequest, resp.WithError("send_password_failed", fmt.Sprintf("Failed to send password: %s", err.Error()))) + } else if nextStep.StepID == connector.LoginStepIDComplete { + exhttp.WriteJSONResponse(w, http.StatusOK, resp.WithState("logged-in")) + } else { + exhttp.WriteJSONResponse(w, http.StatusInternalServerError, resp.WithError("unexpected_step", fmt.Sprintf("Unexpected step %s", nextStep.StepID))) + } + + // If we got here, then there was an error, or the login is complete. + // Delete the in-flight login. + inflightLegacyLoginsLock.Lock() + delete(inflightLegacyLogins, user.MXID) + inflightLegacyLoginsLock.Unlock() +} + +func legacyProvLogout(w http.ResponseWriter, r *http.Request) { + user := m.Matrix.Provisioning.GetUser(r) + logins := user.GetUserLogins() + for _, login := range logins { + login.Logout(r.Context()) + } + exhttp.WriteEmptyJSONResponse(w, http.StatusOK) +} diff --git a/cmd/mautrix-telegram/main.go b/cmd/mautrix-telegram/main.go index fa269e24..1e122a86 100644 --- a/cmd/mautrix-telegram/main.go +++ b/cmd/mautrix-telegram/main.go @@ -36,7 +36,7 @@ var ( BuildTime = "unknown" ) -var c = connector.NewConnector() +var c = &connector.TelegramConnector{Config: &connector.TelegramConfig{}} var m = mxmain.BridgeMain{ Name: "mautrix-telegram", URL: "https://github.com/mautrix/telegram", @@ -58,6 +58,15 @@ func init() { func main() { bridgeconfig.HackyMigrateLegacyNetworkConfig = migrateLegacyConfig versionWithoutCommit := m.Version + m.PostStart = func() { + if m.Matrix.Provisioning != nil { + // m.Matrix.Provisioning.Router.HandleFunc("/v1/user/{userID}/login/qr", legacyProvLoginQR) + m.Matrix.Provisioning.Router.HandleFunc("/v1/user/{userID}/login/request_code", legacyProvLoginRequestCode) + m.Matrix.Provisioning.Router.HandleFunc("/v1/user/{userID}/login/send_code", legacyProvLoginSendCode) + m.Matrix.Provisioning.Router.HandleFunc("/v1/user/{userID}/login/send_password", legacyProvLoginSendPassword) + m.Matrix.Provisioning.Router.HandleFunc("/v1/user/{userID}/logout", legacyProvLogout) + } + } m.PostInit = func() { if c.Config.DeviceInfo.AppVersion == "auto" { c.Config.DeviceInfo.AppVersion = versionWithoutCommit diff --git a/go.mod b/go.mod index 94344a91..01951101 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/gotd/td v0.105.0 github.com/rs/zerolog v1.33.0 github.com/stretchr/testify v1.9.0 - go.mau.fi/util v0.7.1-0.20240827120252-821b350d3b0b + go.mau.fi/util v0.7.1-0.20240827213018-b75de7efab85 go.mau.fi/zerozap v0.1.1 go.uber.org/zap v1.27.0 golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa diff --git a/go.sum b/go.sum index 17a322e2..f3e84e16 100644 --- a/go.sum +++ b/go.sum @@ -73,8 +73,8 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg= github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -go.mau.fi/util v0.7.1-0.20240827120252-821b350d3b0b h1:P2ss5SCNFTQ0X471E/nxEJTbSpXjwvjoSEws3zrOgU0= -go.mau.fi/util v0.7.1-0.20240827120252-821b350d3b0b/go.mod h1:WuAOOV0O/otkxGkFUvfv/XE2ztegaoyM15ovS6SYbf4= +go.mau.fi/util v0.7.1-0.20240827213018-b75de7efab85 h1:9JTm4++sVl/AF0feWhyxHhcU2jGvWlvOkYiAOsO1Gmw= +go.mau.fi/util v0.7.1-0.20240827213018-b75de7efab85/go.mod h1:WuAOOV0O/otkxGkFUvfv/XE2ztegaoyM15ovS6SYbf4= go.mau.fi/zeroconfig v0.1.3 h1:As9wYDKmktjmNZW5i1vn8zvJlmGKHeVxHVIBMXsm4kM= go.mau.fi/zeroconfig v0.1.3/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70= go.mau.fi/zerozap v0.1.1 h1:mxE/dW4wtkqBYOXOEEzXldk5qKB+ahsZXjoTGnvEhZQ= diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index e746913e..3f19155b 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -38,12 +38,6 @@ var _ bridgev2.NetworkConnector = (*TelegramConnector)(nil) // var _ bridgev2.MaxFileSizeingNetwork = (*TelegramConnector)(nil) -func NewConnector() *TelegramConnector { - return &TelegramConnector{ - Config: &TelegramConfig{}, - } -} - func (tg *TelegramConnector) Init(bridge *bridgev2.Bridge) { tg.Store = store.NewStore(bridge.DB.Database, dbutil.ZeroLogger(bridge.Log.With().Str("db_section", "telegram").Logger())) tg.Bridge = bridge diff --git a/pkg/connector/login.go b/pkg/connector/login.go index 38a7990d..c6b2cd46 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -54,10 +54,10 @@ func (tg *TelegramConnector) CreateLogin(ctx context.Context, user *bridgev2.Use } const ( - phoneNumberStep = "fi.mau.telegram.phone_number" - codeStep = "fi.mau.telegram.code" - passwordStep = "fi.mau.telegram.password" - completeStep = "fi.mau.telegram.complete" + LoginStepIDPhoneNumber = "fi.mau.telegram.phone_number" + LoginStepIDCode = "fi.mau.telegram.code" + LoginStepIDPassword = "fi.mau.telegram.password" + LoginStepIDComplete = "fi.mau.telegram.complete" ) type PhoneLogin struct { @@ -80,13 +80,13 @@ func (p *PhoneLogin) Cancel() { func (p *PhoneLogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) { return &bridgev2.LoginStep{ Type: bridgev2.LoginStepTypeUserInput, - StepID: phoneNumberStep, + StepID: LoginStepIDPhoneNumber, Instructions: "Please enter your phone number", UserInputParams: &bridgev2.LoginUserInputParams{ Fields: []bridgev2.LoginInputDataField{ { Type: bridgev2.LoginInputFieldTypePhoneNumber, - ID: phoneNumberStep, + ID: LoginStepIDPhoneNumber, Name: "Phone Number", Description: "Include the country code with +", }, @@ -96,7 +96,7 @@ func (p *PhoneLogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) { } func (p *PhoneLogin) SubmitUserInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) { - if phone, ok := input[phoneNumberStep]; ok { + if phone, ok := input[LoginStepIDPhoneNumber]; ok { p.phone = phone p.client = telegram.NewClient(p.main.Config.APIID, p.main.Config.APIHash, telegram.Options{ CustomSessionStorage: &p.authData, @@ -116,13 +116,13 @@ func (p *PhoneLogin) SubmitUserInput(ctx context.Context, input map[string]strin p.hash = s.PhoneCodeHash return &bridgev2.LoginStep{ Type: bridgev2.LoginStepTypeUserInput, - StepID: codeStep, + StepID: LoginStepIDCode, Instructions: "Please enter the code sent to your phone", UserInputParams: &bridgev2.LoginUserInputParams{ Fields: []bridgev2.LoginInputDataField{ { Type: bridgev2.LoginInputFieldType2FACode, - ID: codeStep, + ID: LoginStepIDCode, Name: "Code", }, }, @@ -141,18 +141,18 @@ func (p *PhoneLogin) SubmitUserInput(ctx context.Context, input map[string]strin default: return nil, fmt.Errorf("unexpected sent code type: %T", sentCode) } - } else if code, ok := input[codeStep]; ok { + } else if code, ok := input[LoginStepIDCode]; ok { authorization, err := p.client.Auth().SignIn(ctx, p.phone, code, p.hash) if errors.Is(err, auth.ErrPasswordAuthNeeded) { return &bridgev2.LoginStep{ Type: bridgev2.LoginStepTypeUserInput, - StepID: passwordStep, + StepID: LoginStepIDPassword, Instructions: "Please enter your password", UserInputParams: &bridgev2.LoginUserInputParams{ Fields: []bridgev2.LoginInputDataField{ { Type: bridgev2.LoginInputFieldTypePassword, - ID: passwordStep, + ID: LoginStepIDPassword, Name: "Password", }, }, @@ -164,7 +164,7 @@ func (p *PhoneLogin) SubmitUserInput(ctx context.Context, input map[string]strin return nil, fmt.Errorf("failed to submit code: %w", err) } return p.handleAuthSuccess(ctx, authorization) - } else if password, ok := input[passwordStep]; ok { + } else if password, ok := input[LoginStepIDPassword]; ok { authorization, err := p.client.Auth().Password(ctx, password) if err != nil { return nil, fmt.Errorf("failed to submit password: %w", err) @@ -208,7 +208,7 @@ func (p *PhoneLogin) handleAuthSuccess(ctx context.Context, authorization *tg.Au }() return &bridgev2.LoginStep{ Type: bridgev2.LoginStepTypeComplete, - StepID: completeStep, + StepID: LoginStepIDComplete, Instructions: fmt.Sprintf("Successfully logged in as %d / +%s (%s)", user.ID, user.Phone, util.FormatFullName(user.FirstName, user.LastName)), CompleteParams: &bridgev2.LoginCompleteParams{ UserLoginID: ul.ID,