confirm.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. package web
  2. import (
  3. "net/http"
  4. "net/url"
  5. "regexp"
  6. "strconv"
  7. "strings"
  8. "github.com/aws/aws-sdk-go-v2/aws/arn"
  9. "github.com/gin-contrib/sessions"
  10. "github.com/gin-gonic/gin"
  11. "github.com/google/uuid"
  12. csrf "github.com/utrack/gin-csrf"
  13. "golang.org/x/exp/maps"
  14. "code.osinet.fr/fgm/sqs_demo/back/services/redriver"
  15. )
  16. type (
  17. // Level is an information level: danger | warning.
  18. Level string
  19. // QueueOp is an allowed operation or reserved error: delete|redrive|invalid.
  20. QueueOp string
  21. )
  22. const (
  23. LevelDanger Level = "danger"
  24. LevelInfo Level = "info"
  25. LevelWarning Level = "warning"
  26. OpDelete QueueOp = "delete"
  27. OpInvalid QueueOp = "invalid"
  28. OpPurge QueueOp = "purge"
  29. OpRedrive QueueOp = "redrive"
  30. )
  31. var confirms = map[QueueOp]struct {
  32. question, description string
  33. confirm string
  34. Level // danger|warning
  35. }{
  36. OpDelete: {
  37. confirm: "Clear",
  38. description: "These messages cannot be recovered after that step",
  39. question: "Do you confirm this deletion request?",
  40. Level: LevelDanger,
  41. },
  42. OpInvalid: {
  43. description: "The operation requested is invalid",
  44. Level: LevelInfo,
  45. },
  46. OpPurge: {
  47. confirm: "Purge",
  48. description: "All the messages in the letter queue will be lost after that step",
  49. question: "Do you confirm this purge request?",
  50. Level: LevelDanger,
  51. },
  52. OpRedrive: {
  53. confirm: "Redrive",
  54. description: "These messages will be in their source queue after that step",
  55. question: "Do you confirm this redriving request?",
  56. Level: LevelWarning,
  57. },
  58. }
  59. // getOp extracts the operation from the unsafe form data, returning a safe value.
  60. func getOp(u *url.URL) QueueOp {
  61. q := u.Query()
  62. found := 0
  63. var res QueueOp
  64. for key := range q {
  65. switch op := QueueOp(strings.ToLower(key)); op {
  66. case OpDelete, OpPurge, OpRedrive:
  67. res = op
  68. found++
  69. default:
  70. continue
  71. }
  72. }
  73. if found != 1 {
  74. return OpInvalid
  75. }
  76. return res
  77. }
  78. type IDValidator func(in string) bool
  79. // validateUint is an IDValidator for unsigned integer strings.
  80. func validateUint(in string) bool {
  81. _, err := strconv.Atoi(in)
  82. return err == nil
  83. }
  84. // validateUUID is an IDValidator for UUID strings
  85. func validateUUID(in string) bool {
  86. _, err := uuid.Parse(in)
  87. return err == nil
  88. }
  89. // validateARN is an IDValidator for ARN strings
  90. func validateARN(in string) bool {
  91. return arn.IsARN(in)
  92. }
  93. func parseIDs(v url.Values, isValid IDValidator) []string {
  94. const prefix = "id-"
  95. var ids []string
  96. for k, vs := range v {
  97. // Weed out bad keys, bad value lengths, and non-matching checkbox statuses.
  98. if pos := strings.Index(k, prefix); pos == -1 || len(vs) != 1 || vs[0] != "on" {
  99. continue
  100. }
  101. b, a, found := strings.Cut(k, prefix)
  102. if b == "" && found && isValid(a) {
  103. ids = append(ids, a)
  104. }
  105. }
  106. return ids
  107. }
  108. func parseMessages(v url.Values, ids []string) []redriver.Message {
  109. m := make(map[int]redriver.Message, len(ids))
  110. keyRx := regexp.MustCompile("^(id|mid|rh)-(.+)")
  111. mOK := make(map[int]bool, len(ids))
  112. for k, vs := range v {
  113. ms := keyRx.FindStringSubmatch(k)
  114. if len(ms) != 3 || len(vs) != 1 {
  115. continue
  116. }
  117. v := vs[0]
  118. id, err := strconv.Atoi(ms[2])
  119. if err != nil {
  120. continue
  121. }
  122. message := m[id]
  123. switch ms[1] {
  124. case "id":
  125. if v == "on" {
  126. mOK[id] = true
  127. }
  128. case "rh":
  129. message.ReceiptHandle = v
  130. case "mid":
  131. message.MessageId = v
  132. }
  133. m[id] = message
  134. }
  135. // Only return selected messages.
  136. res := make(map[int]redriver.Message, len(mOK))
  137. for id := range mOK {
  138. res[id] = m[id]
  139. }
  140. return maps.Values(res)
  141. }
  142. func makeConfirmHandler() gin.HandlerFunc {
  143. return func(c *gin.Context) {
  144. qName := c.Param("name")
  145. u := c.Request.URL
  146. sess := sessions.Default(c)
  147. flashes := sess.Flashes()
  148. defer func() {
  149. _ = sess.Save()
  150. }()
  151. op := getOp(u)
  152. ids := parseIDs(u.Query(), validateUint)
  153. var messages []redriver.Message
  154. if len(ids) == 0 && op != OpPurge {
  155. op = OpInvalid
  156. messages = nil
  157. } else {
  158. messages = parseMessages(u.Query(), ids)
  159. }
  160. token := csrf.GetToken(c)
  161. data := confirms[op] // op is guaranteed to be valid by getOp().
  162. h := gin.H{
  163. "action": "/queue/" + qName + "/" + string(op),
  164. "cancel": "Cancel",
  165. "cancelURL": "/queue/" + qName,
  166. "confirm": data.confirm,
  167. "csrf": token,
  168. "description": data.description,
  169. "flashes": flashes,
  170. "level": data.Level,
  171. "messages": messages,
  172. "question": data.question,
  173. }
  174. if qName == "" {
  175. h["cancelURL"] = "/"
  176. }
  177. if op == OpInvalid {
  178. h["messages"] = nil
  179. }
  180. c.HTML(http.StatusOK, "confirm", h)
  181. }
  182. }