127 lines
4.6 KiB
Go
127 lines
4.6 KiB
Go
package localreset
|
|
|
|
import (
|
|
"context"
|
|
"crypto/subtle"
|
|
|
|
"git.pcux.cn/xuans/Scert/backend/internal/deployment"
|
|
)
|
|
|
|
const MaxRecoveryRequests = 32
|
|
|
|
// RecoverySession 仅供受保护的本机连接使用,与邮箱密码重置阶段互斥。
|
|
type RecoverySession interface {
|
|
RecoveryAvailable() bool
|
|
ListRecoveries(context.Context, string) (deployment.PendingRecoveryPage, error)
|
|
InspectRecovery(context.Context, string) (deployment.RecoveryInspection, error)
|
|
}
|
|
|
|
func validRecoveryRequest(request Request, nonce string) bool {
|
|
if request.Version != Version || subtle.ConstantTimeCompare([]byte(request.Nonce), []byte(nonce)) != 1 ||
|
|
request.Method != "" || request.Code != "" || request.Password != "" {
|
|
return false
|
|
}
|
|
switch request.Action {
|
|
case "recovery_list":
|
|
return request.TransactionID == "" && (request.Cursor == "" || validRecoveryID(request.Cursor)) && noDispositionRequest(request)
|
|
case "recovery_inspect":
|
|
return request.Cursor == "" && validRecoveryID(request.TransactionID) && noDispositionRequest(request)
|
|
case "recovery_prepare":
|
|
return request.Cursor == "" && validRecoveryID(request.TransactionID) && validRecoveryAction(request.RecoveryAction) && request.DispositionID == "" && request.ConfirmationToken == ""
|
|
case "recovery_execute":
|
|
return request.Cursor == "" && request.TransactionID == "" && request.RecoveryAction == "" && validRecoverySecret(request.DispositionID) && validRecoverySecret(request.ConfirmationToken)
|
|
case "recovery_result", "recovery_reconcile_prepare":
|
|
return request.Cursor == "" && request.TransactionID == "" && request.RecoveryAction == "" && validRecoverySecret(request.DispositionID) && request.ConfirmationToken == ""
|
|
}
|
|
return false
|
|
}
|
|
|
|
func validRecoveryID(value string) bool {
|
|
if len(value) == 0 || len(value) > 160 || value == "." || value == ".." {
|
|
return false
|
|
}
|
|
for _, c := range value {
|
|
if (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') && c != '-' && c != '.' && c != '_' {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (c *Client) RecoveryAvailable() bool { return c.recoveryAvailable }
|
|
|
|
func (c *Client) ListRecoveries(cursor string) (deployment.PendingRecoveryPage, error) {
|
|
response, err := c.recoveryExchange(Request{Action: "recovery_list", Cursor: cursor})
|
|
if err != nil {
|
|
return deployment.PendingRecoveryPage{}, err
|
|
}
|
|
return *response.RecoveryPage, nil
|
|
}
|
|
|
|
func (c *Client) InspectRecovery(id string) (deployment.RecoveryInspection, error) {
|
|
response, err := c.recoveryExchange(Request{Action: "recovery_inspect", TransactionID: id})
|
|
if err != nil {
|
|
return deployment.RecoveryInspection{}, err
|
|
}
|
|
return *response.Inspection, nil
|
|
}
|
|
|
|
func (c *Client) recoveryExchange(request Request) (Response, error) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
fail := func() (Response, error) {
|
|
c.stage = -1
|
|
_ = c.Close()
|
|
return Response{}, ErrProtocol
|
|
}
|
|
if !c.recoveryAvailable || (c.stage != 0 && c.stage != 3) {
|
|
return fail()
|
|
}
|
|
if request.Action != "recovery_list" && request.Action != "recovery_inspect" && !c.dispositionAvailable {
|
|
return fail()
|
|
}
|
|
request.Version, request.Nonce = Version, c.nonce
|
|
if !validRecoveryRequest(request, c.nonce) || writeFrame(c.conn, request) != nil {
|
|
return fail()
|
|
}
|
|
var response Response
|
|
if readFrame(c.reader, &response) != nil || !validChallenge(response) || response.Nonce == c.nonce ||
|
|
len(response.Methods) != 0 || response.RecoveryAvailable || response.DispositionAvailable {
|
|
return fail()
|
|
}
|
|
if request.Action == "recovery_list" {
|
|
if response.RecoveryPage == nil || response.Inspection != nil || response.InspectionPart != nil || hasDispositionPayload(response) || len(response.RecoveryPage.Items) > deployment.RecoveryPageSize {
|
|
return fail()
|
|
}
|
|
previous := request.Cursor
|
|
for _, item := range response.RecoveryPage.Items {
|
|
if !validRecoveryID(item.ID) || item.ID <= previous {
|
|
return fail()
|
|
}
|
|
previous = item.ID
|
|
}
|
|
if cursor := response.RecoveryPage.NextCursor; cursor != "" &&
|
|
(len(response.RecoveryPage.Items) != deployment.RecoveryPageSize || cursor != previous) {
|
|
return fail()
|
|
}
|
|
} else if request.Action == "recovery_inspect" {
|
|
if hasDispositionPayload(response) {
|
|
return fail()
|
|
}
|
|
if response.InspectionPart != nil {
|
|
inspection, err := readInspectionParts(c.reader, response)
|
|
if err != nil {
|
|
return fail()
|
|
}
|
|
response.Inspection, response.InspectionPart = inspection, nil
|
|
}
|
|
if !validInspectionPayload(response.Inspection) || response.RecoveryPage != nil || response.Inspection.TransactionID != request.TransactionID {
|
|
return fail()
|
|
}
|
|
} else if !c.dispositionAvailable || !validDispositionResponse(request, response) {
|
|
return fail()
|
|
}
|
|
c.nonce, c.stage = response.Nonce, 3
|
|
return response, nil
|
|
}
|