owpengram-server/internal/rpc/auth.go
2026-06-04 01:37:39 +08:00

347 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package rpc
import (
"context"
"encoding/binary"
"fmt"
"strings"
"time"
"go.uber.org/zap"
"github.com/gotd/td/proto"
"github.com/gotd/td/tg"
"telesrv/internal/compat/tdesktop"
"telesrv/internal/domain"
)
// devCodeLength 是开发固定验证码长度,写入 auth.sentCode 的 type.length。
const devCodeLength = 5
const loginMessagePushDelay = 2 * time.Second
// registerAuth 注册 auth.* RPC handler。
func (r *Router) registerAuth(d *tg.ServerDispatcher) {
d.OnAuthBindTempAuthKey(r.onAuthBindTempAuthKey)
d.OnAuthExportLoginToken(r.onAuthExportLoginToken)
d.OnAuthSendCode(r.onAuthSendCode)
d.OnAuthSignIn(r.onAuthSignIn)
d.OnAuthSignUp(r.onAuthSignUp)
d.OnAuthLogOut(r.onAuthLogOut)
}
// onAuthBindTempAuthKey 记录 TDesktop 的 PFS temp→perm auth key 绑定。
func (r *Router) onAuthBindTempAuthKey(ctx context.Context, req *tg.AuthBindTempAuthKeyRequest) (bool, error) {
if r.deps.Auth == nil {
return true, nil
}
id, _ := RawAuthKeyIDFrom(ctx)
if id == ([8]byte{}) {
id, _ = AuthKeyIDFrom(ctx)
}
sessionID, _ := SessionIDFrom(ctx)
if err := r.deps.Auth.BindTempAuthKey(ctx, sessionID, domain.TempAuthKeyBinding{
TempAuthKeyID: id,
PermAuthKeyID: req.PermAuthKeyID,
Nonce: req.Nonce,
ExpiresAt: req.ExpiresAt,
EncryptedMessage: append([]byte(nil), req.EncryptedMessage...),
}); err != nil {
return false, bindTempAuthKeyErr(err)
}
if r.deps.Sessions != nil {
if scoped, ok := r.scopedSessions(); ok {
rawAuthKeyID, _ := RawAuthKeyIDFrom(ctx)
scoped.BindAuthKeyForSession(rawAuthKeyID, sessionID, authKeyIDFromInt64(req.PermAuthKeyID))
} else {
r.deps.Sessions.BindAuthKey(sessionID, authKeyIDFromInt64(req.PermAuthKeyID))
}
}
r.invalidateAuthUserCache(id)
return true, nil
}
// onAuthExportLoginToken 给 TDesktop QR 登录页返回一个短期占位 token。
func (r *Router) onAuthExportLoginToken(ctx context.Context, _ *tg.AuthExportLoginTokenRequest) (tg.AuthLoginTokenClass, error) {
id, _ := AuthKeyIDFrom(ctx)
sessionID, _ := SessionIDFrom(ctx)
return tdesktop.LoginToken(r.clock.Now(), id, sessionID), nil
}
// onAuthSendCode 处理 auth.sendCode生成 phone_code_hash 并返回 sentCode。
func (r *Router) onAuthSendCode(ctx context.Context, req *tg.AuthSendCodeRequest) (tg.AuthSentCodeClass, error) {
hash, err := r.deps.Auth.SendCode(ctx, req.PhoneNumber)
if err != nil {
return nil, internalErr()
}
return &tg.AuthSentCode{
Type: &tg.AuthSentCodeTypeApp{Length: devCodeLength},
PhoneCodeHash: hash,
}, nil
}
// onAuthSignIn 处理 auth.signIn校验验证码用户不存在时返回 SignUpRequired。
func (r *Router) onAuthSignIn(ctx context.Context, req *tg.AuthSignInRequest) (tg.AuthAuthorizationClass, error) {
u, loginMessage, needSignUp, err := r.deps.Auth.SignIn(ctx, r.authzFromCtx(ctx), req.PhoneNumber, req.PhoneCodeHash, req.PhoneCode)
if err != nil {
return nil, signInErr(err)
}
if needSignUp {
return &tg.AuthAuthorizationSignUpRequired{}, nil
}
if err := r.clearAuthKeyStateOnUserChange(ctx, u.ID); err != nil {
return nil, internalErr()
}
if id, ok := AuthKeyIDFrom(ctx); ok {
r.setAuthUserCache(id, u.ID, true)
}
r.bindSessionUser(ctx, u.ID)
r.recordAndScheduleLoginMessagePush(ctx, loginMessage)
r.pushSignInServiceNotificationToOthers(ctx, u)
return &tg.AuthAuthorization{User: r.tgSelfUser(u)}, nil
}
// onAuthSignUp 处理 auth.signUp创建用户并绑定授权。
func (r *Router) onAuthSignUp(ctx context.Context, req *tg.AuthSignUpRequest) (tg.AuthAuthorizationClass, error) {
u, loginMessage, err := r.deps.Auth.SignUp(ctx, r.authzFromCtx(ctx), req.PhoneNumber, req.PhoneCodeHash, req.FirstName, req.LastName)
if err != nil {
return nil, signInErr(err)
}
if err := r.clearAuthKeyStateOnUserChange(ctx, u.ID); err != nil {
return nil, internalErr()
}
if id, ok := AuthKeyIDFrom(ctx); ok {
r.setAuthUserCache(id, u.ID, true)
}
r.bindSessionUser(ctx, u.ID)
r.recordAndScheduleLoginMessagePush(ctx, loginMessage)
return &tg.AuthAuthorization{User: r.tgSelfUser(u)}, nil
}
// onAuthLogOut 处理 auth.logOut解绑当前 auth_key 的授权。
func (r *Router) onAuthLogOut(ctx context.Context) (*tg.AuthLoggedOut, error) {
id, _ := AuthKeyIDFrom(ctx)
userID, authorized, userErr := r.currentUserID(ctx)
if err := r.deps.Auth.LogOut(ctx, id); err != nil {
return nil, internalErr()
}
r.invalidateAuthUserCache(id)
r.unbindAuthKey(id)
if userErr == nil && authorized && userID != 0 {
status := r.setPresenceFromContext(ctx, userID, true)
r.pushUserStatus(ctx, userID, status)
}
if err := r.clearAuthKeyState(ctx, id); err != nil {
return nil, internalErr()
}
return &tg.AuthLoggedOut{}, nil
}
func (r *Router) clearAuthKeyStateOnUserChange(ctx context.Context, newUserID int64) error {
oldUserID, ok := UserIDFrom(ctx)
if !ok || oldUserID == 0 || oldUserID == newUserID {
return nil
}
id, ok := AuthKeyIDFrom(ctx)
if !ok {
return nil
}
return r.clearAuthKeyState(ctx, id)
}
func (r *Router) clearAuthKeyState(ctx context.Context, authKeyID [8]byte) error {
if r.deps.Updates == nil {
return nil
}
return r.deps.Updates.ClearAuthKey(ctx, authKeyID)
}
func (r *Router) bindSessionUser(ctx context.Context, userID int64) {
if r.deps.Sessions == nil {
return
}
sessionID, ok := SessionIDFrom(ctx)
if !ok {
return
}
if scoped, ok := r.scopedSessions(); ok {
rawAuthKeyID, _ := RawAuthKeyIDFrom(ctx)
scoped.BindUserForAuthKey(rawAuthKeyID, sessionID, userID)
r.announceSessionOnline(ctx, userID)
return
}
r.deps.Sessions.BindUser(sessionID, userID)
r.announceSessionOnline(ctx, userID)
}
func (r *Router) unbindAuthKey(authKeyID [8]byte) {
if r.deps.Sessions == nil {
return
}
r.deps.Sessions.UnbindAuthKey(authKeyID)
}
func (r *Router) pushSignInServiceNotificationToOthers(ctx context.Context, u domain.User) {
if r.deps.Sessions == nil || u.ID == 0 {
return
}
authKeyID, hasAuthKeyID := AuthKeyIDFrom(ctx)
sessionID, hasSessionID := SessionIDFrom(ctx)
if !hasAuthKeyID || !hasSessionID {
return
}
notification := r.tgSignInServiceNotification(ctx, u, authKeyID)
go func() {
pushCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if scoped, ok := r.scopedSessions(); ok {
if sent, err := scoped.PushToUserExceptAuthKeySession(pushCtx, u.ID, authKeyID, sessionID, proto.MessageFromServer, notification); err != nil {
r.log.Debug("push sign-in service notification", zap.Int64("user_id", u.ID), zap.Int("sent", sent), zap.Error(err))
}
return
}
if sent, err := r.deps.Sessions.PushToUserExceptSession(pushCtx, u.ID, sessionID, proto.MessageFromServer, notification); err != nil {
r.log.Debug("push sign-in service notification", zap.Int64("user_id", u.ID), zap.Int("sent", sent), zap.Error(err))
}
}()
}
func (r *Router) recordAndScheduleLoginMessagePush(ctx context.Context, msg domain.Message) {
authKeyID, hasAuthKeyID := AuthKeyIDFrom(ctx)
sessionID, hasSessionID := SessionIDFrom(ctx)
if !hasAuthKeyID || !hasSessionID || msg.ID == 0 {
return
}
event := domain.UpdateEvent{Type: domain.UpdateEventNewMessage, Pts: 1, PtsCount: 1, Date: msg.Date, Message: msg}
state := domain.UpdateState{Pts: 1, Date: msg.Date, Seq: 0}
if r.deps.Updates != nil {
recorded, st, err := r.deps.Updates.RecordNewMessage(ctx, authKeyID, msg.OwnerUserID, msg)
if err != nil {
r.log.Warn("record login message update", zap.Error(err))
return
}
event = recorded
state = st
}
if r.deps.Sessions == nil {
return
}
// 提前从请求 ctx 取出 rawAuthKeyID值类型闭包只捕获该值、不捕获请求 ctx——
// 避免延迟推送的 AfterFunc 在 loginMessagePushDelay 期间延长请求 ctx 链路的存活。
rawAuthKeyID, _ := RawAuthKeyIDFrom(ctx)
time.AfterFunc(loginMessagePushDelay, func() {
pushCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
r.pushLoginMessage(pushCtx, rawAuthKeyID, sessionID, event, state)
})
}
func (r *Router) pushLoginMessage(ctx context.Context, rawAuthKeyID [8]byte, sessionID int64, event domain.UpdateEvent, state domain.UpdateState) {
if r.deps.Sessions == nil || event.Message.ID == 0 {
return
}
updates := tgLoginMessageUpdates(event, state)
if updates == nil {
return
}
var err error
if scoped, ok := r.scopedSessions(); ok && rawAuthKeyID != ([8]byte{}) {
err = scoped.PushToSessionForAuthKey(ctx, rawAuthKeyID, sessionID, proto.MessageFromServer, updates)
} else {
err = r.deps.Sessions.PushToSession(ctx, sessionID, proto.MessageFromServer, updates)
}
if err != nil {
r.log.Debug("push login message", zap.Int64("session_id", sessionID), zap.Error(err))
return
}
r.log.Debug("pushed login message",
zap.Int64("session_id", sessionID),
zap.Int("message_id", event.Message.ID),
zap.Int("pts", event.Pts),
zap.Int("seq", state.Seq),
)
}
func tgLoginMessageUpdates(event domain.UpdateEvent, state domain.UpdateState) *tg.Updates {
item := tgMessage(event.Message)
if item == nil {
return nil
}
if state.Date == 0 {
state.Date = event.Date
}
return &tg.Updates{
Updates: []tg.UpdateClass{
&tg.UpdateNewMessage{
Message: item,
Pts: event.Pts,
PtsCount: event.PtsCount,
},
},
Users: []tg.UserClass{tgUser(domain.OfficialSystemUser())},
Date: state.Date,
Seq: state.Seq,
}
}
func (r *Router) tgSignInServiceNotification(ctx context.Context, u domain.User, authKeyID [8]byte) *tg.Updates {
now := r.clock.Now()
client := "Unknown device"
if ci, ok := ClientInfoFrom(ctx); ok {
parts := []string{}
if ci.DeviceModel != "" {
parts = append(parts, ci.DeviceModel)
}
if ci.SystemVersion != "" {
parts = append(parts, ci.SystemVersion)
}
if ci.AppVersion != "" {
parts = append(parts, ci.AppVersion)
}
if len(parts) > 0 {
client = strings.Join(parts, " / ")
}
}
name := strings.TrimSpace(strings.TrimSpace(u.FirstName + " " + u.LastName))
if name == "" {
name = u.Phone
}
if name == "" {
name = "there"
}
message := fmt.Sprintf("New login.\nDear %s, we detected a login into your account from a new device on %s.\n\nDevice: %s\nLocation: Unknown\n\nIf this wasn't you, you can terminate that session in Settings > Devices (or Privacy & Security > Active Sessions).",
name,
now.UTC().Format(time.RFC1123),
client,
)
authID := int64(binary.LittleEndian.Uint64(authKeyID[:]))
update := &tg.UpdateServiceNotification{
InboxDate: int(now.Unix()),
Type: fmt.Sprintf("auth%d_%d", authID, now.Unix()),
Message: message,
Media: &tg.MessageMediaEmpty{},
Entities: signInNotificationEntities(message),
}
return &tg.Updates{
Updates: []tg.UpdateClass{update},
Date: int(now.Unix()),
}
}
func signInNotificationEntities(message string) []tg.MessageEntityClass {
terms := []string{"New login.", "Settings > Devices", "Privacy & Security > Active Sessions"}
out := make([]tg.MessageEntityClass, 0, len(terms))
for _, term := range terms {
if offset := strings.Index(message, term); offset >= 0 {
out = append(out, &tg.MessageEntityBold{Offset: offset, Length: len(term)})
}
}
return out
}
func authKeyIDFromInt64(v int64) [8]byte {
var id [8]byte
binary.LittleEndian.PutUint64(id[:], uint64(v))
return id
}