You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

170 lines
4.7KB

  1. package cors
  2. import (
  3. "fmt"
  4. "log"
  5. "net/http"
  6. "net/url"
  7. "strconv"
  8. "strings"
  9. macaron "gitea.com/macaron/macaron"
  10. )
  11. const version = "0.1.1"
  12. const anyDomain = "!*"
  13. // Version returns the version of this module
  14. func Version() string {
  15. return version
  16. }
  17. /*
  18. Options to configure the CORS middleware read from the [cors] section of the ini configuration file.
  19. SCHEME may be http or https as accepted schemes or the '*' wildcard to accept any scheme.
  20. ALLOW_DOMAIN may be a comma separated list of domains that are allowed to run CORS requests
  21. Special values are the a single '*' wildcard that will allow any domain to send requests without
  22. credentials and the special '!*' wildcard which will reply with requesting domain in the 'access-control-allow-origin'
  23. header and hence allow requess from any domain *with* credentials.
  24. ALLOW_SUBDOMAIN set to true accepts requests from any subdomain of ALLOW_DOMAIN.
  25. METHODS may be a comma separated list of HTTP-methods to be accepted.
  26. MAX_AGE_SECONDS may be the duration in secs for which the response is cached (default 600).
  27. ref: https://stackoverflow.com/questions/54300997/is-it-possible-to-cache-http-options-response?noredirect=1#comment95790277_54300997
  28. ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age
  29. ALLOW_CREDENTIALS set to false rejects any request with credentials.
  30. */
  31. type Options struct {
  32. Section string
  33. Scheme string
  34. AllowDomain []string
  35. AllowSubdomain bool
  36. Methods []string
  37. MaxAgeSeconds int
  38. AllowCredentials bool
  39. }
  40. func prepareOptions(options []Options) Options {
  41. var opt Options
  42. if len(options) > 0 {
  43. opt = options[0]
  44. }
  45. if len(opt.Section) == 0 {
  46. opt.Section = "cors"
  47. }
  48. sec := macaron.Config().Section(opt.Section)
  49. if len(opt.Scheme) == 0 {
  50. opt.Scheme = sec.Key("SCHEME").MustString("http")
  51. }
  52. if len(opt.AllowDomain) == 0 {
  53. opt.AllowDomain = sec.Key("ALLOW_DOMAIN").Strings(",")
  54. if len(opt.AllowDomain) == 0 {
  55. opt.AllowDomain = []string{"*"}
  56. }
  57. }
  58. if !opt.AllowSubdomain {
  59. opt.AllowSubdomain = sec.Key("ALLOW_SUBDOMAIN").MustBool(false)
  60. }
  61. if len(opt.Methods) == 0 {
  62. opt.Methods = sec.Key("METHODS").Strings(",")
  63. if len(opt.Methods) == 0 {
  64. opt.Methods = []string{
  65. http.MethodGet,
  66. http.MethodHead,
  67. http.MethodPost,
  68. http.MethodPut,
  69. http.MethodPatch,
  70. http.MethodDelete,
  71. http.MethodOptions,
  72. }
  73. }
  74. }
  75. if opt.MaxAgeSeconds <= 0 {
  76. opt.MaxAgeSeconds = sec.Key("MAX_AGE_SECONDS").MustInt(600)
  77. }
  78. if !opt.AllowCredentials {
  79. opt.AllowCredentials = sec.Key("ALLOW_CREDENTIALS").MustBool(true)
  80. }
  81. return opt
  82. }
  83. // CORS responds to preflight requests with adequat access-control-* respond headers
  84. // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
  85. // https://fetch.spec.whatwg.org/#cors-protocol-and-credentials
  86. func CORS(options ...Options) macaron.Handler {
  87. opt := prepareOptions(options)
  88. return func(ctx *macaron.Context, log *log.Logger) {
  89. reqOptions := ctx.Req.Method == http.MethodOptions
  90. headers := map[string]string{
  91. "access-control-allow-methods": strings.Join(opt.Methods, ","),
  92. "access-control-allow-headers": ctx.Req.Header.Get("access-control-request-headers"),
  93. "access-control-max-age": strconv.Itoa(opt.MaxAgeSeconds),
  94. }
  95. if opt.AllowDomain[0] == "*" {
  96. headers["access-control-allow-origin"] = "*"
  97. } else {
  98. origin := ctx.Req.Header.Get("Origin")
  99. if reqOptions && origin == "" {
  100. respErrorf(ctx, log, http.StatusBadRequest, "missing origin header in CORS request")
  101. return
  102. }
  103. u, err := url.Parse(origin)
  104. if err != nil {
  105. respErrorf(ctx, log, http.StatusBadRequest, "Failed to parse CORS origin header. Reason: %v", err)
  106. return
  107. }
  108. ok := false
  109. for _, d := range opt.AllowDomain {
  110. if u.Hostname() == d || (opt.AllowSubdomain && strings.HasSuffix(u.Hostname(), "."+d)) || d == anyDomain {
  111. ok = true
  112. break
  113. }
  114. }
  115. if ok {
  116. if opt.Scheme != "*" {
  117. u.Scheme = opt.Scheme
  118. }
  119. headers["access-control-allow-origin"] = u.String()
  120. headers["access-control-allow-credentials"] = strconv.FormatBool(opt.AllowCredentials)
  121. headers["vary"] = "Origin"
  122. }
  123. if reqOptions && !ok {
  124. respErrorf(ctx, log, http.StatusBadRequest, "CORS request from prohibited domain %v", origin)
  125. return
  126. }
  127. }
  128. ctx.Resp.Before(func(w macaron.ResponseWriter) {
  129. for k, v := range headers {
  130. w.Header().Set(k, v)
  131. }
  132. })
  133. if reqOptions {
  134. ctx.Resp.WriteHeader(200) // return response
  135. return
  136. }
  137. }
  138. }
  139. func respErrorf(ctx *macaron.Context, log *log.Logger, statusCode int, format string, a ...interface{}) {
  140. msg := fmt.Sprintf(format, a...)
  141. log.Println(msg)
  142. ctx.WriteHeader(statusCode)
  143. _, err := ctx.Write([]byte(msg))
  144. if err != nil {
  145. panic(err)
  146. }
  147. return
  148. }