feat: sync login email verification support
This commit is contained in:
parent
e0cabb4930
commit
9a501f900a
39 changed files with 2198 additions and 117 deletions
|
|
@ -416,8 +416,7 @@ func (r *Router) onAccountCancelPasswordEmail(ctx context.Context) (bool, error)
|
|||
}
|
||||
|
||||
// onAccountSendVerifyEmailCode 处理 account.sendVerifyEmailCode:为登录邮箱的设置/变更
|
||||
// 发送验证码。开发环境不真正发邮件、验证码任意,故此处直接持久化待确认的登录邮箱地址,
|
||||
// 由后续 verifyEmail 做确认回显。loginChange 走已登录用户,loginSetup 走登录流程中的手机号。
|
||||
// 发送邮箱验证码。loginChange 走已登录用户,loginSetup 走登录流程中的手机号 + phone_code_hash。
|
||||
func (r *Router) onAccountSendVerifyEmailCode(ctx context.Context, req *tg.AccountSendVerifyEmailCodeRequest) (*tg.AccountSentEmailCode, error) {
|
||||
if r.deps.Account == nil {
|
||||
return nil, internalErr()
|
||||
|
|
@ -435,29 +434,32 @@ func (r *Router) onAccountSendVerifyEmailCode(ctx context.Context, req *tg.Accou
|
|||
if userID == 0 {
|
||||
return nil, authKeyUnregisteredErr()
|
||||
}
|
||||
if err := r.deps.Account.SetLoginEmail(ctx, userID, email); err != nil {
|
||||
pattern, length, err := r.deps.Account.SendLoginEmailCode(ctx, userID, "", "", email, false)
|
||||
if err != nil {
|
||||
return nil, passwordErr(err)
|
||||
}
|
||||
return &tg.AccountSentEmailCode{EmailPattern: pattern, Length: length}, nil
|
||||
case *tg.EmailVerifyPurposeLoginSetup:
|
||||
if err := r.deps.Account.SetLoginEmailByPhone(ctx, p.PhoneNumber, email); err != nil {
|
||||
pattern, length, err := r.deps.Account.SendLoginEmailCode(ctx, 0, p.PhoneNumber, p.PhoneCodeHash, email, true)
|
||||
if err != nil {
|
||||
return nil, passwordErr(err)
|
||||
}
|
||||
return &tg.AccountSentEmailCode{EmailPattern: pattern, Length: length}, nil
|
||||
default:
|
||||
return nil, emailInvalidErr()
|
||||
}
|
||||
return &tg.AccountSentEmailCode{EmailPattern: domain.MaskEmail(email), Length: devCodeLength}, nil
|
||||
}
|
||||
|
||||
// onAccountVerifyEmail 处理 account.verifyEmail:确认登录邮箱(验证码任意非空即通过)。
|
||||
// onAccountVerifyEmail 处理 account.verifyEmail:确认登录邮箱验证码。
|
||||
// loginChange(已登录)返回 emailVerified{email};loginSetup(登录流程中)返回
|
||||
// emailVerifiedLogin{email, sent_code},其中 sent_code 是供客户端继续手机登录的新验证码。
|
||||
// emailVerifiedLogin{email, sent_code}。TDesktop 能消费嵌套 auth.sentCodeSuccess,
|
||||
// 直接进入注册/登录完成;DrKLO Android 12.8.1 该路径漏处理 sentCodeSuccess,
|
||||
// 临时降级为普通 emailCode sentCode,待 Android 补齐后移除。
|
||||
func (r *Router) onAccountVerifyEmail(ctx context.Context, req *tg.AccountVerifyEmailRequest) (tg.AccountEmailVerifiedClass, error) {
|
||||
if r.deps.Account == nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if strings.TrimSpace(emailVerificationCode(req.Verification)) == "" {
|
||||
return nil, emailCodeInvalidErr()
|
||||
}
|
||||
code := emailVerificationCode(req.Verification)
|
||||
switch p := req.Purpose.(type) {
|
||||
case *tg.EmailVerifyPurposeLoginChange:
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
|
|
@ -467,30 +469,36 @@ func (r *Router) onAccountVerifyEmail(ctx context.Context, req *tg.AccountVerify
|
|||
if userID == 0 {
|
||||
return nil, authKeyUnregisteredErr()
|
||||
}
|
||||
email, found, err := r.deps.Account.LoginEmail(ctx, userID)
|
||||
email, err := r.deps.Account.VerifyLoginEmail(ctx, userID, "", "", code, false)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if !found {
|
||||
return nil, emailCodeInvalidErr()
|
||||
return nil, passwordErr(err)
|
||||
}
|
||||
return &tg.AccountEmailVerified{Email: email}, nil
|
||||
case *tg.EmailVerifyPurposeLoginSetup:
|
||||
email, found, err := r.deps.Account.LoginEmailByPhone(ctx, p.PhoneNumber)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if !found {
|
||||
return nil, emailCodeInvalidErr()
|
||||
}
|
||||
if r.deps.Auth == nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
hash, err := r.deps.Auth.SendCode(ctx, p.PhoneNumber)
|
||||
email, err := r.deps.Account.VerifyLoginEmail(ctx, 0, p.PhoneNumber, p.PhoneCodeHash, code, true)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
return nil, passwordErr(err)
|
||||
}
|
||||
return &tg.AccountEmailVerifiedLogin{Email: email, SentCode: tgSentCode(hash)}, nil
|
||||
if ClientTypeFrom(ctx) == ClientTypeAndroid {
|
||||
return &tg.AccountEmailVerifiedLogin{
|
||||
Email: email,
|
||||
SentCode: tgEmailSentCode(p.PhoneCodeHash, domain.MaskEmail(email), len(strings.TrimSpace(code))),
|
||||
}, nil
|
||||
}
|
||||
u, loginMessage, needSignUp, signInErr := r.deps.Auth.SignInWithEmail(ctx, r.authzFromCtx(ctx), p.PhoneNumber, p.PhoneCodeHash, code)
|
||||
authorization, err := r.finishAuthSignIn(ctx, u, loginMessage, needSignUp, signInErr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tg.AccountEmailVerifiedLogin{
|
||||
Email: email,
|
||||
SentCode: &tg.AuthSentCodeSuccess{
|
||||
Authorization: authorization,
|
||||
},
|
||||
}, nil
|
||||
default:
|
||||
return nil, emailInvalidErr()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -241,6 +241,7 @@ func (r *Router) pushLoginTokenAccepted(ctx context.Context, target loginTokenTa
|
|||
// 若该手机号账号设置了登录邮箱,验证码改投递到邮箱,返回 sentCodeTypeEmailCode
|
||||
// (客户端据此进入"输入邮箱验证码"界面,随后用 auth.signIn 的 email_verification 完成登录)。
|
||||
func (r *Router) onAuthSendCode(ctx context.Context, req *tg.AuthSendCodeRequest) (tg.AuthSentCodeClass, error) {
|
||||
r.rememberClientAPIID(ctx, req.APIID)
|
||||
hash, err := r.deps.Auth.SendCode(ctx, req.PhoneNumber)
|
||||
if err != nil {
|
||||
if errors.Is(err, auth.ErrPhoneNumberInvalid) ||
|
||||
|
|
@ -249,35 +250,30 @@ func (r *Router) onAuthSendCode(ctx context.Context, req *tg.AuthSendCodeRequest
|
|||
}
|
||||
return nil, internalErr()
|
||||
}
|
||||
if pattern, ok := r.loginEmailPattern(ctx, req.PhoneNumber); ok {
|
||||
return tgEmailSentCode(hash, pattern), nil
|
||||
}
|
||||
return tgSentCode(hash), nil
|
||||
}
|
||||
|
||||
// loginEmailPattern 返回该手机号账号已确认登录邮箱的掩码,不存在则 ok=false。
|
||||
func (r *Router) loginEmailPattern(ctx context.Context, phone string) (string, bool) {
|
||||
if r.deps.Account == nil {
|
||||
return "", false
|
||||
}
|
||||
email, found, err := r.deps.Account.LoginEmailByPhone(ctx, phone)
|
||||
if err != nil || !found || email == "" {
|
||||
return "", false
|
||||
}
|
||||
return domain.MaskEmail(email), true
|
||||
return r.tgSentCodeForHash(ctx, hash)
|
||||
}
|
||||
|
||||
func tgSentCode(hash string) tg.AuthSentCodeClass {
|
||||
return tgSentCodeWithLength(hash, devCodeLength)
|
||||
}
|
||||
|
||||
func tgSentCodeWithLength(hash string, length int) tg.AuthSentCodeClass {
|
||||
if length <= 0 {
|
||||
length = devCodeLength
|
||||
}
|
||||
return &tg.AuthSentCode{
|
||||
Type: &tg.AuthSentCodeTypeApp{Length: devCodeLength},
|
||||
Type: &tg.AuthSentCodeTypeApp{Length: length},
|
||||
PhoneCodeHash: hash,
|
||||
}
|
||||
}
|
||||
|
||||
func tgEmailSentCode(hash, emailPattern string) tg.AuthSentCodeClass {
|
||||
func tgEmailSentCode(hash, emailPattern string, length int) tg.AuthSentCodeClass {
|
||||
if length <= 0 {
|
||||
length = devCodeLength
|
||||
}
|
||||
codeType := &tg.AuthSentCodeTypeEmailCode{
|
||||
EmailPattern: emailPattern,
|
||||
Length: devCodeLength,
|
||||
Length: length,
|
||||
}
|
||||
// reset_available_period=0 表示可立即调用 auth.resetLoginEmail(开发环境无等待期),
|
||||
// 让客户端的"无法访问邮箱?"逃生入口可用。
|
||||
|
|
@ -288,6 +284,34 @@ func tgEmailSentCode(hash, emailPattern string) tg.AuthSentCodeClass {
|
|||
}
|
||||
}
|
||||
|
||||
func tgEmailSetupRequiredSentCode(hash string) tg.AuthSentCodeClass {
|
||||
return &tg.AuthSentCode{
|
||||
Type: &tg.AuthSentCodeTypeSetUpEmailRequired{},
|
||||
PhoneCodeHash: hash,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) tgSentCodeForHash(ctx context.Context, hash string) (tg.AuthSentCodeClass, error) {
|
||||
if r.deps.Auth == nil {
|
||||
return tgSentCode(hash), nil
|
||||
}
|
||||
delivery, found, err := r.deps.Auth.CodeDelivery(ctx, hash)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if !found {
|
||||
return nil, signInErr(auth.ErrCodeExpired)
|
||||
}
|
||||
switch delivery.Kind {
|
||||
case domain.AuthCodeDeliveryEmail:
|
||||
return tgEmailSentCode(hash, delivery.EmailPattern, delivery.Length), nil
|
||||
case domain.AuthCodeDeliveryEmailSetupRequired:
|
||||
return tgEmailSetupRequiredSentCode(hash), nil
|
||||
default:
|
||||
return tgSentCodeWithLength(hash, delivery.Length), nil
|
||||
}
|
||||
}
|
||||
|
||||
// onAuthSignIn 处理 auth.signIn:校验验证码;用户不存在时返回 SignUpRequired。
|
||||
// 带 email_verification 时走登录邮箱路径(验证码来自邮箱而非短信)。
|
||||
func (r *Router) onAuthSignIn(ctx context.Context, req *tg.AuthSignInRequest) (tg.AuthAuthorizationClass, error) {
|
||||
|
|
@ -302,6 +326,10 @@ func (r *Router) onAuthSignIn(ctx context.Context, req *tg.AuthSignInRequest) (t
|
|||
} else {
|
||||
u, loginMessage, needSignUp, err = r.deps.Auth.SignIn(ctx, r.authzFromCtx(ctx), req.PhoneNumber, req.PhoneCodeHash, req.PhoneCode)
|
||||
}
|
||||
return r.finishAuthSignIn(ctx, u, loginMessage, needSignUp, err)
|
||||
}
|
||||
|
||||
func (r *Router) finishAuthSignIn(ctx context.Context, u domain.User, loginMessage domain.Message, needSignUp bool, err error) (tg.AuthAuthorizationClass, error) {
|
||||
if err != nil {
|
||||
if errors.Is(err, domain.ErrSessionPasswordNeeded) && u.ID != 0 {
|
||||
if err := r.clearAuthKeyStateOnUserChange(ctx, u.ID); err != nil {
|
||||
|
|
@ -337,7 +365,7 @@ func (r *Router) onAuthResendCode(ctx context.Context, req *tg.AuthResendCodeReq
|
|||
if err != nil {
|
||||
return nil, signInErr(err)
|
||||
}
|
||||
return tgSentCode(hash), nil
|
||||
return r.tgSentCodeForHash(ctx, hash)
|
||||
}
|
||||
|
||||
func (r *Router) onAuthCancelCode(ctx context.Context, req *tg.AuthCancelCodeRequest) (bool, error) {
|
||||
|
|
@ -502,7 +530,7 @@ func (r *Router) onAuthResetLoginEmail(ctx context.Context, req *tg.AuthResetLog
|
|||
}
|
||||
return nil, internalErr()
|
||||
}
|
||||
return tgSentCode(hash), nil
|
||||
return r.tgSentCodeForHash(ctx, hash)
|
||||
}
|
||||
|
||||
// emailVerificationCode 从 emailVerification 取出可校验的字符串(验证码 / Google·Apple
|
||||
|
|
|
|||
145
internal/rpc/auth_login_email_rpc_test.go
Normal file
145
internal/rpc/auth_login_email_rpc_test.go
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gotd/td/tg"
|
||||
"go.uber.org/zap/zaptest"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
type loginEmailAccountService struct {
|
||||
AccountService
|
||||
verifiedEmail string
|
||||
}
|
||||
|
||||
func (s loginEmailAccountService) VerifyLoginEmail(context.Context, int64, string, string, string, bool) (string, error) {
|
||||
return s.verifiedEmail, nil
|
||||
}
|
||||
|
||||
func TestEmailSentCodeUsesDeliveryLength(t *testing.T) {
|
||||
authSvc := &captureAuthService{
|
||||
codeDelivery: domain.AuthCodeDelivery{
|
||||
Kind: domain.AuthCodeDeliveryEmail,
|
||||
EmailPattern: "a***e@example.test",
|
||||
Length: 6,
|
||||
},
|
||||
}
|
||||
r := New(Config{}, Deps{Auth: authSvc}, zaptest.NewLogger(t), fixedClock{now: time.Unix(1700000000, 0)})
|
||||
|
||||
sent, err := r.tgSentCodeForHash(context.Background(), "hash-email")
|
||||
if err != nil {
|
||||
t.Fatalf("tgSentCodeForHash: %v", err)
|
||||
}
|
||||
code, ok := sent.(*tg.AuthSentCode)
|
||||
if !ok {
|
||||
t.Fatalf("sent = %T, want *tg.AuthSentCode", sent)
|
||||
}
|
||||
emailType, ok := code.Type.(*tg.AuthSentCodeTypeEmailCode)
|
||||
if !ok {
|
||||
t.Fatalf("sent type = %T, want *tg.AuthSentCodeTypeEmailCode", code.Type)
|
||||
}
|
||||
if emailType.Length != 6 {
|
||||
t.Fatalf("email sent code length = %d, want 6", emailType.Length)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountVerifyEmailLoginSetupReturnsSentCodeSuccess(t *testing.T) {
|
||||
user := domain.User{
|
||||
ID: 100200300,
|
||||
AccessHash: 900100200,
|
||||
Phone: "8618800000020",
|
||||
FirstName: "Alice",
|
||||
}
|
||||
authSvc := &captureAuthService{signInUser: user}
|
||||
r := New(Config{}, Deps{
|
||||
Auth: authSvc,
|
||||
Account: loginEmailAccountService{verifiedEmail: "alice@example.test"},
|
||||
}, zaptest.NewLogger(t), fixedClock{now: time.Unix(1700000000, 0)})
|
||||
|
||||
got, err := r.onAccountVerifyEmail(context.Background(), &tg.AccountVerifyEmailRequest{
|
||||
Purpose: &tg.EmailVerifyPurposeLoginSetup{
|
||||
PhoneNumber: "+86 188 0000 0020",
|
||||
PhoneCodeHash: "hash-email-setup",
|
||||
},
|
||||
Verification: &tg.EmailVerificationCode{Code: "654321"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("onAccountVerifyEmail: %v", err)
|
||||
}
|
||||
verified, ok := got.(*tg.AccountEmailVerifiedLogin)
|
||||
if !ok {
|
||||
t.Fatalf("verified = %T, want *tg.AccountEmailVerifiedLogin", got)
|
||||
}
|
||||
if verified.Email != "alice@example.test" {
|
||||
t.Fatalf("verified email = %q", verified.Email)
|
||||
}
|
||||
success, ok := verified.SentCode.(*tg.AuthSentCodeSuccess)
|
||||
if !ok {
|
||||
t.Fatalf("sent code = %T, want *tg.AuthSentCodeSuccess", verified.SentCode)
|
||||
}
|
||||
authorization, ok := success.Authorization.(*tg.AuthAuthorization)
|
||||
if !ok {
|
||||
t.Fatalf("authorization = %T, want *tg.AuthAuthorization", success.Authorization)
|
||||
}
|
||||
self, ok := authorization.User.(*tg.User)
|
||||
if !ok {
|
||||
t.Fatalf("authorization user = %T, want *tg.User", authorization.User)
|
||||
}
|
||||
if self.ID != user.ID || !self.Self {
|
||||
t.Fatalf("authorization user = %+v, want self user %d", self, user.ID)
|
||||
}
|
||||
if authSvc.signInWithEmailCount != 1 {
|
||||
t.Fatalf("SignInWithEmail calls = %d, want 1", authSvc.signInWithEmailCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountVerifyEmailLoginSetupAndroidReturnsEmailSentCode(t *testing.T) {
|
||||
authSvc := &captureAuthService{}
|
||||
r := New(Config{}, Deps{
|
||||
Auth: authSvc,
|
||||
Account: loginEmailAccountService{verifiedEmail: "alice@example.test"},
|
||||
}, zaptest.NewLogger(t), fixedClock{now: time.Unix(1700000000, 0)})
|
||||
|
||||
ctx := WithClientInfo(context.Background(), ClientInfo{
|
||||
Type: ClientTypeAndroid,
|
||||
AppVersion: "12.8.1 (69169) pbeta",
|
||||
})
|
||||
got, err := r.onAccountVerifyEmail(ctx, &tg.AccountVerifyEmailRequest{
|
||||
Purpose: &tg.EmailVerifyPurposeLoginSetup{
|
||||
PhoneNumber: "+86 188 0000 0020",
|
||||
PhoneCodeHash: "hash-email-setup",
|
||||
},
|
||||
Verification: &tg.EmailVerificationCode{Code: "654321"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("onAccountVerifyEmail: %v", err)
|
||||
}
|
||||
verified, ok := got.(*tg.AccountEmailVerifiedLogin)
|
||||
if !ok {
|
||||
t.Fatalf("verified = %T, want *tg.AccountEmailVerifiedLogin", got)
|
||||
}
|
||||
sent, ok := verified.SentCode.(*tg.AuthSentCode)
|
||||
if !ok {
|
||||
t.Fatalf("sent code = %T, want *tg.AuthSentCode", verified.SentCode)
|
||||
}
|
||||
if sent.PhoneCodeHash != "hash-email-setup" {
|
||||
t.Fatalf("phone_code_hash = %q", sent.PhoneCodeHash)
|
||||
}
|
||||
emailType, ok := sent.Type.(*tg.AuthSentCodeTypeEmailCode)
|
||||
if !ok {
|
||||
t.Fatalf("sent type = %T, want *tg.AuthSentCodeTypeEmailCode", sent.Type)
|
||||
}
|
||||
if emailType.EmailPattern != "a***e@example.test" {
|
||||
t.Fatalf("email pattern = %q", emailType.EmailPattern)
|
||||
}
|
||||
if emailType.Length != 6 {
|
||||
t.Fatalf("email code length = %d, want 6", emailType.Length)
|
||||
}
|
||||
if authSvc.signInWithEmailCount != 0 {
|
||||
t.Fatalf("SignInWithEmail calls = %d, want 0 for Android compat downgrade", authSvc.signInWithEmailCount)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,12 +2,13 @@ package rpc
|
|||
|
||||
import "context"
|
||||
|
||||
// withAndroidCompatMetadata 为「客户端构造器漂移」请求仅兜底 client 类型。
|
||||
// DrKLO/OwpenGram Android 可能在不同版本使用不同 TL layer,client-private 构造器
|
||||
// 只能证明这是 Android 兼容路径,不能替代 invokeWithLayer 里的真实 layer。
|
||||
func (r *Router) withAndroidCompatMetadata(ctx context.Context) context.Context {
|
||||
if ClientTypeFrom(ctx) == ClientTypeUnknown {
|
||||
ctx = WithClientInfo(ctx, ClientInfo{LangPack: string(ClientTypeAndroid), Type: ClientTypeAndroid})
|
||||
// withClientDriftMetadata 只在调用方已经用 constructor drift 证明客户端来源时
|
||||
// 补最小 client 类型。它不是 unknown fallback;不能在普通裸 RPC 上调用。
|
||||
// DrKLO Android 的 client-private constructor 只能证明 Android 兼容路径,
|
||||
// 不能替代 invokeWithLayer/auth_keys/authorizations 里的真实 layer。
|
||||
func (r *Router) withClientDriftMetadata(ctx context.Context, typ ClientType) context.Context {
|
||||
if typ == ClientTypeUnknown || ClientTypeFrom(ctx) != ClientTypeUnknown {
|
||||
return ctx
|
||||
}
|
||||
return ctx
|
||||
return WithClientInfo(ctx, ClientInfo{Type: typ})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,7 +97,23 @@ func knownClientType(t ClientType) bool {
|
|||
}
|
||||
}
|
||||
|
||||
func clientTypeFromAPIID(apiID int) ClientType {
|
||||
switch apiID {
|
||||
// DrKLO local BuildVars.APP_ID uses 4; TDesktop's active session
|
||||
// classifier also recognizes the official Android ids below.
|
||||
case 4, 5, 6, 24, 1026, 1083, 2458, 2521, 21724:
|
||||
return ClientTypeAndroid
|
||||
case 2040, 17349, 611335:
|
||||
return ClientTypeTDesktop
|
||||
default:
|
||||
return ClientTypeUnknown
|
||||
}
|
||||
}
|
||||
|
||||
func detectClientType(info ClientInfo) ClientType {
|
||||
if t := clientTypeFromAPIID(info.APIID); t != ClientTypeUnknown {
|
||||
return t
|
||||
}
|
||||
if strings.EqualFold(info.LangPack, string(ClientTypeAndroid)) {
|
||||
return ClientTypeAndroid
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ type AuthService interface {
|
|||
PendingPasswordUserID(ctx context.Context, authKeyID [8]byte) (int64, bool, error)
|
||||
CompletePasswordSignIn(ctx context.Context, authKeyID [8]byte) error
|
||||
SendCode(ctx context.Context, phone string) (string, error)
|
||||
CodeDelivery(ctx context.Context, phoneCodeHash string) (domain.AuthCodeDelivery, bool, error)
|
||||
ResendCode(ctx context.Context, phone, phoneCodeHash string) (string, error)
|
||||
CancelCode(ctx context.Context, phone, phoneCodeHash string) error
|
||||
SignIn(ctx context.Context, a domain.Authorization, phone, phoneCodeHash, code string) (domain.User, domain.Message, bool, error)
|
||||
|
|
@ -40,6 +41,8 @@ type AuthService interface {
|
|||
LogOut(ctx context.Context, authKeyID [8]byte) error
|
||||
Authorization(ctx context.Context, authKeyID [8]byte) (domain.Authorization, bool, error)
|
||||
UpdateAuthorizationLayer(ctx context.Context, authKeyID [8]byte, layer int) error
|
||||
AuthKeyClientInfo(ctx context.Context, authKeyID [8]byte) (domain.AuthKeyClientInfo, bool, error)
|
||||
UpdateAuthKeyClientInfo(ctx context.Context, authKeyID [8]byte, info domain.AuthKeyClientInfo) error
|
||||
ListAuthorizations(ctx context.Context, userID int64) ([]domain.Authorization, error)
|
||||
ResetAuthorization(ctx context.Context, userID, hash int64) (domain.Authorization, bool, error)
|
||||
ResetAuthorizations(ctx context.Context, userID int64, keepAuthKeyID [8]byte) ([]domain.Authorization, error)
|
||||
|
|
@ -262,6 +265,8 @@ type AccountService interface {
|
|||
ResendPasswordEmail(ctx context.Context, userID int64) error
|
||||
CancelPasswordEmail(ctx context.Context, userID int64) error
|
||||
// 登录邮箱(独立于 2FA 恢复邮箱):authed 走 userID,登录流程/重置走 phone。
|
||||
SendLoginEmailCode(ctx context.Context, userID int64, phone, phoneCodeHash, email string, setup bool) (string, int, error)
|
||||
VerifyLoginEmail(ctx context.Context, userID int64, phone, phoneCodeHash, code string, setup bool) (string, error)
|
||||
SetLoginEmail(ctx context.Context, userID int64, email string) error
|
||||
SetLoginEmailByPhone(ctx context.Context, phone, email string) error
|
||||
LoginEmail(ctx context.Context, userID int64) (string, bool, error)
|
||||
|
|
|
|||
|
|
@ -261,6 +261,7 @@ func srpPasswordChangedErr() error { return tgerr.New(400, "SRP_PASSWORD_CHAN
|
|||
func newSettingsInvalidErr() error { return tgerr.New(400, "NEW_SETTINGS_INVALID") }
|
||||
func newSaltInvalidErr() error { return tgerr.New(400, "NEW_SALT_INVALID") }
|
||||
func emailInvalidErr() error { return tgerr.New(400, "EMAIL_INVALID") }
|
||||
func emailNotAllowedErr() error { return tgerr.New(400, "EMAIL_NOT_ALLOWED") }
|
||||
func emailCodeInvalidErr() error { return tgerr.New(400, "CODE_INVALID") }
|
||||
func passwordRecoveryNAErr() error { return tgerr.New(400, "PASSWORD_RECOVERY_NA") }
|
||||
|
||||
|
|
@ -393,6 +394,10 @@ func passwordErr(err error) error {
|
|||
return newSaltInvalidErr()
|
||||
case errors.Is(err, domain.ErrEmailInvalid):
|
||||
return emailInvalidErr()
|
||||
case errors.Is(err, domain.ErrEmailOccupied):
|
||||
return emailNotAllowedErr()
|
||||
case errors.Is(err, domain.ErrEmailNotAllowed):
|
||||
return emailNotAllowedErr()
|
||||
case errors.Is(err, domain.ErrEmailCodeInvalid):
|
||||
return emailCodeInvalidErr()
|
||||
case errors.Is(err, domain.ErrPasswordRecoveryNA):
|
||||
|
|
|
|||
15
internal/rpc/errors_test.go
Normal file
15
internal/rpc/errors_test.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gotd/td/tgerr"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func TestPasswordErrMapsOccupiedLoginEmailToNotAllowed(t *testing.T) {
|
||||
if err := passwordErr(domain.ErrEmailOccupied); !tgerr.Is(err, "EMAIL_NOT_ALLOWED") {
|
||||
t.Fatalf("passwordErr(ErrEmailOccupied) = %v, want EMAIL_NOT_ALLOWED", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -34,8 +34,9 @@ type tempResolveResult struct {
|
|||
}
|
||||
|
||||
const (
|
||||
authKeyResolveSingleflightPrefix = "resolve:"
|
||||
authClientInfoSingleflightPrefix = "authinfo:"
|
||||
authKeyResolveSingleflightPrefix = "resolve:"
|
||||
authClientInfoSingleflightPrefix = "authinfo:"
|
||||
authKeyClientInfoSingleflightPrefix = "authkeyinfo:"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -154,6 +155,7 @@ type clientSessionInfo struct {
|
|||
layer int
|
||||
clientInfo ClientInfo
|
||||
hasClientInfo bool
|
||||
authKeyInfoChecked bool
|
||||
authorizationChecked bool
|
||||
}
|
||||
|
||||
|
|
@ -226,6 +228,19 @@ func (r *Router) Dispatch(ctx context.Context, authKeyID [8]byte, sessionID int6
|
|||
}
|
||||
tUser := r.clock.Now()
|
||||
info, hasClientMetadata, clientMetadataStored := r.clientSessionInfo(ctx)
|
||||
if authInfo, ok := r.clientSessionInfoFromAuthKey(ctx, effectiveAuthKeyID, info); ok {
|
||||
info = mergeClientSessionInfo(info, authInfo)
|
||||
hasClientMetadata = true
|
||||
r.rememberClientSessionInfo(ctx, info)
|
||||
clientMetadataStored = true
|
||||
if info.layer != 0 {
|
||||
if binder, okBinder := r.deps.Sessions.(ClientLayerBinder); okBinder {
|
||||
if rawAuthKeyID, okRaw := RawAuthKeyIDFrom(ctx); okRaw {
|
||||
binder.SetClientLayerForAuthKey(rawAuthKeyID, sessionID, info.layer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if hasUserID {
|
||||
if authInfo, ok := r.clientSessionInfoFromAuthorization(ctx, userID, effectiveAuthKeyID, info); ok {
|
||||
info = mergeClientSessionInfo(info, authInfo)
|
||||
|
|
@ -501,6 +516,7 @@ func (r *Router) invalidateAuthUserCache(authKeyID [8]byte) {
|
|||
r.authUserSF.Forget(key)
|
||||
r.authUserSF.Forget(authKeyResolveSingleflightPrefix + key)
|
||||
r.authUserSF.Forget(authClientInfoSingleflightPrefix + key)
|
||||
r.authUserSF.Forget(authKeyClientInfoSingleflightPrefix + key)
|
||||
}
|
||||
|
||||
func (r *Router) scopedSessions() (ScopedSessionBinder, bool) {
|
||||
|
|
@ -619,7 +635,7 @@ func (r *Router) dispatch(ctx context.Context, b *bin.Buffer, depth int) (bin.En
|
|||
if clientDrift {
|
||||
// 客户端漂移只能证明这是 Android 兼容路径;layer 仍以
|
||||
// invokeWithLayer 或授权记录里的真实观测值为准。
|
||||
ctx = r.withAndroidCompatMetadata(ctx)
|
||||
ctx = r.withClientDriftMetadata(ctx, ClientTypeAndroid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -699,6 +715,17 @@ func (r *Router) rememberClientInfo(ctx context.Context, info ClientInfo) {
|
|||
sessionInfo.layer = layer
|
||||
}
|
||||
})
|
||||
r.persistAuthKeyClientInfo(ctx, clientSessionInfo{layer: layer, clientInfo: info, hasClientInfo: true})
|
||||
}
|
||||
|
||||
func (r *Router) rememberClientAPIID(ctx context.Context, apiID int) {
|
||||
if apiID == 0 {
|
||||
return
|
||||
}
|
||||
info := ClientInfo{APIID: apiID, Type: clientTypeFromAPIID(apiID)}
|
||||
sessionInfo := clientSessionInfo{clientInfo: info, hasClientInfo: true}
|
||||
r.rememberClientSessionInfo(ctx, sessionInfo)
|
||||
r.persistAuthKeyClientInfo(ctx, sessionInfo)
|
||||
}
|
||||
|
||||
func (r *Router) rememberClientLayer(ctx context.Context, layer int) {
|
||||
|
|
@ -747,6 +774,7 @@ func (r *Router) rememberClientLayer(ctx context.Context, layer int) {
|
|||
binder.SetClientLayerForAuthKey(rawAuthKeyID, sessionID, layer)
|
||||
}
|
||||
}
|
||||
r.persistAuthKeyClientInfo(ctx, clientSessionInfo{layer: layer})
|
||||
if persistAuthLayer && r.deps.Auth != nil {
|
||||
if err := r.deps.Auth.UpdateAuthorizationLayer(ctx, authKeyID, layer); err != nil {
|
||||
r.log.Warn("update authorization layer failed",
|
||||
|
|
@ -757,6 +785,36 @@ func (r *Router) rememberClientLayer(ctx context.Context, layer int) {
|
|||
}
|
||||
}
|
||||
|
||||
func (r *Router) persistAuthKeyClientInfo(ctx context.Context, info clientSessionInfo) {
|
||||
if r.deps.Auth == nil {
|
||||
return
|
||||
}
|
||||
domainInfo := domainAuthKeyClientInfo(info)
|
||||
if domainInfo.Layer == 0 && domainInfo.DeviceModel == "" && domainInfo.Platform == "" &&
|
||||
domainInfo.SystemVersion == "" && domainInfo.APIID == 0 && domainInfo.AppVersion == "" {
|
||||
return
|
||||
}
|
||||
seen := make(map[[8]byte]struct{}, 2)
|
||||
if rawAuthKeyID, ok := RawAuthKeyIDFrom(ctx); ok && rawAuthKeyID != ([8]byte{}) {
|
||||
seen[rawAuthKeyID] = struct{}{}
|
||||
if err := r.deps.Auth.UpdateAuthKeyClientInfo(ctx, rawAuthKeyID, domainInfo); err != nil {
|
||||
r.log.Warn("update auth key client info failed",
|
||||
zap.String("auth_key_id", fmt.Sprintf("%x", rawAuthKeyID[:])),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
if authKeyID, ok := AuthKeyIDFrom(ctx); ok && authKeyID != ([8]byte{}) {
|
||||
if _, done := seen[authKeyID]; done {
|
||||
return
|
||||
}
|
||||
if err := r.deps.Auth.UpdateAuthKeyClientInfo(ctx, authKeyID, domainInfo); err != nil {
|
||||
r.log.Warn("update auth key client info failed",
|
||||
zap.String("auth_key_id", fmt.Sprintf("%x", authKeyID[:])),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NegotiatedLayer returns the TL layer the given session negotiated via
|
||||
// invokeWithLayer/initConnection. It is keyed first by (auth_key, session) then
|
||||
// falls back to the stable auth_key — so a reconnect with a new session_id still
|
||||
|
|
@ -892,6 +950,9 @@ func clientSessionInfoContains(current, required clientSessionInfo) bool {
|
|||
if required.authorizationChecked && !current.authorizationChecked {
|
||||
return false
|
||||
}
|
||||
if required.authKeyInfoChecked && !current.authKeyInfoChecked {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -982,6 +1043,43 @@ func (r *Router) cachedResolvedAuthClientInfo(authKeyID [8]byte) (clientSessionI
|
|||
return info, true
|
||||
}
|
||||
|
||||
func (r *Router) cachedResolvedAuthKeyClientInfo(authKeyID [8]byte) (clientSessionInfo, bool) {
|
||||
r.clientInfoMu.RLock()
|
||||
defer r.clientInfoMu.RUnlock()
|
||||
info, ok := r.authInfo[authKeyID]
|
||||
if !ok || clientSessionInfoNeedsAuthKeyInfo(info) {
|
||||
return clientSessionInfo{}, false
|
||||
}
|
||||
return info, true
|
||||
}
|
||||
|
||||
func (r *Router) clientSessionInfoFromAuthKey(ctx context.Context, authKeyID [8]byte, current clientSessionInfo) (clientSessionInfo, bool) {
|
||||
if !clientSessionInfoNeedsAuthKeyInfo(current) || r.deps.Auth == nil || authKeyID == ([8]byte{}) {
|
||||
return clientSessionInfo{}, false
|
||||
}
|
||||
v, err, _ := r.authUserSF.Do(authKeyClientInfoSingleflightPrefix+string(authKeyID[:]), func() (any, error) {
|
||||
if cached, ok := r.cachedResolvedAuthKeyClientInfo(authKeyID); ok {
|
||||
return cached, nil
|
||||
}
|
||||
info, found, err := r.deps.Auth.AuthKeyClientInfo(ctx, authKeyID)
|
||||
if err != nil {
|
||||
return clientSessionInfo{}, err
|
||||
}
|
||||
if !found {
|
||||
return clientSessionInfo{authKeyInfoChecked: true}, nil
|
||||
}
|
||||
return clientSessionInfoFromAuthKeyClientInfo(info, current), nil
|
||||
})
|
||||
if err != nil {
|
||||
return clientSessionInfo{}, false
|
||||
}
|
||||
info := v.(clientSessionInfo)
|
||||
if info.layer == 0 && !info.hasClientInfo && !info.authKeyInfoChecked {
|
||||
return clientSessionInfo{}, false
|
||||
}
|
||||
return info, true
|
||||
}
|
||||
|
||||
func mergeClientSessionInfo(base, fallback clientSessionInfo) clientSessionInfo {
|
||||
if base.layer == 0 {
|
||||
base.layer = fallback.layer
|
||||
|
|
@ -993,9 +1091,48 @@ func mergeClientSessionInfo(base, fallback clientSessionInfo) clientSessionInfo
|
|||
if fallback.authorizationChecked {
|
||||
base.authorizationChecked = true
|
||||
}
|
||||
if fallback.authKeyInfoChecked {
|
||||
base.authKeyInfoChecked = true
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func clientSessionInfoFromAuthKeyClientInfo(item domain.AuthKeyClientInfo, current clientSessionInfo) clientSessionInfo {
|
||||
info := clientSessionInfo{
|
||||
layer: item.Layer,
|
||||
authKeyInfoChecked: true,
|
||||
clientInfo: ClientInfo{
|
||||
APIID: item.APIID,
|
||||
DeviceModel: item.DeviceModel,
|
||||
SystemVersion: item.SystemVersion,
|
||||
AppVersion: item.AppVersion,
|
||||
Type: ClientType(item.Platform),
|
||||
},
|
||||
}
|
||||
info.clientInfo = normalizeClientInfo(info.clientInfo)
|
||||
info.hasClientInfo = info.clientInfo.ClientType() != ClientTypeUnknown ||
|
||||
info.clientInfo.DeviceModel != "" ||
|
||||
info.clientInfo.SystemVersion != "" ||
|
||||
info.clientInfo.AppVersion != "" ||
|
||||
info.clientInfo.APIID != 0
|
||||
if info.layer == 0 && current.layer != 0 {
|
||||
info.layer = current.layer
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
func domainAuthKeyClientInfo(info clientSessionInfo) domain.AuthKeyClientInfo {
|
||||
out := domain.AuthKeyClientInfo{Layer: info.layer}
|
||||
if info.hasClientInfo {
|
||||
out.APIID = info.clientInfo.APIID
|
||||
out.DeviceModel = info.clientInfo.DeviceModel
|
||||
out.SystemVersion = info.clientInfo.SystemVersion
|
||||
out.AppVersion = info.clientInfo.AppVersion
|
||||
out.Platform = string(info.clientInfo.ClientType())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *Router) clientSessionInfoFromAuthorization(ctx context.Context, userID int64, authKeyID [8]byte, current clientSessionInfo) (clientSessionInfo, bool) {
|
||||
if !clientSessionInfoNeedsAuthorization(current) || r.deps.Auth == nil || userID == 0 {
|
||||
return clientSessionInfo{}, false
|
||||
|
|
@ -1052,6 +1189,13 @@ func clientSessionInfoNeedsAuthorization(info clientSessionInfo) bool {
|
|||
return info.layer == 0 || !info.hasClientInfo || info.clientInfo.ClientType() == ClientTypeUnknown
|
||||
}
|
||||
|
||||
func clientSessionInfoNeedsAuthKeyInfo(info clientSessionInfo) bool {
|
||||
if info.authKeyInfoChecked {
|
||||
return false
|
||||
}
|
||||
return info.layer == 0 || !info.hasClientInfo || info.clientInfo.ClientType() == ClientTypeUnknown
|
||||
}
|
||||
|
||||
// fallback 处理未注册的 RPC:记录到 compatibility trace(落兼容矩阵),
|
||||
// 返回 NOT_IMPLEMENTED rpc_error 让客户端继续运行而非断连。
|
||||
func (r *Router) fallback(ctx context.Context, b *bin.Buffer) (bin.Encoder, error) {
|
||||
|
|
|
|||
|
|
@ -188,6 +188,146 @@ func TestDispatchRemembersLayerAndClientTypeForSession(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestDispatchPersistsPreLoginClientMetadataOnInitConnection(t *testing.T) {
|
||||
auth := &captureAuthService{}
|
||||
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{
|
||||
Auth: auth,
|
||||
}, zaptest.NewLogger(t), clock.System)
|
||||
rawAuthKeyID := [8]byte{0x22, 0xdb, 0xcf, 0xc8, 0x0d, 0x4c, 0x77, 0x97}
|
||||
sessionID := int64(8103956954238395544)
|
||||
|
||||
req := &tg.InvokeWithLayerRequest{
|
||||
Layer: currentClientLayer,
|
||||
Query: &tg.InitConnectionRequest{
|
||||
APIID: 4,
|
||||
DeviceModel: "GooglePixel 9a",
|
||||
SystemVersion: "SDK 36",
|
||||
AppVersion: "12.8.1 (69169) pbeta",
|
||||
SystemLangCode: "en",
|
||||
LangPack: "android",
|
||||
LangCode: "en",
|
||||
Query: &tg.HelpGetConfigRequest{},
|
||||
},
|
||||
}
|
||||
var in bin.Buffer
|
||||
if err := req.Encode(&in); err != nil {
|
||||
t.Fatalf("encode init request: %v", err)
|
||||
}
|
||||
if _, err := r.Dispatch(context.Background(), rawAuthKeyID, sessionID, &in); err != nil {
|
||||
t.Fatalf("dispatch init request: %v", err)
|
||||
}
|
||||
|
||||
got, ok := auth.authKeyClientInfos[rawAuthKeyID]
|
||||
if !ok {
|
||||
t.Fatalf("auth key client metadata was not persisted")
|
||||
}
|
||||
if got.Layer != currentClientLayer {
|
||||
t.Fatalf("persisted layer = %d, want %d", got.Layer, currentClientLayer)
|
||||
}
|
||||
if got.Platform != string(ClientTypeAndroid) {
|
||||
t.Fatalf("persisted platform = %q, want android", got.Platform)
|
||||
}
|
||||
if got.DeviceModel != "GooglePixel 9a" || got.SystemVersion != "SDK 36" || got.APIID != 4 || got.AppVersion != "12.8.1 (69169) pbeta" {
|
||||
t.Fatalf("persisted client metadata = %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispatchPersistsPreLoginClientMetadataFromSendCodeAPIID(t *testing.T) {
|
||||
auth := &captureAuthService{}
|
||||
rawAuthKeyID := [8]byte{0x33, 0xdb, 0xcf, 0xc8, 0x0d, 0x4c, 0x77, 0x97}
|
||||
const sessionID = int64(8103956954238395544)
|
||||
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{
|
||||
Auth: auth,
|
||||
}, zaptest.NewLogger(t), clock.System)
|
||||
|
||||
var sendCode bin.Buffer
|
||||
if err := (&tg.AuthSendCodeRequest{
|
||||
PhoneNumber: "+8618800000020",
|
||||
APIID: 4,
|
||||
APIHash: "android",
|
||||
Settings: tg.CodeSettings{},
|
||||
}).Encode(&sendCode); err != nil {
|
||||
t.Fatalf("encode auth.sendCode: %v", err)
|
||||
}
|
||||
if _, err := r.Dispatch(context.Background(), rawAuthKeyID, sessionID, &sendCode); err != nil {
|
||||
t.Fatalf("dispatch auth.sendCode: %v", err)
|
||||
}
|
||||
persisted, ok := auth.authKeyClientInfos[rawAuthKeyID]
|
||||
if !ok {
|
||||
t.Fatalf("auth.sendCode did not persist auth key client metadata")
|
||||
}
|
||||
if persisted.APIID != 4 || persisted.Platform != string(ClientTypeAndroid) {
|
||||
t.Fatalf("persisted client metadata = %+v, want android api_id=4", persisted)
|
||||
}
|
||||
|
||||
core, logs := observer.New(zap.DebugLevel)
|
||||
afterRestart := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{
|
||||
Auth: auth,
|
||||
}, zap.New(core), clock.System)
|
||||
var help bin.Buffer
|
||||
if err := (&tg.HelpGetConfigRequest{}).Encode(&help); err != nil {
|
||||
t.Fatalf("encode help.getConfig: %v", err)
|
||||
}
|
||||
if _, err := afterRestart.Dispatch(context.Background(), rawAuthKeyID, sessionID+1, &help); err != nil {
|
||||
t.Fatalf("dispatch help.getConfig after restart: %v", err)
|
||||
}
|
||||
entries := logs.FilterMessage("RPC inner handled").All()
|
||||
if len(entries) == 0 {
|
||||
t.Fatalf("RPC inner handled log missing")
|
||||
}
|
||||
fields := entries[len(entries)-1].ContextMap()
|
||||
if got := fields["client_type"]; got != string(ClientTypeAndroid) {
|
||||
t.Fatalf("logged client_type = %v, want %s", got, ClientTypeAndroid)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispatchRestoresPreLoginAndroidMetadataFromAuthKey(t *testing.T) {
|
||||
core, logs := observer.New(zap.DebugLevel)
|
||||
authKeyID := [8]byte{0x22, 0xdb, 0xcf, 0xc8, 0x0d, 0x4c, 0x77, 0x97}
|
||||
auth := &captureAuthService{
|
||||
authKeyClientInfos: map[[8]byte]domain.AuthKeyClientInfo{
|
||||
authKeyID: {
|
||||
Layer: currentClientLayer,
|
||||
DeviceModel: "GooglePixel 9a",
|
||||
Platform: string(ClientTypeAndroid),
|
||||
SystemVersion: "SDK 36",
|
||||
APIID: 4,
|
||||
AppVersion: "12.8.1 (69169) pbeta",
|
||||
},
|
||||
},
|
||||
}
|
||||
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{
|
||||
Auth: auth,
|
||||
}, zap.New(core), clock.System)
|
||||
|
||||
var in bin.Buffer
|
||||
if err := (&tg.HelpGetConfigRequest{}).Encode(&in); err != nil {
|
||||
t.Fatalf("encode request: %v", err)
|
||||
}
|
||||
const sessionID = int64(8103956954238395544)
|
||||
if _, err := r.Dispatch(context.Background(), authKeyID, sessionID, &in); err != nil {
|
||||
t.Fatalf("dispatch plain request: %v", err)
|
||||
}
|
||||
|
||||
entries := logs.FilterMessage("RPC inner handled").All()
|
||||
if len(entries) == 0 {
|
||||
t.Fatalf("RPC inner handled log missing")
|
||||
}
|
||||
fields := entries[len(entries)-1].ContextMap()
|
||||
if got := intLogField(fields["layer"]); got != currentClientLayer {
|
||||
t.Fatalf("logged layer = %d fields=%v, want %d", got, fields, currentClientLayer)
|
||||
}
|
||||
if got := fields["client_type"]; got != string(ClientTypeAndroid) {
|
||||
t.Fatalf("logged client_type = %v, want %s", got, ClientTypeAndroid)
|
||||
}
|
||||
if got := fields["app_version"]; got != "12.8.1 (69169) pbeta" {
|
||||
t.Fatalf("logged app_version = %v, want 12.8.1 (69169) pbeta", got)
|
||||
}
|
||||
if got, ok := r.NegotiatedLayer(authKeyID, sessionID+1); !ok || got != currentClientLayer {
|
||||
t.Fatalf("auth-key fallback layer = (%d,%v), want (%d,true)", got, ok, currentClientLayer)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAndroidLegacyCompatLogsClientMetadataWithoutInit(t *testing.T) {
|
||||
core, logs := observer.New(zap.DebugLevel)
|
||||
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{}, zap.New(core), clock.System)
|
||||
|
|
@ -210,9 +350,10 @@ func TestAndroidLegacyCompatLogsClientMetadataWithoutInit(t *testing.T) {
|
|||
t.Fatalf("dispatch legacy updates.getDifference: %v", err)
|
||||
}
|
||||
|
||||
// The legacy android constructor is upgraded by layerwire and dispatched
|
||||
// normally; client metadata is still applied (withAndroidCompatMetadata for
|
||||
// client drift), now surfaced on the standard "RPC inner handled" log.
|
||||
// The legacy Android constructor is upgraded by layerwire and dispatched
|
||||
// normally; client metadata is still applied only because IsClientDrift
|
||||
// positively identified a DrKLO constructor, now surfaced on the standard
|
||||
// "RPC inner handled" log.
|
||||
entries := logs.FilterMessage("RPC inner handled").All()
|
||||
if len(entries) == 0 {
|
||||
t.Fatalf("RPC inner handled log missing")
|
||||
|
|
@ -337,6 +478,16 @@ func TestClientTypeDetectsAndroidSDKVersion(t *testing.T) {
|
|||
if got := info.ClientType(); got != ClientTypeUnknown {
|
||||
t.Fatalf("gotd test client type = %s, want %s", got, ClientTypeUnknown)
|
||||
}
|
||||
|
||||
info = normalizeClientInfo(ClientInfo{APIID: 4})
|
||||
if got := info.ClientType(); got != ClientTypeAndroid {
|
||||
t.Fatalf("DrKLO api_id=4 client type = %s, want %s", got, ClientTypeAndroid)
|
||||
}
|
||||
|
||||
info = normalizeClientInfo(ClientInfo{APIID: 2040})
|
||||
if got := info.ClientType(); got != ClientTypeTDesktop {
|
||||
t.Fatalf("TDesktop api_id=2040 client type = %s, want %s", got, ClientTypeTDesktop)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispatchRestoresClientMetadataFromAuthorization(t *testing.T) {
|
||||
|
|
@ -538,6 +689,28 @@ func TestDispatchCachesMissingClientMetadataAuthorizationLookup(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestDispatchCachesMissingAuthKeyClientMetadataLookup(t *testing.T) {
|
||||
authKeyID := [8]byte{0x68, 0x25, 0xc2, 0xee, 0xf8, 0x82, 0xef, 0x72}
|
||||
auth := &captureAuthService{}
|
||||
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{
|
||||
Auth: auth,
|
||||
}, zaptest.NewLogger(t), clock.System)
|
||||
|
||||
for _, sessionID := range []int64{101, 102, 103} {
|
||||
var in bin.Buffer
|
||||
if err := (&tg.HelpGetConfigRequest{}).Encode(&in); err != nil {
|
||||
t.Fatalf("encode request: %v", err)
|
||||
}
|
||||
if _, err := r.Dispatch(context.Background(), authKeyID, sessionID, &in); err != nil {
|
||||
t.Fatalf("dispatch session %d: %v", sessionID, err)
|
||||
}
|
||||
}
|
||||
|
||||
if auth.authKeyInfoLookups != 1 {
|
||||
t.Fatalf("auth key client info lookups = %d, want 1 cached miss", auth.authKeyInfoLookups)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentUserIDUsesAuthUserCache(t *testing.T) {
|
||||
authKeyID := [8]byte{0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42}
|
||||
auth := &captureAuthService{userID: 1000000001}
|
||||
|
|
|
|||
|
|
@ -25,11 +25,15 @@ type captureAuthService struct {
|
|||
authorizationLookups int
|
||||
authorizationLists int
|
||||
layerUpdates int
|
||||
authKeyClientInfos map[[8]byte]domain.AuthKeyClientInfo
|
||||
authKeyInfoLookups int
|
||||
loggedOutAuthKeyID [8]byte
|
||||
pendingPasswordUserID int64
|
||||
pendingPassword bool
|
||||
completedPasswordKey [8]byte
|
||||
completePasswordCount int
|
||||
codeDelivery domain.AuthCodeDelivery
|
||||
signInWithEmailCount int
|
||||
}
|
||||
|
||||
type blockingUserAuthService struct {
|
||||
|
|
@ -72,6 +76,10 @@ func (s *blockingUserAuthService) SendCode(context.Context, string) (string, err
|
|||
return "", nil
|
||||
}
|
||||
|
||||
func (s *blockingUserAuthService) CodeDelivery(context.Context, string) (domain.AuthCodeDelivery, bool, error) {
|
||||
return domain.AuthCodeDelivery{Kind: domain.AuthCodeDeliveryPhone, Length: devCodeLength}, true, nil
|
||||
}
|
||||
|
||||
func (s *blockingUserAuthService) ResendCode(context.Context, string, string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
|
@ -116,6 +124,14 @@ func (s *blockingUserAuthService) UpdateAuthorizationLayer(context.Context, [8]b
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *blockingUserAuthService) AuthKeyClientInfo(context.Context, [8]byte) (domain.AuthKeyClientInfo, bool, error) {
|
||||
return domain.AuthKeyClientInfo{}, false, nil
|
||||
}
|
||||
|
||||
func (s *blockingUserAuthService) UpdateAuthKeyClientInfo(context.Context, [8]byte, domain.AuthKeyClientInfo) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *blockingUserAuthService) ListAuthorizations(context.Context, int64) ([]domain.Authorization, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
|
@ -154,6 +170,13 @@ func (s *captureAuthService) SendCode(context.Context, string) (string, error) {
|
|||
return "", nil
|
||||
}
|
||||
|
||||
func (s *captureAuthService) CodeDelivery(context.Context, string) (domain.AuthCodeDelivery, bool, error) {
|
||||
if s.codeDelivery.Kind != "" {
|
||||
return s.codeDelivery, true, nil
|
||||
}
|
||||
return domain.AuthCodeDelivery{Kind: domain.AuthCodeDeliveryPhone, Length: devCodeLength}, true, nil
|
||||
}
|
||||
|
||||
func (s *captureAuthService) ResendCode(context.Context, string, string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
|
@ -170,6 +193,7 @@ func (s *captureAuthService) SignIn(context.Context, domain.Authorization, strin
|
|||
}
|
||||
|
||||
func (s *captureAuthService) SignInWithEmail(context.Context, domain.Authorization, string, string, string) (domain.User, domain.Message, bool, error) {
|
||||
s.signInWithEmailCount++
|
||||
if s.signInUser.ID != 0 {
|
||||
return s.signInUser, domain.Message{}, false, nil
|
||||
}
|
||||
|
|
@ -238,6 +262,39 @@ func (s *captureAuthService) UpdateAuthorizationLayer(_ context.Context, authKey
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *captureAuthService) AuthKeyClientInfo(_ context.Context, authKeyID [8]byte) (domain.AuthKeyClientInfo, bool, error) {
|
||||
s.authKeyInfoLookups++
|
||||
info, ok := s.authKeyClientInfos[authKeyID]
|
||||
return info, ok, nil
|
||||
}
|
||||
|
||||
func (s *captureAuthService) UpdateAuthKeyClientInfo(_ context.Context, authKeyID [8]byte, info domain.AuthKeyClientInfo) error {
|
||||
if s.authKeyClientInfos == nil {
|
||||
s.authKeyClientInfos = make(map[[8]byte]domain.AuthKeyClientInfo)
|
||||
}
|
||||
current := s.authKeyClientInfos[authKeyID]
|
||||
if info.Layer > 0 {
|
||||
current.Layer = info.Layer
|
||||
}
|
||||
if info.DeviceModel != "" {
|
||||
current.DeviceModel = info.DeviceModel
|
||||
}
|
||||
if info.Platform != "" {
|
||||
current.Platform = info.Platform
|
||||
}
|
||||
if info.SystemVersion != "" {
|
||||
current.SystemVersion = info.SystemVersion
|
||||
}
|
||||
if info.APIID != 0 {
|
||||
current.APIID = info.APIID
|
||||
}
|
||||
if info.AppVersion != "" {
|
||||
current.AppVersion = info.AppVersion
|
||||
}
|
||||
s.authKeyClientInfos[authKeyID] = current
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *captureAuthService) ListAuthorizations(context.Context, int64) ([]domain.Authorization, error) {
|
||||
s.authorizationLists++
|
||||
return append([]domain.Authorization(nil), s.authorizations...), nil
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue