@@ -6,47 +6,90 @@ package middleware
66import (
77 "encoding/json"
88 "net/http"
9+ "net/url"
910 "os"
1011 "strings"
1112 "sync"
1213)
1314
1415// CORS is a simple "always add CORS headers" middleware.
1516// The legacy Python backend sets these headers in response middleware.
16-
1717var (
1818 allowedOriginsOnce sync.Once
1919 allowedOrigins []string
2020 allowAllOrigins bool
2121)
2222
23+ func normalizeAllowedOrigin (raw string ) string {
24+ raw = strings .TrimSpace (strings .Trim (raw , "\" '" ))
25+ if raw == "" || raw == "*" {
26+ return raw
27+ }
28+
29+ // CLA_CONTRIBUTOR_BASE / CLA_CONTRIBUTOR_V2_BASE can be configured as a
30+ // hostname. Browser Origin values are scheme + host only.
31+ if ! strings .Contains (raw , "://" ) {
32+ raw = "https://" + raw
33+ }
34+
35+ u , err := url .Parse (raw )
36+ if err == nil && u .Scheme != "" && u .Host != "" {
37+ return u .Scheme + "://" + u .Host
38+ }
39+
40+ return strings .TrimRight (raw , "/" )
41+ }
42+
43+ func addAllowedOrigin (raw string ) {
44+ origin := normalizeAllowedOrigin (raw )
45+ if origin == "" {
46+ return
47+ }
48+ if origin == "*" {
49+ allowAllOrigins = true
50+ return
51+ }
52+
53+ for _ , existing := range allowedOrigins {
54+ if existing == origin {
55+ return
56+ }
57+ }
58+ allowedOrigins = append (allowedOrigins , origin )
59+ }
60+
61+ func addContributorConsoleOrigins () {
62+ addAllowedOrigin (os .Getenv ("CLA_CONTRIBUTOR_BASE" ))
63+ addAllowedOrigin (os .Getenv ("CLA_CONTRIBUTOR_V2_BASE" ))
64+ }
65+
2366func loadAllowedOriginsFromEnv () {
2467 raw := strings .TrimSpace (os .Getenv ("ALLOWED_ORIGINS" ))
2568 if raw == "" {
2669 // Backwards compatible default: allow all.
2770 allowAllOrigins = true
71+ addContributorConsoleOrigins ()
2872 return
2973 }
74+
3075 // Supported formats:
31- // - JSON array: ["https://a", "https://b"]
32- // - CSV: https://a,https://b
33- // - Space/newline separated
76+ // - JSON array: ["https://a", "https://b"]
77+ // - CSV: https://a,https://b
78+ // - Space/newline separated
3479 if strings .HasPrefix (raw , "[" ) {
3580 var arr []string
3681 if err := json .Unmarshal ([]byte (raw ), & arr ); err == nil {
3782 for _ , v := range arr {
38- v = strings .TrimSpace (strings .Trim (v , "\" '" ))
39- if v == "" {
40- continue
41- }
42- allowedOrigins = append (allowedOrigins , v )
43- if v == "*" {
44- allowAllOrigins = true
45- }
83+ addAllowedOrigin (v )
84+ }
85+ addContributorConsoleOrigins ()
86+ if len (allowedOrigins ) == 0 && ! allowAllOrigins {
87+ allowAllOrigins = true
4688 }
4789 return
4890 }
4991 }
92+
5093 parts := strings .FieldsFunc (raw , func (r rune ) bool {
5194 switch r {
5295 case ',' , ';' , ' ' , '\n' , '\t' , '\r' :
@@ -56,16 +99,15 @@ func loadAllowedOriginsFromEnv() {
5699 }
57100 })
58101 for _ , p := range parts {
59- p = strings .TrimSpace (strings .Trim (p , "\" '" ))
60- if p == "" {
61- continue
62- }
63- allowedOrigins = append (allowedOrigins , p )
64- if p == "*" {
65- allowAllOrigins = true
66- }
102+ addAllowedOrigin (p )
67103 }
68- if len (allowedOrigins ) == 0 {
104+
105+ // The legacy GitHub signing flow redirects contributors to these consoles.
106+ // Therefore these origins must be allowed to call the v1/v2 APIs after
107+ // GitHub OAuth redirects back to EasyCLA.
108+ addContributorConsoleOrigins ()
109+
110+ if len (allowedOrigins ) == 0 && ! allowAllOrigins {
69111 allowAllOrigins = true
70112 }
71113}
@@ -75,10 +117,12 @@ func isOriginAllowed(origin string) bool {
75117 if allowAllOrigins {
76118 return true
77119 }
78- origin = strings .TrimSpace (origin )
120+
121+ origin = normalizeAllowedOrigin (origin )
79122 if origin == "" {
80123 return false
81124 }
125+
82126 for _ , o := range allowedOrigins {
83127 if origin == o {
84128 return true
@@ -89,22 +133,21 @@ func isOriginAllowed(origin string) bool {
89133
90134func CORS (next http.Handler ) http.Handler {
91135 return http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
92- origin := r .Header .Get ("Origin" )
136+ allowedOriginsOnce .Do (loadAllowedOriginsFromEnv )
137+
138+ origin := normalizeAllowedOrigin (r .Header .Get ("Origin" ))
139+
93140 if origin != "" && isOriginAllowed (origin ) {
94- // Echo the origin when allowlisting is enabled .
141+ // Echo the origin. Browsers reject "*" when credentials/cookies are used .
95142 w .Header ().Set ("Access-Control-Allow-Origin" , origin )
96143 w .Header ().Add ("Vary" , "Origin" )
97144 } else if origin == "" && isOriginAllowed ("*" ) {
98145 // Non-browser clients.
99146 w .Header ().Set ("Access-Control-Allow-Origin" , "*" )
100- } else if origin != "" && isOriginAllowed ("*" ) {
101- // Backwards compatible default: allow all.
102- w .Header ().Set ("Access-Control-Allow-Origin" , "*" )
103147 }
148+
104149 // Legacy Python sets the string literal "true".
105150 w .Header ().Set ("Access-Control-Allow-Credentials" , "true" )
106- // Keep this list *exactly* aligned with the legacy Python middleware:
107- // response.set_header("Access-Control-Allow-Headers", "Content-Type, Authorization")
108151 w .Header ().Set ("Access-Control-Allow-Headers" , "Content-Type, Authorization" )
109152 w .Header ().Set ("Access-Control-Allow-Methods" , "GET, POST, PUT, DELETE, OPTIONS" )
110153
0 commit comments