Smail/backend/services/adminreset/client.go
smail b183aef0c7
Some checks are pending
Security Checks / security (push) Waiting to run
完善管理员重置结果提示并缩短邮件验证等待
2026-09-19 22:53:58 +08:00

164 lines
4.6 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 adminreset
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"strconv"
"strings"
"time"
)
var (
ErrVerificationUnavailable = errors.New("no verification method available")
ErrVerificationFailed = errors.New("verification failed")
ErrEmailDeliveryFailed = errors.New("verification email delivery failed")
)
func responseError(code string) error {
switch code {
case "VERIFICATION_UNAVAILABLE":
return ErrVerificationUnavailable
case "VERIFICATION_FAILED":
return ErrVerificationFailed
case "EMAIL_DELIVERY_FAILED":
return ErrEmailDeliveryFailed
case "REPLAY_OR_EXPIRED":
return ErrExpiredSession
default:
return ErrInvalidRequest
}
}
func SendVerifiedReset(ctx context.Context, conn net.Conn, input func(string) (string, error), readPassword func() (string, error)) error {
if input == nil {
return ErrInvalidRequest
}
return SendVerifiedResetWithInput(ctx, conn, func(_ context.Context, prompt string) (string, error) {
return input(prompt)
}, readPassword)
}
// SendVerifiedResetWithInput 允许邮件发送与验证码输入并行,发送失败会取消输入。
func SendVerifiedResetWithInput(ctx context.Context, conn net.Conn, input func(context.Context, string) (string, error), readPassword func() (string, error)) error {
if conn == nil || input == nil || readPassword == nil {
return ErrInvalidRequest
}
deadline := time.Now().Add(controlRequestTimeout)
if contextDeadline, ok := ctx.Deadline(); ok && contextDeadline.Before(deadline) {
deadline = contextDeadline
}
if err := conn.SetDeadline(deadline); err != nil {
return fmt.Errorf("set reset deadline: %w", err)
}
defer conn.SetDeadline(time.Time{})
challenge, err := DecodeResponse(conn)
if err != nil {
return err
}
if !challenge.OK {
return responseError(challenge.Code)
}
if challenge.Code != "CHALLENGE" || challenge.Nonce == "" {
return ErrInvalidRequest
}
var prompt strings.Builder
prompt.WriteString("请选择安全验证方式\n\n")
for i, method := range challenge.Methods {
fmt.Fprintf(&prompt, "%d. %s\n", i+1, method.Label)
}
prompt.WriteString("\n选项:")
choice, err := input(ctx, prompt.String())
if err != nil {
return err
}
index, err := strconv.Atoi(strings.TrimSpace(choice))
if err != nil || index < 1 || index > len(challenge.Methods) {
return ErrInvalidRequest
}
exchange := func(request Request) error {
request.Version, request.Nonce = ProtocolVersion, challenge.Nonce
if err := json.NewEncoder(conn).Encode(request); err != nil {
return err
}
response, err := DecodeResponse(conn)
if err != nil {
return err
}
if !response.OK {
return responseError(response.Code)
}
return nil
}
method := challenge.Methods[index-1].ID
request := Request{Version: ProtocolVersion, Nonce: challenge.Nonce, Action: "select", Method: method}
var code string
if strings.HasPrefix(method, "email:") {
code, err = selectEmailAndReadCode(ctx, conn, request, input)
} else {
if err = exchange(request); err != nil {
return err
}
code, err = input(ctx, "请输入所选方式的验证码或安全码")
}
if err != nil {
return err
}
err = exchange(Request{Action: "verify", Code: code})
clearPassword(&code)
if err != nil {
return err
}
value, err := readPassword()
if err != nil {
return err
}
defer clearPassword(&value)
if err := json.NewEncoder(conn).Encode(Request{Version: ProtocolVersion, Nonce: challenge.Nonce, Action: "reset", NewPassword: value}); err != nil {
return fmt.Errorf("send reset request: %w", err)
}
response, err := DecodeResponse(conn)
if err != nil {
return fmt.Errorf("read reset response: %w", err)
}
if !response.OK {
return responseError(response.Code)
}
return nil
}
func selectEmailAndReadCode(ctx context.Context, conn net.Conn, request Request, input func(context.Context, string) (string, error)) (string, error) {
if err := json.NewEncoder(conn).Encode(request); err != nil {
return "", err
}
inputContext, cancel := context.WithCancel(ctx)
defer cancel()
result := make(chan error, 1)
go func() {
response, err := DecodeResponse(conn)
if err == nil && !response.OK {
err = responseError(response.Code)
}
if err != nil {
cancel()
}
result <- err
}()
code, inputErr := input(inputContext, "正在发送邮件验证码,收到后输入\n请输入邮件验证码")
if inputErr != nil {
// 取消输入时关闭本次连接,同时解除等待投递结果的读取。
_ = conn.Close()
}
deliveryErr := <-result
if deliveryErr != nil || inputErr != nil {
clearPassword(&code)
if deliveryErr != nil {
return "", deliveryErr
}
return "", inputErr
}
return code, nil
}