203 lines
4.5 KiB
Go
203 lines
4.5 KiB
Go
package web
|
|
|
|
import (
|
|
"net/http"
|
|
"net/url"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/aws/arn"
|
|
"github.com/gin-contrib/sessions"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
csrf "github.com/utrack/gin-csrf"
|
|
"golang.org/x/exp/maps"
|
|
|
|
"code.osinet.fr/fgm/sqs_demo/back/services/redriver"
|
|
)
|
|
|
|
type (
|
|
// Level is an information level: danger | warning.
|
|
Level string
|
|
// QueueOp is an allowed operation or reserved error: delete|redrive|invalid.
|
|
QueueOp string
|
|
)
|
|
|
|
const (
|
|
LevelDanger Level = "danger"
|
|
LevelInfo Level = "info"
|
|
LevelWarning Level = "warning"
|
|
|
|
OpDelete QueueOp = "delete"
|
|
OpInvalid QueueOp = "invalid"
|
|
OpPurge QueueOp = "purge"
|
|
OpRedrive QueueOp = "redrive"
|
|
)
|
|
|
|
var confirms = map[QueueOp]struct {
|
|
question, description string
|
|
confirm string
|
|
Level // danger|warning
|
|
}{
|
|
OpDelete: {
|
|
confirm: "Clear",
|
|
description: "These messages cannot be recovered after that step",
|
|
question: "Do you confirm this deletion request?",
|
|
Level: LevelDanger,
|
|
},
|
|
OpInvalid: {
|
|
description: "The operation requested is invalid",
|
|
Level: LevelInfo,
|
|
},
|
|
OpPurge: {
|
|
confirm: "Purge",
|
|
description: "All the messages in the letter queue will be lost after that step",
|
|
question: "Do you confirm this purge request?",
|
|
Level: LevelDanger,
|
|
},
|
|
OpRedrive: {
|
|
confirm: "Redrive",
|
|
description: "These messages will be in their source queue after that step",
|
|
question: "Do you confirm this redriving request?",
|
|
Level: LevelWarning,
|
|
},
|
|
}
|
|
|
|
// getOp extracts the operation from the unsafe form data, returning a safe value.
|
|
func getOp(u *url.URL) QueueOp {
|
|
q := u.Query()
|
|
found := 0
|
|
var res QueueOp
|
|
for key := range q {
|
|
switch op := QueueOp(strings.ToLower(key)); op {
|
|
case OpDelete, OpPurge, OpRedrive:
|
|
res = op
|
|
found++
|
|
default:
|
|
continue
|
|
}
|
|
}
|
|
if found != 1 {
|
|
return OpInvalid
|
|
}
|
|
return res
|
|
}
|
|
|
|
type IDValidator func(in string) bool
|
|
|
|
// validateUint is an IDValidator for unsigned integer strings.
|
|
func validateUint(in string) bool {
|
|
_, err := strconv.Atoi(in)
|
|
return err == nil
|
|
}
|
|
|
|
// validateUUID is an IDValidator for UUID strings
|
|
func validateUUID(in string) bool {
|
|
_, err := uuid.Parse(in)
|
|
return err == nil
|
|
}
|
|
|
|
// validateARN is an IDValidator for ARN strings
|
|
func validateARN(in string) bool {
|
|
return arn.IsARN(in)
|
|
}
|
|
|
|
func parseIDs(v url.Values, isValid IDValidator) []string {
|
|
const prefix = "id-"
|
|
var ids []string
|
|
for k, vs := range v {
|
|
// Weed out bad keys, bad value lengths, and non-matching checkbox statuses.
|
|
if pos := strings.Index(k, prefix); pos == -1 || len(vs) != 1 || vs[0] != "on" {
|
|
continue
|
|
}
|
|
b, a, found := strings.Cut(k, prefix)
|
|
if b == "" && found && isValid(a) {
|
|
ids = append(ids, a)
|
|
}
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func parseMessages(v url.Values, ids []string) []redriver.Message {
|
|
m := make(map[int]redriver.Message, len(ids))
|
|
keyRx := regexp.MustCompile("^(id|mid|rh)-(.+)")
|
|
mOK := make(map[int]bool, len(ids))
|
|
for k, vs := range v {
|
|
ms := keyRx.FindStringSubmatch(k)
|
|
if len(ms) != 3 || len(vs) != 1 {
|
|
continue
|
|
}
|
|
v := vs[0]
|
|
|
|
id, err := strconv.Atoi(ms[2])
|
|
if err != nil {
|
|
continue
|
|
}
|
|
message := m[id]
|
|
switch ms[1] {
|
|
case "id":
|
|
if v == "on" {
|
|
mOK[id] = true
|
|
}
|
|
case "rh":
|
|
message.ReceiptHandle = v
|
|
case "mid":
|
|
message.MessageId = v
|
|
}
|
|
m[id] = message
|
|
}
|
|
|
|
// Only return selected messages.
|
|
res := make(map[int]redriver.Message, len(mOK))
|
|
for id := range mOK {
|
|
res[id] = m[id]
|
|
}
|
|
return maps.Values(res)
|
|
}
|
|
|
|
func makeConfirmHandler() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
qName := c.Param("name")
|
|
u := c.Request.URL
|
|
sess := sessions.Default(c)
|
|
flashes := sess.Flashes()
|
|
defer func() {
|
|
_ = sess.Save()
|
|
}()
|
|
|
|
op := getOp(u)
|
|
ids := parseIDs(u.Query(), validateUint)
|
|
var messages []redriver.Message
|
|
if len(ids) == 0 && op != OpPurge {
|
|
op = OpInvalid
|
|
messages = nil
|
|
} else {
|
|
messages = parseMessages(u.Query(), ids)
|
|
}
|
|
|
|
token := csrf.GetToken(c)
|
|
|
|
data := confirms[op] // op is guaranteed to be valid by getOp().
|
|
|
|
h := gin.H{
|
|
"action": "/queue/" + qName + "/" + string(op),
|
|
"cancel": "Cancel",
|
|
"cancelURL": "/queue/" + qName,
|
|
"confirm": data.confirm,
|
|
"csrf": token,
|
|
"description": data.description,
|
|
"flashes": flashes,
|
|
"level": data.Level,
|
|
"messages": messages,
|
|
"question": data.question,
|
|
}
|
|
if qName == "" {
|
|
h["cancelURL"] = "/"
|
|
}
|
|
if op == OpInvalid {
|
|
h["messages"] = nil
|
|
}
|
|
c.HTML(http.StatusOK, "confirm", h)
|
|
}
|
|
}
|