confirm.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  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: "Delete",
  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 deadletter 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, 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(u *url.URL, isValid IDValidator) []string {
  94. const prefix = "id-"
  95. q := u.Query()
  96. var ids []string
  97. for k, vs := range q {
  98. // Weed out bad keys, bad value lengths, and non-matching checkbox statuses.
  99. if pos := strings.Index(k, prefix); pos == -1 || len(vs) != 1 || vs[0] != "on" {
  100. continue
  101. }
  102. b, a, found := strings.Cut(k, prefix)
  103. if b == "" && found && isValid(a) {
  104. ids = append(ids, a)
  105. }
  106. }
  107. return ids
  108. }
  109. func parseMessages(u *url.URL, ids []string) []redriver.Message {
  110. m := make(map[int]redriver.Message, len(ids))
  111. keyRx := regexp.MustCompile("^(id|mid|rh)-(.+)")
  112. mOK := make(map[int]bool, len(ids))
  113. for k, vs := range u.Query() {
  114. ms := keyRx.FindStringSubmatch(k)
  115. if len(ms) != 3 || len(vs) != 1 {
  116. continue
  117. }
  118. v := vs[0]
  119. id, err := strconv.Atoi(ms[2])
  120. if err != nil {
  121. continue
  122. }
  123. message := m[id]
  124. switch ms[1] {
  125. case "id":
  126. if v == "on" {
  127. mOK[id] = true
  128. }
  129. case "rh":
  130. message.ReceiptHandle = v
  131. case "mid":
  132. message.MessageId = v
  133. }
  134. m[id] = message
  135. }
  136. // Only return selected messages.
  137. res := make(map[int]redriver.Message, len(mOK))
  138. for id := range mOK {
  139. res[id] = m[id]
  140. }
  141. return maps.Values(res)
  142. }
  143. func makeConfirmHandler() gin.HandlerFunc {
  144. return func(c *gin.Context) {
  145. u := c.Request.URL
  146. op := getOp(u)
  147. // For real data, probably use validateUUID or validateARN.
  148. ids := parseIDs(u, validateUint)
  149. var messages []redriver.Message
  150. if len(ids) == 0 {
  151. op = OpInvalid
  152. messages = nil
  153. } else {
  154. messages = parseMessages(u, ids)
  155. }
  156. data := confirms[op] // op is guaranteed to be valid by getOp().
  157. qName := c.Param("name")
  158. sess := sessions.Default(c)
  159. flashes := sess.Flashes()
  160. token := csrf.GetToken(c)
  161. _ = sess.Save()
  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. }