├── .gitignore ├── .deepsource.toml ├── .github └── workflows │ ├── go.yml │ └── lock-threads.yml ├── README.md ├── go.mod ├── mux ├── cors.go └── cors_test.go ├── cors.go ├── gin ├── cors.go └── cors_test.go ├── cors_test.go ├── go.sum └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | -------------------------------------------------------------------------------- /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "go" 5 | enabled = true 6 | 7 | [analyzers.meta] 8 | import_root = "github.com/krakendio/krakend-cors" 9 | 10 | [[transformers]] 11 | name = "gofmt" 12 | enabled = true 13 | 14 | [[transformers]] 15 | name = "gofumpt" 16 | enabled = true -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v6 15 | - uses: actions/setup-go@v6 16 | with: 17 | go-version: '1.24' 18 | - run: go test -v ./... 19 | -------------------------------------------------------------------------------- /.github/workflows/lock-threads.yml: -------------------------------------------------------------------------------- 1 | name: 'Lock Threads' 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | concurrency: 13 | group: lock 14 | 15 | jobs: 16 | action: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: dessant/lock-threads@v3 20 | with: 21 | pr-inactive-days: '90' 22 | issue-inactive-days: '90' 23 | add-issue-labels: 'locked' 24 | issue-comment: > 25 | This issue was marked as resolved a long time ago and now has been 26 | automatically locked as there has not been any recent activity after it. 27 | You can still open a new issue and reference this link. 28 | pr-comment: > 29 | This pull request was marked as resolved a long time ago and now has been 30 | automatically locked as there has not been any recent activity after it. 31 | You can still open a new issue and reference this link. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Report Card](https://goreportcard.com/badge/github.com/krakend/krakend-cors)](https://goreportcard.com/report/github.com/krakend/krakend-cors) [![GoDoc](https://godoc.org/github.com/krakend/krakend-cors?status.svg)](https://godoc.org/github.com/krakend/krakend-cors) 2 | 3 | KrakenD CORS 4 | ==== 5 | 6 | A set of building blocks for instrumenting [KrakenD](http://www.krakend.io) gateways 7 | 8 | ## Available flavours 9 | 10 | 1. [mux](github.com/krakend/krakend-cors/blob/master/mux) Mux based handlers 11 | 2. [gin](github.com/krakend/krakend-cors/blob/master/gin) Gin based handlers 12 | 13 | Check the tests and the documentation for more details 14 | 15 | ## Configuration 16 | 17 | You need to add an ExtraConfig section to the configuration to enable the CORS middleware. 18 | At least one option should be defined. 19 | 20 | - `allow_origins` list of strings (you can also use a wildcard, leaving it empty allows all origins too) 21 | - `allow_headers` list of strings 22 | - `allow_methods` list of strings 23 | - `expose_headers` list of strings 24 | - `allow_credentials` bool 25 | - `max_age` duration (Ex: "12h", "5m", "3600s", ...) 26 | 27 | ### Configuration Example 28 | 29 | ``` 30 | "extra_config": { 31 | "github_com/devopsfaith/krakend-cors": { 32 | "allow_origins": [ "http://foobar.com" ], 33 | "allow_methods": [ "POST", "GET"], 34 | "max_age": "12h" 35 | } 36 | } 37 | ``` 38 | 39 | or leave the defaults (the defaults allows all origins): 40 | ``` 41 | "extra_config": { 42 | "github_com/devopsfaith/krakend-cors": { 43 | "allow_origins": [] 44 | } 45 | } 46 | ``` 47 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/krakend/krakend-cors/v2 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.9.1 7 | github.com/luraproject/lura/v2 v2.11.0 8 | github.com/rs/cors v1.11.1 9 | github.com/rs/cors/wrapper/gin v0.0.0-20240830163046-1084d89a1692 10 | ) 11 | 12 | require ( 13 | github.com/bytedance/sonic v1.12.5 // indirect 14 | github.com/bytedance/sonic/loader v0.2.0 // indirect 15 | github.com/cloudwego/base64x v0.1.4 // indirect 16 | github.com/cloudwego/iasm v0.2.0 // indirect 17 | github.com/gabriel-vasile/mimetype v1.4.7 // indirect 18 | github.com/gin-contrib/sse v0.1.0 // indirect 19 | github.com/go-playground/locales v0.14.1 // indirect 20 | github.com/go-playground/universal-translator v0.18.1 // indirect 21 | github.com/go-playground/validator/v10 v10.23.0 // indirect 22 | github.com/goccy/go-json v0.10.4 // indirect 23 | github.com/json-iterator/go v1.1.12 // indirect 24 | github.com/klauspost/cpuid/v2 v2.2.9 // indirect 25 | github.com/kr/pretty v0.3.1 // indirect 26 | github.com/krakend/flatmap v1.2.0 // indirect 27 | github.com/leodido/go-urn v1.4.0 // indirect 28 | github.com/mattn/go-isatty v0.0.20 // indirect 29 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 30 | github.com/modern-go/reflect2 v1.0.2 // indirect 31 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 32 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 33 | github.com/ugorji/go/codec v1.2.12 // indirect 34 | github.com/valyala/fastrand v1.1.0 // indirect 35 | golang.org/x/arch v0.18.0 // indirect 36 | golang.org/x/crypto v0.45.0 // indirect 37 | golang.org/x/net v0.47.0 // indirect 38 | golang.org/x/sys v0.38.0 // indirect 39 | golang.org/x/text v0.31.0 // indirect 40 | google.golang.org/protobuf v1.36.6 // indirect 41 | gopkg.in/yaml.v3 v3.0.1 // indirect 42 | ) 43 | -------------------------------------------------------------------------------- /mux/cors.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "log" 7 | 8 | krakendcors "github.com/krakend/krakend-cors/v2" 9 | "github.com/luraproject/lura/v2/config" 10 | "github.com/luraproject/lura/v2/logging" 11 | "github.com/luraproject/lura/v2/router/mux" 12 | "github.com/rs/cors" 13 | ) 14 | 15 | // New returns a mux.HandlerMiddleware (which implements the http.Handler interface) 16 | // with the CORS configuration defined in the ExtraConfig. 17 | func New(e config.ExtraConfig) mux.HandlerMiddleware { 18 | return NewWithLogger(e, nil) 19 | } 20 | 21 | func NewWithLogger(e config.ExtraConfig, l logging.Logger) mux.HandlerMiddleware { 22 | tmp := krakendcors.ConfigGetter(e) 23 | if tmp == nil { 24 | return nil 25 | } 26 | cfg, ok := tmp.(krakendcors.Config) 27 | if !ok { 28 | return nil 29 | } 30 | 31 | if len(cfg.AllowOrigins) == 0 { 32 | cfg.AllowOrigins = []string{"*"} 33 | } 34 | if len(cfg.AllowHeaders) == 0 { 35 | cfg.AllowHeaders = []string{"*"} 36 | } 37 | 38 | c := cors.New(cors.Options{ 39 | AllowedOrigins: cfg.AllowOrigins, 40 | AllowedMethods: cfg.AllowMethods, 41 | AllowedHeaders: cfg.AllowHeaders, 42 | ExposedHeaders: cfg.ExposeHeaders, 43 | AllowCredentials: cfg.AllowCredentials, 44 | AllowPrivateNetwork: cfg.AllowPrivateNetwork, 45 | OptionsPassthrough: cfg.OptionsPassthrough, 46 | OptionsSuccessStatus: cfg.OptionsSuccessStatus, 47 | Debug: cfg.Debug, 48 | MaxAge: int(cfg.MaxAge.Seconds()), 49 | }) 50 | if l == nil || !cfg.Debug { 51 | return c 52 | } 53 | 54 | r, w := io.Pipe() 55 | c.Log = log.New(w, "", log.LstdFlags) 56 | go writeLog(r, l) 57 | 58 | return c 59 | } 60 | 61 | func writeLog(r *io.PipeReader, l logging.Logger) { 62 | scanner := bufio.NewScanner(r) 63 | scanner.Split(bufio.ScanLines) 64 | 65 | for scanner.Scan() { 66 | l.Debug("[CORS]", scanner.Text()) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /cors.go: -------------------------------------------------------------------------------- 1 | package cors 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/luraproject/lura/v2/config" 7 | ) 8 | 9 | // Namespace is the key to look for extra configuration details 10 | const Namespace = "github_com/devopsfaith/krakend-cors" 11 | 12 | // Config holds the configuration of CORS 13 | type Config struct { 14 | AllowOrigins []string 15 | AllowMethods []string 16 | AllowHeaders []string 17 | ExposeHeaders []string 18 | AllowCredentials bool 19 | AllowPrivateNetwork bool 20 | OptionsPassthrough bool 21 | OptionsSuccessStatus int 22 | MaxAge time.Duration 23 | Debug bool 24 | } 25 | 26 | // ConfigGetter implements the config.ConfigGetter interface. It parses the extra config an allowed 27 | // origin must be defined, the rest of the options will use a default if not defined. 28 | func ConfigGetter(e config.ExtraConfig) interface{} { 29 | v, ok := e[Namespace] 30 | if !ok { 31 | return nil 32 | } 33 | 34 | tmp, ok := v.(map[string]interface{}) 35 | if !ok { 36 | return nil 37 | } 38 | 39 | cfg := Config{} 40 | cfg.AllowOrigins = getList(tmp, "allow_origins") 41 | cfg.AllowMethods = getList(tmp, "allow_methods") 42 | cfg.AllowHeaders = getList(tmp, "allow_headers") 43 | cfg.ExposeHeaders = getList(tmp, "expose_headers") 44 | 45 | if allowCredentials, ok := tmp["allow_credentials"]; ok { 46 | if v, ok := allowCredentials.(bool); ok { 47 | cfg.AllowCredentials = v 48 | } 49 | } 50 | 51 | if debug, ok := tmp["debug"]; ok { 52 | v, ok := debug.(bool) 53 | cfg.Debug = ok && v 54 | } 55 | 56 | if allowPrivateNetwork, ok := tmp["allow_private_network"]; ok { 57 | v, ok := allowPrivateNetwork.(bool) 58 | cfg.AllowPrivateNetwork = ok && v 59 | } 60 | 61 | if optionsPassthrough, ok := tmp["options_passthrough"]; ok { 62 | v, ok := optionsPassthrough.(bool) 63 | cfg.OptionsPassthrough = ok && v 64 | } 65 | 66 | if optionsSuccessStatus, ok := tmp["options_success_status"]; ok { 67 | if v, ok := optionsSuccessStatus.(float64); ok { 68 | cfg.OptionsSuccessStatus = int(v) 69 | } 70 | } 71 | 72 | if maxAge, ok := tmp["max_age"]; ok { 73 | if d, err := time.ParseDuration(maxAge.(string)); err == nil { 74 | cfg.MaxAge = d 75 | } 76 | } 77 | return cfg 78 | } 79 | 80 | func getList(data map[string]interface{}, name string) []string { 81 | var out []string 82 | if vs, ok := data[name]; ok { 83 | if v, ok := vs.([]interface{}); ok { 84 | for _, s := range v { 85 | if j, ok := s.(string); ok { 86 | out = append(out, j) 87 | } 88 | } 89 | } 90 | } 91 | return out 92 | } 93 | -------------------------------------------------------------------------------- /gin/cors.go: -------------------------------------------------------------------------------- 1 | package gin 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | krakendcors "github.com/krakend/krakend-cors/v2" 9 | "github.com/krakend/krakend-cors/v2/mux" 10 | "github.com/luraproject/lura/v2/config" 11 | "github.com/luraproject/lura/v2/logging" 12 | "github.com/rs/cors" 13 | wrapper "github.com/rs/cors/wrapper/gin" 14 | ) 15 | 16 | // New returns a gin.HandlerFunc with the CORS configuration provided in the ExtraConfig 17 | func New(e config.ExtraConfig) gin.HandlerFunc { 18 | tmp := krakendcors.ConfigGetter(e) 19 | if tmp == nil { 20 | return nil 21 | } 22 | cfg, ok := tmp.(krakendcors.Config) 23 | if !ok { 24 | return nil 25 | } 26 | 27 | if len(cfg.AllowOrigins) == 0 { 28 | cfg.AllowOrigins = []string{"*"} 29 | } 30 | if len(cfg.AllowHeaders) == 0 { 31 | cfg.AllowHeaders = []string{"*"} 32 | } 33 | // Maintain the old default value to not change behaviour 34 | // the rs/cors new default is to return a 204 35 | if cfg.OptionsSuccessStatus == 0 { 36 | cfg.OptionsSuccessStatus = 200 37 | } 38 | 39 | return wrapper.New(cors.Options{ 40 | AllowedOrigins: cfg.AllowOrigins, 41 | AllowedMethods: cfg.AllowMethods, 42 | AllowedHeaders: cfg.AllowHeaders, 43 | ExposedHeaders: cfg.ExposeHeaders, 44 | AllowCredentials: cfg.AllowCredentials, 45 | AllowPrivateNetwork: cfg.AllowPrivateNetwork, 46 | OptionsPassthrough: cfg.OptionsPassthrough, 47 | OptionsSuccessStatus: cfg.OptionsSuccessStatus, 48 | MaxAge: int(cfg.MaxAge.Seconds()), 49 | Debug: cfg.Debug, 50 | }) 51 | } 52 | 53 | // RunServer defines the interface of a function used by the KrakenD router to start the service 54 | type RunServer func(context.Context, config.ServiceConfig, http.Handler) error 55 | 56 | // NewRunServer returns a RunServer wrapping the injected one with a CORS middleware, so it is called before the 57 | // actual router checks the URL, method and other details related to selecting the proper handler for the 58 | // incoming request 59 | func NewRunServer(next RunServer) RunServer { 60 | return NewRunServerWithLogger(next, nil) 61 | } 62 | 63 | // NewRunServerWithLogger returns a RunServer wrapping the injected one with a CORS middleware, so it is called before the 64 | // actual router checks the URL, method and other details related to selecting the proper handler for the 65 | // incoming request 66 | func NewRunServerWithLogger(next RunServer, l logging.Logger) RunServer { 67 | if l == nil { 68 | l = logging.NoOp 69 | } 70 | return func(ctx context.Context, cfg config.ServiceConfig, handler http.Handler) error { 71 | corsMw := mux.NewWithLogger(cfg.ExtraConfig, l) 72 | if corsMw == nil { 73 | return next(ctx, cfg, handler) 74 | } 75 | l.Debug("[SERVICE: Gin][CORS] Enabled CORS for all requests") 76 | return next(ctx, cfg, corsMw.Handler(handler)) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /cors_test.go: -------------------------------------------------------------------------------- 1 | package cors 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestConfigGetter(t *testing.T) { 10 | sampleCfg := map[string]interface{}{} 11 | serialized := []byte(`{ "github_com/devopsfaith/krakend-cors": { 12 | "allow_origins": [ "http://localhost", "http://www.example.com" ], 13 | "allow_headers": [ "X-Test", "Content-Type"], 14 | "allow_methods": [ "POST", "GET" ], 15 | "expose_headers": [ "Content-Type" ], 16 | "allow_credentials": false, 17 | "max_age": "24h" 18 | } 19 | }`) 20 | json.Unmarshal(serialized, &sampleCfg) 21 | testCfg := ConfigGetter(sampleCfg).(Config) 22 | 23 | if len(testCfg.AllowOrigins) != 2 { 24 | t.Error("Should have exactly 2 allowed origins.\n") 25 | } 26 | for i, v := range []string{"http://localhost", "http://www.example.com"} { 27 | if testCfg.AllowOrigins[i] != v { 28 | t.Errorf("Invalid value %s should be %s\n", testCfg.AllowOrigins[i], v) 29 | } 30 | } 31 | if len(testCfg.AllowHeaders) != 2 { 32 | t.Error("Should have exactly 2 allowed headers.\n") 33 | } 34 | for i, v := range []string{"X-Test", "Content-Type"} { 35 | if testCfg.AllowHeaders[i] != v { 36 | t.Errorf("Invalid value %s should be %s\n", testCfg.AllowHeaders[i], v) 37 | } 38 | } 39 | if len(testCfg.AllowMethods) != 2 { 40 | t.Error("Should have exactly 2 allowed headers.\n") 41 | } 42 | for i, v := range []string{"POST", "GET"} { 43 | if testCfg.AllowMethods[i] != v { 44 | t.Errorf("Invalid value %s should be %s\n", testCfg.AllowMethods[i], v) 45 | } 46 | } 47 | if len(testCfg.ExposeHeaders) != 1 { 48 | t.Error("Should have exactly 2 allowed headers.\n") 49 | } 50 | for i, v := range []string{"Content-Type"} { 51 | if testCfg.ExposeHeaders[i] != v { 52 | t.Errorf("Invalid value %s should be %s\n", testCfg.ExposeHeaders[i], v) 53 | } 54 | } 55 | if testCfg.AllowCredentials { 56 | t.Error("Allow Credentials should be disabled.\n") 57 | } 58 | 59 | if testCfg.MaxAge != 24*time.Hour { 60 | t.Errorf("Unexpected collection time: %v\n", testCfg.MaxAge) 61 | } 62 | } 63 | 64 | func TestDefaultConfiguration(t *testing.T) { 65 | sampleCfg := map[string]interface{}{} 66 | serialized := []byte(`{ "github_com/devopsfaith/krakend-cors": { 67 | "allow_origins": [ "http://www.example.com" ] 68 | }}`) 69 | json.Unmarshal(serialized, &sampleCfg) 70 | defaultCfg := ConfigGetter(sampleCfg).(Config) 71 | if defaultCfg.AllowOrigins[0] != "http://www.example.com" { 72 | t.Error("Wrong AllowOrigin.\n") 73 | } 74 | } 75 | 76 | func TestWrongConfiguration(t *testing.T) { 77 | sampleCfg := map[string]interface{}{} 78 | if _, ok := ConfigGetter(sampleCfg).(Config); ok { 79 | t.Error("The config should be nil\n") 80 | } 81 | badCfg := map[string]interface{}{Namespace: "test"} 82 | if _, ok := ConfigGetter(badCfg).(Config); ok { 83 | t.Error("The config should be nil\n") 84 | } 85 | } 86 | 87 | func TestEmptyConfiguration(t *testing.T) { 88 | noOriginCfg := map[string]interface{}{} 89 | serialized := []byte(`{ "github_com/devopsfaith/krakend-cors": { 90 | } 91 | }`) 92 | json.Unmarshal(serialized, &noOriginCfg) 93 | if v, ok := ConfigGetter(noOriginCfg).(Config); !ok { 94 | t.Errorf("The configuration should not be empty: %v\n", v) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /mux/cors_test.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/luraproject/lura/v2/logging" 12 | ) 13 | 14 | func TestInvalidCfg(t *testing.T) { 15 | sampleCfg := map[string]interface{}{} 16 | corsMw := New(sampleCfg) 17 | if corsMw != nil { 18 | t.Error("The corsMw should be nil.\n") 19 | } 20 | } 21 | 22 | func TestNew(t *testing.T) { 23 | sampleCfg := map[string]interface{}{} 24 | serialized := []byte(`{ "github_com/devopsfaith/krakend-cors": { 25 | "allow_origins": [ "http://foobar.com" ], 26 | "allow_headers": [ "Origin" ], 27 | "allow_methods": [ "GET" ], 28 | "max_age": "2h" 29 | } 30 | }`) 31 | if err := json.Unmarshal(serialized, &sampleCfg); err != nil { 32 | t.Error(err) 33 | return 34 | } 35 | h := New(sampleCfg) 36 | res := httptest.NewRecorder() 37 | req, _ := http.NewRequest("OPTIONS", "https://example.com/foo", http.NoBody) 38 | req.Header.Add("Origin", "http://foobar.com") 39 | req.Header.Add("Access-Control-Request-Method", "GET") 40 | req.Header.Add("Access-Control-Request-Headers", "origin") 41 | handler := h.Handler(testHandler) 42 | handler.ServeHTTP(res, req) 43 | 44 | assertHeaders(t, res.Header(), map[string]string{ 45 | "Vary": "Origin, Access-Control-Request-Method, Access-Control-Request-Headers", 46 | "Access-Control-Allow-Origin": "http://foobar.com", 47 | "Access-Control-Allow-Methods": "GET", 48 | "Access-Control-Allow-Headers": "origin", 49 | "Access-Control-Max-Age": "7200", 50 | }) 51 | } 52 | 53 | func TestNewWithLogger(t *testing.T) { 54 | buf := bytes.NewBuffer(nil) 55 | logger, err := logging.NewLogger("DEBUG", buf, "") 56 | if err != nil { 57 | t.Error(err) 58 | return 59 | } 60 | sampleCfg := map[string]interface{}{} 61 | serialized := []byte(`{ "github_com/devopsfaith/krakend-cors": { 62 | "allow_origins": [ "http://foobar.com" ], 63 | "allow_methods": [ "GET" ], 64 | "max_age": "2h" 65 | } 66 | }`) 67 | if err := json.Unmarshal(serialized, &sampleCfg); err != nil { 68 | t.Error(err) 69 | return 70 | } 71 | h := NewWithLogger(sampleCfg, logger) 72 | res := httptest.NewRecorder() 73 | req, _ := http.NewRequest("OPTIONS", "https://example.com/foo", http.NoBody) 74 | req.Header.Add("Origin", "http://foobar.com") 75 | handler := h.Handler(testHandler) 76 | handler.ServeHTTP(res, req) 77 | if res.Code != http.StatusOK { 78 | t.Errorf("Invalid status code: %d should be 200", res.Code) 79 | } 80 | 81 | assertHeaders(t, res.Header(), map[string]string{ 82 | "Vary": "Origin", 83 | "Access-Control-Allow-Origin": "http://foobar.com", 84 | "Access-Control-Allow-Methods": "", 85 | "Access-Control-Allow-Headers": "", 86 | "Access-Control-Max-Age": "", 87 | }) 88 | 89 | loggedMsg := buf.String() 90 | if loggedMsg != "" { 91 | t.Error("unexpected logged msg:", loggedMsg) 92 | } 93 | } 94 | 95 | func TestAllowOriginEmpty(t *testing.T) { 96 | sampleCfg := map[string]interface{}{} 97 | serialized := []byte(`{ "github_com/devopsfaith/krakend-cors": { 98 | } 99 | }`) 100 | if err := json.Unmarshal(serialized, &sampleCfg); err != nil { 101 | t.Error(err) 102 | return 103 | } 104 | h := New(sampleCfg) 105 | res := httptest.NewRecorder() 106 | req, _ := http.NewRequest("OPTIONS", "https://example.com/foo", http.NoBody) 107 | req.Header.Add("Access-Control-Request-Method", "GET") 108 | req.Header.Add("Access-Control-Request-Headers", "origin") 109 | req.Header.Add("Origin", "http://foobar.com") 110 | handler := h.Handler(testHandler) 111 | handler.ServeHTTP(res, req) 112 | if res.Code != http.StatusNoContent { 113 | t.Errorf("Invalid status code: %d should be 204", res.Code) 114 | } 115 | 116 | assertHeaders(t, res.Header(), map[string]string{ 117 | "Vary": "Origin, Access-Control-Request-Method, Access-Control-Request-Headers", 118 | "Access-Control-Allow-Origin": "*", 119 | "Access-Control-Allow-Methods": "GET", 120 | "Access-Control-Allow-Headers": "origin", 121 | }) 122 | } 123 | 124 | func TestOptionsSuccess(t *testing.T) { 125 | sampleCfg := map[string]interface{}{} 126 | serialized := []byte(`{ "github_com/devopsfaith/krakend-cors": { 127 | "options_success_status": 200 128 | } 129 | }`) 130 | if err := json.Unmarshal(serialized, &sampleCfg); err != nil { 131 | t.Error(err) 132 | return 133 | } 134 | h := New(sampleCfg) 135 | res := httptest.NewRecorder() 136 | req, _ := http.NewRequest("OPTIONS", "https://example.com/foo", http.NoBody) 137 | req.Header.Add("Access-Control-Request-Method", "GET") 138 | req.Header.Add("Access-Control-Request-Headers", "origin") 139 | req.Header.Add("Origin", "http://foobar.com") 140 | handler := h.Handler(testHandler) 141 | handler.ServeHTTP(res, req) 142 | if res.Code != http.StatusOK { 143 | t.Errorf("Invalid status code: %d should be 200", res.Code) 144 | } 145 | 146 | assertHeaders(t, res.Header(), map[string]string{ 147 | "Vary": "Origin, Access-Control-Request-Method, Access-Control-Request-Headers", 148 | "Access-Control-Allow-Origin": "*", 149 | "Access-Control-Allow-Methods": "GET", 150 | "Access-Control-Allow-Headers": "origin", 151 | }) 152 | } 153 | 154 | func TestAllowPrivateNetwork(t *testing.T) { 155 | sampleCfg := map[string]interface{}{} 156 | serialized := []byte(`{ "github_com/devopsfaith/krakend-cors": { 157 | "allow_private_network": true 158 | } 159 | }`) 160 | if err := json.Unmarshal(serialized, &sampleCfg); err != nil { 161 | t.Error(err) 162 | return 163 | } 164 | h := New(sampleCfg) 165 | res := httptest.NewRecorder() 166 | req, _ := http.NewRequest("OPTIONS", "https://example.com/foo", http.NoBody) 167 | req.Header.Add("Access-Control-Request-Method", "GET") 168 | req.Header.Add("Access-Control-Request-Private-Network", "true") 169 | req.Header.Add("Origin", "http://foobar.com") 170 | handler := h.Handler(testHandler) 171 | handler.ServeHTTP(res, req) 172 | if res.Code != http.StatusNoContent { 173 | t.Errorf("Invalid status code: %d should be 204", res.Code) 174 | } 175 | 176 | assertHeaders(t, res.Header(), map[string]string{ 177 | "Vary": "Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Request-Private-Network", 178 | "Access-Control-Allow-Origin": "*", 179 | "Access-Control-Allow-Methods": "GET", 180 | "Access-Control-Allow-Private-Network": "true", 181 | }) 182 | } 183 | 184 | func TestOptionPasstrough(t *testing.T) { 185 | sampleCfg := map[string]interface{}{} 186 | serialized := []byte(`{ "github_com/devopsfaith/krakend-cors": { 187 | "options_passthrough": true 188 | } 189 | }`) 190 | 191 | if err := json.Unmarshal(serialized, &sampleCfg); err != nil { 192 | t.Error(err) 193 | return 194 | } 195 | 196 | h := New(sampleCfg) 197 | res := httptest.NewRecorder() 198 | req, _ := http.NewRequest("OPTIONS", "https://example.com/foo", http.NoBody) 199 | req.Header.Add("Access-Control-Request-Method", "GET") 200 | req.Header.Add("Origin", "http://foobar.com") 201 | handler := h.Handler(testHandler) 202 | handler.ServeHTTP(res, req) 203 | if res.Code != http.StatusOK { 204 | t.Errorf("Invalid status code: %d should be 200", res.Code) 205 | } 206 | 207 | assertHeaders(t, res.Header(), map[string]string{ 208 | "Vary": "Origin, Access-Control-Request-Method, Access-Control-Request-Headers", 209 | "Access-Control-Allow-Origin": "*", 210 | "Access-Control-Allow-Methods": "GET", 211 | }) 212 | } 213 | 214 | var allHeaders = []string{ 215 | "Vary", 216 | "Access-Control-Allow-Origin", 217 | "Access-Control-Allow-Methods", 218 | "Access-Control-Allow-Headers", 219 | "Access-Control-Allow-Credentials", 220 | "Access-Control-Max-Age", 221 | "Access-Control-Expose-Headers", 222 | } 223 | 224 | func assertHeaders(t *testing.T, resHeaders http.Header, expHeaders map[string]string) { 225 | for _, name := range allHeaders { 226 | got := strings.Join(resHeaders[name], ", ") 227 | want := expHeaders[name] 228 | if got != want { 229 | t.Errorf("Response header %q = %q, want %q", name, got, want) 230 | } 231 | } 232 | } 233 | 234 | var testHandler = http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 235 | w.Write([]byte("bar")) 236 | }) 237 | -------------------------------------------------------------------------------- /gin/cors_test.go: -------------------------------------------------------------------------------- 1 | package gin 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "net/http" 9 | "net/http/httptest" 10 | "regexp" 11 | "strings" 12 | "testing" 13 | 14 | "github.com/gin-gonic/gin" 15 | "github.com/luraproject/lura/v2/config" 16 | "github.com/luraproject/lura/v2/logging" 17 | ) 18 | 19 | func TestInvalidCfg(t *testing.T) { 20 | sampleCfg := map[string]interface{}{} 21 | corsMw := New(sampleCfg) 22 | if corsMw != nil { 23 | t.Error("The corsMw should be nil.\n") 24 | } 25 | } 26 | 27 | func TestNew(t *testing.T) { 28 | sampleCfg := map[string]interface{}{} 29 | serialized := []byte(`{ "github_com/devopsfaith/krakend-cors": { 30 | "allow_origins": [ "http://foobar.com" ], 31 | "allow_methods": [ "GET" ], 32 | "max_age": "2h" 33 | } 34 | }`) 35 | json.Unmarshal(serialized, &sampleCfg) 36 | gin.SetMode(gin.TestMode) 37 | e := gin.New() 38 | corsMw := New(sampleCfg) 39 | if corsMw == nil { 40 | t.Error("The cors middleware should not be nil.\n") 41 | } 42 | e.Use(corsMw) 43 | e.GET("/foo", func(c *gin.Context) { c.String(200, "Yeah") }) 44 | res := httptest.NewRecorder() 45 | req, _ := http.NewRequest("OPTIONS", "https://example.com/foo", http.NoBody) 46 | req.Header.Add("Origin", "http://foobar.com") 47 | req.Header.Add("Access-Control-Request-Method", "GET") 48 | req.Header.Add("Access-Control-Request-Headers", "origin") 49 | e.ServeHTTP(res, req) 50 | if res.Code != 200 { 51 | t.Errorf("Invalid status code: %d should be 200", res.Code) 52 | } 53 | 54 | assertHeaders(t, res.Header(), map[string]string{ 55 | "Vary": "Origin, Access-Control-Request-Method, Access-Control-Request-Headers", 56 | "Access-Control-Allow-Origin": "http://foobar.com", 57 | "Access-Control-Allow-Methods": "GET", 58 | "Access-Control-Allow-Headers": "origin", 59 | "Access-Control-Max-Age": "7200", 60 | }) 61 | } 62 | 63 | func TestAllowOriginWildcard(t *testing.T) { 64 | sampleCfg := map[string]interface{}{} 65 | serialized := []byte(`{ "github_com/devopsfaith/krakend-cors": { 66 | "allow_origins": [ "*" ] 67 | } 68 | }`) 69 | json.Unmarshal(serialized, &sampleCfg) 70 | gin.SetMode(gin.TestMode) 71 | e := gin.New() 72 | corsMw := New(sampleCfg) 73 | if corsMw == nil { 74 | t.Error("The cors middleware should not be nil.\n") 75 | } 76 | e.Use(corsMw) 77 | e.GET("/wildcard", func(c *gin.Context) { c.String(200, "Yeah") }) 78 | res := httptest.NewRecorder() 79 | req, _ := http.NewRequest("OPTIONS", "https://example.com/wildcard", http.NoBody) 80 | req.Header.Add("Origin", "http://foobar.com") 81 | req.Header.Add("Access-Control-Request-Method", "GET") 82 | req.Header.Add("Access-Control-Request-Headers", "origin") 83 | e.ServeHTTP(res, req) 84 | if res.Code != 200 { 85 | t.Errorf("Invalid status code: %d should be 200", res.Code) 86 | } 87 | 88 | assertHeaders(t, res.Header(), map[string]string{ 89 | "Vary": "Origin, Access-Control-Request-Method, Access-Control-Request-Headers", 90 | "Access-Control-Allow-Origin": "*", 91 | "Access-Control-Allow-Methods": "GET", 92 | "Access-Control-Allow-Headers": "origin", 93 | }) 94 | } 95 | 96 | func TestAllowOriginEmpty(t *testing.T) { 97 | sampleCfg := map[string]interface{}{} 98 | serialized := []byte(`{ "github_com/devopsfaith/krakend-cors": { 99 | } 100 | }`) 101 | json.Unmarshal(serialized, &sampleCfg) 102 | gin.SetMode(gin.TestMode) 103 | e := gin.New() 104 | corsMw := New(sampleCfg) 105 | if corsMw == nil { 106 | t.Error("The cors middleware should not be nil.\n") 107 | } 108 | e.Use(corsMw) 109 | e.GET("/foo", func(c *gin.Context) { c.String(200, "Yeah") }) 110 | res := httptest.NewRecorder() 111 | req, _ := http.NewRequest("OPTIONS", "https://example.com/foo", http.NoBody) 112 | req.Header.Add("Origin", "http://foobar.com") 113 | req.Header.Add("Access-Control-Request-Method", "GET") 114 | req.Header.Add("Access-Control-Request-Headers", "origin") 115 | e.ServeHTTP(res, req) 116 | if res.Code != 200 { 117 | t.Errorf("Invalid status code: %d should be 200", res.Code) 118 | } 119 | 120 | assertHeaders(t, res.Header(), map[string]string{ 121 | "Vary": "Origin, Access-Control-Request-Method, Access-Control-Request-Headers", 122 | "Access-Control-Allow-Origin": "*", 123 | "Access-Control-Allow-Methods": "GET", 124 | "Access-Control-Allow-Headers": "origin", 125 | }) 126 | } 127 | 128 | func ExampleNewRunServerWithLogger() { 129 | var localHandler http.Handler 130 | next := func(_ context.Context, _ config.ServiceConfig, handler http.Handler) error { 131 | localHandler = handler 132 | return nil 133 | } 134 | 135 | buf := bytes.NewBuffer(nil) 136 | l, _ := logging.NewLogger("DEBUG", buf, "") 137 | corsRunServer := NewRunServerWithLogger(next, l) 138 | 139 | sampleCfg := map[string]interface{}{} 140 | serialized := []byte(`{ "github_com/devopsfaith/krakend-cors": { 141 | "allow_origins": [ "http://foobar.com" ], 142 | "allow_methods": [ "GET" ], 143 | "max_age": "2h", 144 | "debug": true 145 | } 146 | }`) 147 | json.Unmarshal(serialized, &sampleCfg) 148 | cfg := config.ServiceConfig{ExtraConfig: sampleCfg} 149 | 150 | mux := http.NewServeMux() 151 | mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) { 152 | w.Write([]byte("Yeah")) 153 | }) 154 | 155 | if err := corsRunServer(context.Background(), cfg, mux); err != nil { 156 | fmt.Println(err) 157 | return 158 | } 159 | 160 | res := httptest.NewRecorder() 161 | req, _ := http.NewRequest("OPTIONS", "http://example.com/", nil) 162 | req.Header.Add("Origin", "http://foobar.com") 163 | req.Header.Add("Access-Control-Request-Method", "GET") 164 | req.Header.Add("Access-Control-Request-Headers", "origin") 165 | localHandler.ServeHTTP(res, req) 166 | fmt.Println(res.Code) 167 | 168 | b, _ := json.MarshalIndent(res.Header(), "", "\t") 169 | fmt.Println(string(b)) 170 | 171 | fmt.Println("'" + res.Body.String() + "'") 172 | 173 | res = httptest.NewRecorder() 174 | req, _ = http.NewRequest("GET", "http://example.com/", nil) 175 | req.Header.Add("Origin", "http://foobar.com") 176 | req.Header.Add("Access-Control-Request-Method", "GET") 177 | req.Header.Add("Access-Control-Request-Headers", "origin") 178 | localHandler.ServeHTTP(res, req) 179 | fmt.Println(res.Code) 180 | 181 | b, _ = json.MarshalIndent(res.Header(), "", "\t") 182 | fmt.Println(string(b)) 183 | 184 | fmt.Println("'" + res.Body.String() + "'") 185 | 186 | re := regexp.MustCompile(`(\d\d\d\d\/\d\d\/\d\d \d\d:\d\d:\d\d\s+)`) 187 | fmt.Println(re.ReplaceAllString(buf.String(), "")) 188 | 189 | // output: 190 | // 204 191 | // { 192 | // "Access-Control-Allow-Headers": [ 193 | // "origin" 194 | // ], 195 | // "Access-Control-Allow-Methods": [ 196 | // "GET" 197 | // ], 198 | // "Access-Control-Allow-Origin": [ 199 | // "http://foobar.com" 200 | // ], 201 | // "Access-Control-Max-Age": [ 202 | // "7200" 203 | // ], 204 | // "Vary": [ 205 | // "Origin, Access-Control-Request-Method, Access-Control-Request-Headers" 206 | // ] 207 | // } 208 | // '' 209 | // 200 210 | // { 211 | // "Access-Control-Allow-Origin": [ 212 | // "http://foobar.com" 213 | // ], 214 | // "Content-Type": [ 215 | // "text/plain; charset=utf-8" 216 | // ], 217 | // "Vary": [ 218 | // "Origin" 219 | // ] 220 | // } 221 | // 'Yeah' 222 | // DEBUG: [SERVICE: Gin][CORS] Enabled CORS for all requests 223 | // DEBUG: [CORS] Handler: Preflight request 224 | // DEBUG: [CORS] Preflight response headers: map[Access-Control-Allow-Headers:[origin] Access-Control-Allow-Methods:[GET] Access-Control-Allow-Origin:[http://foobar.com] Access-Control-Max-Age:[7200] Vary:[Origin, Access-Control-Request-Method, Access-Control-Request-Headers]] 225 | // DEBUG: [CORS] Handler: Actual request 226 | // DEBUG: [CORS] Actual response added headers: map[Access-Control-Allow-Origin:[http://foobar.com] Vary:[Origin]] 227 | } 228 | 229 | var allHeaders = []string{ 230 | "Vary", 231 | "Access-Control-Allow-Origin", 232 | "Access-Control-Allow-Methods", 233 | "Access-Control-Allow-Headers", 234 | "Access-Control-Allow-Credentials", 235 | "Access-Control-Max-Age", 236 | "Access-Control-Expose-Headers", 237 | } 238 | 239 | func assertHeaders(t *testing.T, resHeaders http.Header, expHeaders map[string]string) { 240 | for _, name := range allHeaders { 241 | got := strings.Join(resHeaders[name], ", ") 242 | want := expHeaders[name] 243 | if got != want { 244 | t.Errorf("Response header %q = %q, want %q", name, got, want) 245 | } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/sonic v1.12.5 h1:hoZxY8uW+mT+OpkcUWw4k0fDINtOcVavEsGfzwzFU/w= 2 | github.com/bytedance/sonic v1.12.5/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= 3 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 4 | github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= 5 | github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 6 | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= 7 | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 8 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= 9 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 10 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA= 15 | github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU= 16 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 17 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 18 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= 19 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= 20 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 21 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 22 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 23 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 24 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 25 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 26 | github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= 27 | github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 28 | github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= 29 | github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 30 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 31 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 32 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 33 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 34 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 35 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 36 | github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= 37 | github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= 38 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 39 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 40 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 41 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 42 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 43 | github.com/krakend/flatmap v1.2.0 h1:4NPncAKH7Ca/t878kbGlc/LPWLa+m4sgBhs8aT2Q1SY= 44 | github.com/krakend/flatmap v1.2.0/go.mod h1:FyCOoggdVlWr31+aQaOFvBxlMgYfCE5yuwInLbW1/jM= 45 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 46 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 47 | github.com/luraproject/lura/v2 v2.11.0 h1:8VxEZSMOG1qSX+7YciB860m+YQ0Rbkc/510MGqJiuPk= 48 | github.com/luraproject/lura/v2 v2.11.0/go.mod h1:ZUBYMsjVwPAZWVEoBtJ6keodn13tgw/5QQyY3nDkk2I= 49 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 50 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 51 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 52 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 53 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 54 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 55 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 56 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 57 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 58 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 59 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 60 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 61 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 62 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 63 | github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= 64 | github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= 65 | github.com/rs/cors/wrapper/gin v0.0.0-20240830163046-1084d89a1692 h1:lwzJgPw5Y6pvC8mwbedX9HfdywUKcpNdcviftZsb1uY= 66 | github.com/rs/cors/wrapper/gin v0.0.0-20240830163046-1084d89a1692/go.mod h1:742Ialb8SOs5yB2PqRDzFcyND3280PoaS5/wcKQUQKE= 67 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 68 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 69 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 70 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 71 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 72 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 73 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 74 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 75 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 76 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 77 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 78 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 79 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 80 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 81 | github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8= 82 | github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= 83 | golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc= 84 | golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= 85 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= 86 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 87 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 88 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 89 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 90 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 91 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 92 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 93 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 94 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 95 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 96 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 97 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 98 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 99 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 100 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 101 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 102 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 103 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------