├── flagr
├── flagr-config.png
├── local-vs-remote.png
├── go.mod
├── converter.go
├── evaluator
│ ├── local.go
│ └── remote.go
├── caddyfile.go
├── README.md
└── flagr.go
├── layer4
├── go.mod
├── README.md
└── caddyfile.go
├── ratelimit
├── go.mod
├── zone.go
├── zone_test.go
├── caddyfile.go
├── README.md
├── ratelimit_test.go
└── ratelimit.go
├── README.md
├── requestbodyvar
├── go.mod
├── README.md
├── querier_test.go
├── querier.go
└── requestbodyvar.go
├── dynamichandler
├── yaegisymbols
│ ├── symbols.go
│ └── github_com-RussellLuo-caddy-ext-dynamichandler-caddymiddleware.go
├── caddymiddleware
│ └── caddymiddleware.go
├── dynamichandler_test.go
├── plugins
│ └── visitorip
│ │ └── visitorip.go
├── caddyfile.go
├── README.md
├── dynamichandler.go
└── go.mod
└── LICENSE
/flagr/flagr-config.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RussellLuo/caddy-ext/HEAD/flagr/flagr-config.png
--------------------------------------------------------------------------------
/flagr/local-vs-remote.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RussellLuo/caddy-ext/HEAD/flagr/local-vs-remote.png
--------------------------------------------------------------------------------
/layer4/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/RussellLuo/caddy-ext/layer4
2 |
3 | go 1.14
4 |
5 | require (
6 | github.com/caddyserver/caddy/v2 v2.4.6
7 | github.com/mholt/caddy-l4 v0.0.0-20220125094439-07bd718906ce
8 | )
9 |
--------------------------------------------------------------------------------
/ratelimit/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/RussellLuo/caddy-ext/ratelimit
2 |
3 | go 1.14
4 |
5 | require (
6 | github.com/RussellLuo/slidingwindow v0.0.0-20200528002341-535bb99d338b
7 | github.com/caddyserver/caddy/v2 v2.4.5
8 | github.com/hashicorp/golang-lru v0.5.1
9 | go.uber.org/zap v1.19.0
10 | )
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # caddy-ext
2 |
3 | Various Caddy v2 extensions (a.k.a. modules).
4 |
5 |
6 | ## Extensions
7 |
8 | - [dynamichandler](dynamichandler)
9 | - [flagr](flagr)
10 | - [layer4](layer4)
11 | - [ratelimit](ratelimit)
12 | - [requestbodyvar](requestbodyvar)
13 |
14 |
15 | ## License
16 |
17 | [MIT](LICENSE)
18 |
--------------------------------------------------------------------------------
/requestbodyvar/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/RussellLuo/caddy-ext/requestbodyvar
2 |
3 | go 1.14
4 |
5 | require (
6 | github.com/basgys/goxml2json v1.1.0
7 | github.com/bitly/go-simplejson v0.5.0 // indirect
8 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
9 | github.com/caddyserver/caddy/v2 v2.4.5
10 | github.com/tidwall/gjson v1.6.7
11 | go.uber.org/zap v1.19.0
12 | )
13 |
--------------------------------------------------------------------------------
/dynamichandler/yaegisymbols/symbols.go:
--------------------------------------------------------------------------------
1 | package yaegisymbols
2 |
3 | import (
4 | "reflect"
5 | )
6 |
7 | // Symbols stores the map of caddymiddleware package symbols.
8 | var Symbols = map[string]map[string]reflect.Value{}
9 |
10 | type Middleware = _github_com_RussellLuo_caddy_ext_dynamichandler_caddymiddleware_Middleware
11 |
12 | //go:generate yaegi extract github.com/RussellLuo/caddy-ext/dynamichandler/caddymiddleware
13 |
--------------------------------------------------------------------------------
/dynamichandler/caddymiddleware/caddymiddleware.go:
--------------------------------------------------------------------------------
1 | package caddymiddleware
2 |
3 | import (
4 | "net/http"
5 | )
6 |
7 | type Handler interface {
8 | ServeHTTP(http.ResponseWriter, *http.Request) error
9 | }
10 |
11 | type HandlerFunc func(http.ResponseWriter, *http.Request) error
12 |
13 | func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
14 | return f(w, r)
15 | }
16 |
17 | type Middleware interface {
18 | Provision() error
19 | Validate() error
20 | Cleanup() error
21 | ServeHTTP(http.ResponseWriter, *http.Request, Handler) error
22 | }
23 |
--------------------------------------------------------------------------------
/flagr/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/RussellLuo/caddy-ext/flagr
2 |
3 | go 1.14
4 |
5 | require (
6 | github.com/caddyserver/caddy/v2 v2.3.0
7 | github.com/checkr/flagr v0.0.0-20210503185549-d4b7b279061d
8 | github.com/checkr/goflagr v0.0.0-20210326170947-e97f63ad2c23
9 | github.com/golang/mock v1.6.0 // indirect
10 | github.com/onsi/ginkgo v1.16.4 // indirect
11 | github.com/onsi/gomega v1.13.0 // indirect
12 | go.uber.org/zap v1.17.0
13 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e // indirect
14 | golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect
15 | golang.org/x/oauth2 v0.0.0-20210615190721-d04028783cf1 // indirect
16 | golang.org/x/tools v0.1.3 // indirect
17 | )
18 |
--------------------------------------------------------------------------------
/requestbodyvar/README.md:
--------------------------------------------------------------------------------
1 | # requestbodyvar
2 |
3 | A Caddy v2 extension to add support for the `{http.request.body.*}` [placeholder][1] (variable).
4 |
5 |
6 | ## Installation
7 |
8 | ```bash
9 | $ xcaddy build --with github.com/RussellLuo/caddy-ext/requestbodyvar
10 | ```
11 |
12 | ## Example
13 |
14 | With the following Caddyfile:
15 |
16 | ```
17 | localhost:8080 {
18 | route / {
19 | request_body_var
20 |
21 | respond {http.request.body.name}
22 | }
23 | }
24 | ```
25 |
26 | You can get the responses as below:
27 |
28 | ```bash
29 | $ curl -XPOST https://localhost:8080 -d '{"name":"caddy"}'
30 | caddy
31 | $ curl -XPOST https://localhost:8080 -d '{"name":"wow"}'
32 | wow
33 | ```
34 |
35 |
36 | [1]: https://caddyserver.com/docs/conventions#placeholders
37 |
38 |
--------------------------------------------------------------------------------
/requestbodyvar/querier_test.go:
--------------------------------------------------------------------------------
1 | package requestbodyvar
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 | )
7 |
8 | func TestJSON_Query(t *testing.T) {
9 | j := JSON{buf: bytes.NewBufferString(`{"name":{"first":"Janet","last":"Prichard"},"age":47}`)}
10 | want := "Prichard"
11 | result := j.Query("name.last")
12 | if result != want {
13 | t.Fatalf("Result: got (%#v), want (%#v)", result, want)
14 | }
15 | }
16 |
17 | func TestXML_Query(t *testing.T) {
18 | x := XML{buf: bytes.NewBufferString(`
19 |
20 | Janet
21 | Prichard
22 |
23 | 47`)}
24 | want := "Prichard"
25 | result := x.Query("name.last")
26 | if result != want {
27 | t.Fatalf("Result: got (%#v), want (%#v)", result, want)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/dynamichandler/dynamichandler_test.go:
--------------------------------------------------------------------------------
1 | package dynamichandler
2 |
3 | import (
4 | "net/http"
5 | "net/http/httptest"
6 | "testing"
7 |
8 | "go.uber.org/zap"
9 |
10 | "github.com/RussellLuo/caddy-ext/dynamichandler/caddymiddleware"
11 | )
12 |
13 | func TestDynamicHandler_eval(t *testing.T) {
14 | dh := &DynamicHandler{
15 | Name: "visitorip",
16 | Root: "plugins",
17 | Config: `{"output": "stdout"}`,
18 | logger: zap.NewNop(),
19 | }
20 | if err := dh.provision(); err != nil {
21 | t.Fatal(err)
22 | }
23 |
24 | w := httptest.NewRecorder()
25 | r := httptest.NewRequest(http.MethodGet, "/", nil)
26 | next := caddymiddleware.HandlerFunc(func(http.ResponseWriter, *http.Request) error { return nil })
27 | if err := dh.middleware.ServeHTTP(w, r, next); err != nil {
28 | t.Fatal(err)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/flagr/converter.go:
--------------------------------------------------------------------------------
1 | package flagr
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | )
7 |
8 | type Converter func(string) (interface{}, error)
9 |
10 | // A set of converters for string values.
11 | var converters = map[string]Converter{
12 | "int": toInt,
13 | }
14 |
15 | func AddConverter(n string, c Converter) error {
16 | if _, ok := converters[n]; ok {
17 | return fmt.Errorf("converter name %q is reserved", n)
18 | }
19 | converters[n] = c
20 | return nil
21 | }
22 |
23 | func GetConverter(n string) (Converter, error) {
24 | c, ok := converters[n]
25 | if !ok {
26 | return nil, fmt.Errorf("converter name %q is not found", n)
27 | }
28 | return c, nil
29 | }
30 |
31 | func toInt(v string) (interface{}, error) {
32 | i, err := strconv.Atoi(v)
33 | if err != nil {
34 | return 0, err
35 | }
36 | return i, nil
37 | }
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Luo Peng
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/dynamichandler/plugins/visitorip/visitorip.go:
--------------------------------------------------------------------------------
1 | package visitorip
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "net/http"
7 | "os"
8 |
9 | "github.com/RussellLuo/caddy-ext/dynamichandler/caddymiddleware"
10 | )
11 |
12 | // Middleware implements an HTTP handler that writes the
13 | // visitor's IP address to a file or stream.
14 | type Middleware struct {
15 | // The file or stream to write to. Can be "stdout"
16 | // or "stderr".
17 | Output string `json:"output,omitempty"`
18 |
19 | w io.Writer
20 | }
21 |
22 | func New() caddymiddleware.Middleware {
23 | return &Middleware{}
24 | }
25 |
26 | func (m *Middleware) Provision() error {
27 | switch m.Output {
28 | case "stdout":
29 | m.w = os.Stdout
30 | case "stderr":
31 | m.w = os.Stderr
32 | default:
33 | return fmt.Errorf("an output stream is required")
34 | }
35 | return nil
36 | }
37 |
38 | func (m *Middleware) Validate() error {
39 | if m.w == nil {
40 | return fmt.Errorf("no writer")
41 | }
42 | return nil
43 | }
44 |
45 | func (m *Middleware) Cleanup() error { return nil }
46 |
47 | func (m *Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddymiddleware.Handler) error {
48 | _, _ = m.w.Write([]byte(r.RemoteAddr + "\n"))
49 | return next.ServeHTTP(w, r)
50 | }
51 |
--------------------------------------------------------------------------------
/requestbodyvar/querier.go:
--------------------------------------------------------------------------------
1 | package requestbodyvar
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "mime"
7 | "strings"
8 |
9 | "github.com/basgys/goxml2json"
10 | "github.com/tidwall/gjson"
11 | )
12 |
13 | type Querier interface {
14 | Query(string) string
15 | }
16 |
17 | type JSON struct {
18 | buf *bytes.Buffer
19 | }
20 |
21 | func (j JSON) Query(key string) string {
22 | return getJSONField(j.buf, key)
23 | }
24 |
25 | type XML struct {
26 | buf *bytes.Buffer
27 | }
28 |
29 | func (x XML) Query(key string) string {
30 | json, err := xml2json.Convert(x.buf)
31 | if err != nil {
32 | return ""
33 | }
34 | return getJSONField(json, key)
35 | }
36 |
37 | func newQuerier(buf *bytes.Buffer, contentType string) (Querier, error) {
38 | mediaType := "application/json"
39 | if contentType != "" {
40 | var err error
41 | mediaType, _, err = mime.ParseMediaType(contentType)
42 | if err != nil {
43 | return nil, err
44 | }
45 | }
46 |
47 | switch {
48 | case mediaType == "application/json":
49 | return JSON{buf: buf}, nil
50 | case strings.HasSuffix(mediaType, "/xml"):
51 | // application/xml
52 | // text/xml
53 | return XML{buf: buf}, nil
54 | default:
55 | return nil, fmt.Errorf("unsupported Media Type: %q", mediaType)
56 | }
57 | }
58 |
59 | // getJSONField gets the value of the given field from the JSON body,
60 | // which is buffered in buf.
61 | func getJSONField(buf *bytes.Buffer, key string) string {
62 | if buf == nil {
63 | return ""
64 | }
65 | value := gjson.GetBytes(buf.Bytes(), key)
66 | return value.String()
67 | }
68 |
--------------------------------------------------------------------------------
/flagr/evaluator/local.go:
--------------------------------------------------------------------------------
1 | package evaluator
2 |
3 | import (
4 | "context"
5 | "sync"
6 | "time"
7 |
8 | "github.com/checkr/flagr/pkg/config"
9 | "github.com/checkr/flagr/pkg/handler"
10 | "github.com/checkr/flagr/swagger_gen/models"
11 | "github.com/checkr/flagr/swagger_gen/restapi/operations/evaluation"
12 | )
13 |
14 | var (
15 | onceLocal sync.Once
16 | singletonLocal *Local
17 | )
18 |
19 | type Local struct {
20 | eval handler.Eval
21 | }
22 |
23 | func NewLocal(interval time.Duration, url string) (*Local, error) {
24 | var err error
25 | onceLocal.Do(func() {
26 | // Change the global configuration of Flagr once.
27 | config.Config.EvalLoggingEnabled = false
28 | config.Config.EvalCacheRefreshInterval = interval
29 | config.Config.EvalOnlyMode = true
30 | config.Config.DBDriver = "json_http"
31 | // The URL for exporting JSON format of the eval cache dump,
32 | // see https://checkr.github.io/flagr/api_docs/#operation/getExportEvalCacheJSON
33 | config.Config.DBConnectionStr = url + "/export/eval_cache/json"
34 |
35 | // Start the singleton eval cache once.
36 | defer func() {
37 | // EvalCache.Start() may panic if it fails.
38 | if r := recover(); r != nil {
39 | if e, ok := r.(error); ok {
40 | err = e
41 | }
42 | }
43 | }()
44 | handler.GetEvalCache().Start()
45 |
46 | singletonLocal = &Local{
47 | eval: handler.NewEval(),
48 | }
49 | })
50 | return singletonLocal, err
51 | }
52 |
53 | func (l *Local) PostEvaluationBatch(ctx context.Context, req *models.EvaluationBatchRequest) (*models.EvaluationBatchResponse, error) {
54 | resp := l.eval.PostEvaluationBatch(evaluation.PostEvaluationBatchParams{Body: req})
55 | ok := resp.(*evaluation.PostEvaluationBatchOK)
56 | return ok.Payload, nil
57 | }
58 |
--------------------------------------------------------------------------------
/ratelimit/zone.go:
--------------------------------------------------------------------------------
1 | package ratelimit
2 |
3 | import (
4 | "time"
5 | "fmt"
6 |
7 | sw "github.com/RussellLuo/slidingwindow"
8 | "github.com/hashicorp/golang-lru"
9 | )
10 |
11 | type Zone struct {
12 | limiters *lru.Cache
13 |
14 | rateSize time.Duration
15 | rateLimit int64
16 | }
17 |
18 | func NewZone(size int, rateSize time.Duration, rateLimit int64) (*Zone, error) {
19 | cache, err := lru.New(size)
20 | if err != nil {
21 | return nil, err
22 | }
23 | return &Zone{
24 | limiters: cache,
25 | rateSize: rateSize,
26 | rateLimit: rateLimit,
27 | }, nil
28 | }
29 |
30 | // Purge is used to completely clear the zone.
31 | func (z *Zone) Purge() {
32 | z.limiters.Purge()
33 | }
34 |
35 | func (z *Zone) Allow(key string) bool {
36 | lim, _, _ := z.getLimiter(key)
37 | return lim.Allow()
38 | }
39 |
40 | func (z *Zone) RateLimitPolicyHeader() string {
41 | return fmt.Sprintf("%d; w=%d", z.rateLimit, int(z.rateSize.Seconds()))
42 | }
43 |
44 | func (z *Zone) getLimiter(key string) (lim *sw.Limiter, ok, evict bool) {
45 | // If there is already a limiter for key, just return it.
46 | elem, ok := z.limiters.Peek(key)
47 | if ok {
48 | return elem.(*sw.Limiter), true, false
49 | }
50 |
51 | lim, _ = sw.NewLimiter(z.rateSize, z.rateLimit, func() (sw.Window, sw.StopFunc) {
52 | // NewLocalWindow returns an empty stop function, so it's
53 | // unnecessary to call it later.
54 | return sw.NewLocalWindow()
55 | })
56 | // Try to add lim as the limiter for key.
57 | ok, evict = z.limiters.ContainsOrAdd(key, lim)
58 |
59 | if ok {
60 | // The limiter for key has been added by someone else just now.
61 | // We should use the limiter rather than our lim.
62 | elem, _ = z.limiters.Peek(key)
63 | lim = elem.(*sw.Limiter)
64 | }
65 |
66 | return
67 | }
68 |
--------------------------------------------------------------------------------
/ratelimit/zone_test.go:
--------------------------------------------------------------------------------
1 | package ratelimit
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | sw "github.com/RussellLuo/slidingwindow"
8 | )
9 |
10 | func TestZone_getLimiter(t *testing.T) {
11 | zone, _ := NewZone(2, time.Second, 10)
12 |
13 | cases := []struct {
14 | key string
15 | ok bool
16 | evict bool
17 | }{
18 | {"key1", false, false},
19 | {"key1", true, false},
20 | {"key2", false, false},
21 | {"key3", false, true},
22 | }
23 |
24 | for _, c := range cases {
25 | lim, ok, evict := zone.getLimiter(c.key)
26 | if lim == nil {
27 | t.Fatalf("Limiter is nil")
28 | }
29 |
30 | if ok != c.ok {
31 | t.Fatalf("Found: got (%#v), want (%#v)", ok, c.ok)
32 | }
33 |
34 | if evict != c.evict {
35 | t.Fatalf("Evict: got (%#v), want (%#v)", evict, c.evict)
36 | }
37 | }
38 | }
39 |
40 | func TestZone_getLimiterConcurrently(t *testing.T) {
41 | test := func(n int) {
42 | zone, _ := NewZone(1, time.Second, 10)
43 | key := "key1"
44 |
45 | limC := make(chan *sw.Limiter, n)
46 | startC := make(chan struct{})
47 | for i := 0; i < n; i++ {
48 | go func() {
49 | <-startC
50 | lim, _, _ := zone.getLimiter(key)
51 | limC <- lim
52 | }()
53 | }
54 |
55 | // Send a START signal to all the goroutines.
56 | close(startC)
57 |
58 | var gotLims []*sw.Limiter
59 | for i := 0; i < n; i++ {
60 | // Collect all the result limiters.
61 | gotLims = append(gotLims, <-limC)
62 | }
63 |
64 | elem, ok := zone.limiters.Peek(key)
65 | if !ok {
66 | t.Fatalf("Found no limiter")
67 | }
68 | wantLim := elem.(*sw.Limiter)
69 |
70 | for _, lim := range gotLims {
71 | if lim != wantLim {
72 | t.Fatalf("Limiter: got (%#v), want (%#v)", lim, wantLim)
73 | }
74 | }
75 | }
76 |
77 | cases := []struct {
78 | name string
79 | degree int
80 | }{
81 | {"concurrency-degree-8", 8},
82 | {"concurrency-degree-32", 32},
83 | {"concurrency-degree-64", 64},
84 | }
85 |
86 | for _, c := range cases {
87 | t.Run(c.name, func(t *testing.T) {
88 | test(c.degree)
89 | })
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/ratelimit/caddyfile.go:
--------------------------------------------------------------------------------
1 | package ratelimit
2 |
3 | import (
4 | "strconv"
5 |
6 | "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
7 | "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
8 | "github.com/caddyserver/caddy/v2/modules/caddyhttp"
9 | )
10 |
11 | func init() {
12 | httpcaddyfile.RegisterHandlerDirective("rate_limit", parseCaddyfile)
13 | }
14 |
15 | // parseCaddyfile sets up a handler for rate-limiting from Caddyfile tokens. Syntax:
16 | //
17 | // rate_limit [] [ []]
18 | //
19 | // Parameters:
20 | // - : The variable used to differentiate one client from another.
21 | // - : The request rate limit (per key value) specified in requests per second (r/s) or requests per minute (r/m).
22 | // - : The size (i.e. the number of key values) of the LRU zone that keeps states of these key values. Defaults to 10,000.
23 | // - : The HTTP status code of the response when a client exceeds the rate. Defaults to 429 (Too Many Requests).
24 | func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
25 | rl := new(RateLimit)
26 | if err := rl.UnmarshalCaddyfile(h.Dispenser); err != nil {
27 | return nil, err
28 | }
29 | return rl, nil
30 | }
31 |
32 | func (rl *RateLimit) UnmarshalCaddyfile(d *caddyfile.Dispenser) (err error) {
33 | if d.Next() {
34 | args := d.RemainingArgs()
35 | switch len(args) {
36 | case 4:
37 | rl.RejectStatusCode, err = strconv.Atoi(args[3])
38 | if err != nil {
39 | return d.Errf("reject_status must be an integer; invalid: %v", err)
40 | }
41 | fallthrough
42 | case 3:
43 | size, err := strconv.Atoi(args[2])
44 | if err != nil {
45 | return d.Errf("zone_size must be an integer; invalid: %v", err)
46 | }
47 | rl.ZoneSize = size
48 | fallthrough
49 | case 2:
50 | rl.Rate = args[1]
51 | rl.Key = args[0]
52 | default:
53 | return d.ArgErr()
54 | }
55 | }
56 | return nil
57 | }
58 |
59 | // Interface guards
60 | var (
61 | _ caddyfile.Unmarshaler = (*RateLimit)(nil)
62 | )
63 |
--------------------------------------------------------------------------------
/dynamichandler/caddyfile.go:
--------------------------------------------------------------------------------
1 | package dynamichandler
2 |
3 | import (
4 | "fmt"
5 | "path/filepath"
6 |
7 | "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
8 | "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
9 | "github.com/caddyserver/caddy/v2/modules/caddyhttp"
10 | )
11 |
12 | func init() {
13 | httpcaddyfile.RegisterHandlerDirective("dynamic_handler", parseCaddyfile)
14 | }
15 |
16 | // parseCaddyfile sets up a handler from Caddyfile tokens.
17 | func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
18 | dh := new(DynamicHandler)
19 | if err := dh.UnmarshalCaddyfile(h.Dispenser); err != nil {
20 | return nil, err
21 | }
22 | return dh, nil
23 | }
24 |
25 | // UnmarshalCaddyfile implements caddyfile.Unmarshaler. Syntax:
26 | //
27 | // dynamic_handler {
28 | // root
29 | // config
30 | // }
31 | //
32 | func (dh *DynamicHandler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
33 | if !d.Next() {
34 | return d.ArgErr()
35 | }
36 |
37 | if !d.NextArg() {
38 | return d.ArgErr()
39 | }
40 | dh.Name = d.Val()
41 |
42 | // Get the path of the Caddyfile.
43 | caddyfilePath, err := filepath.Abs(d.File())
44 | if err != nil {
45 | return fmt.Errorf("failed to get absolute path of file: %s: %v", d.File(), err)
46 | }
47 | caddyfileDir := filepath.Dir(caddyfilePath)
48 | dh.Root = caddyfileDir // Defaults to the directory of the Caddyfile.
49 |
50 | for nesting := d.Nesting(); d.NextBlock(nesting); {
51 | switch d.Val() {
52 | case "root":
53 | if !d.NextArg() {
54 | return d.ArgErr()
55 | }
56 | root := d.Val()
57 |
58 | if filepath.IsAbs(root) {
59 | dh.Root = root
60 | return nil
61 | }
62 |
63 | // Make the path relative to the Caddyfile rather than the
64 | // current working directory.
65 | dh.Root = filepath.Join(caddyfileDir, root)
66 |
67 | case "config":
68 | if !d.NextArg() {
69 | return d.ArgErr()
70 | }
71 | dh.Config = d.Val()
72 | }
73 | }
74 |
75 | return nil
76 | }
77 |
78 | // Interface guards
79 | var (
80 | _ caddyfile.Unmarshaler = (*DynamicHandler)(nil)
81 | )
82 |
--------------------------------------------------------------------------------
/dynamichandler/README.md:
--------------------------------------------------------------------------------
1 | # dynamichandler
2 |
3 | A Caddy v2 extension to execute plugins (written in Go) dynamically by [Yaegi][1].
4 |
5 | This means plugins do not need to be pre-compiled and linked into Caddy.
6 |
7 |
8 | ## Installation
9 |
10 | ```bash
11 | $ xcaddy build --with github.com/RussellLuo/caddy-ext/dynamichandler
12 | ```
13 |
14 | ## Caddyfile Syntax
15 |
16 | ```
17 | dynamic_handler {
18 | root
19 | config
20 | }
21 | ```
22 |
23 | Parameters:
24 |
25 | - ``: The plugin name (as well as the Go package name).
26 | - ``: The root path of the plugin code. Defaults to the directory of the Caddyfile.
27 | + `` is an absolute path: `//.go`
28 | + `` is a relative path: `///.go`
29 | - ``: The plugin configuration in JSON format.
30 |
31 |
32 | ## Example
33 |
34 | Take the plugin [visitorip](plugins/visitorip/visitorip.go) as an example, with the following Caddyfile:
35 |
36 | ```
37 | localhost:8080 {
38 | route /foo {
39 | dynamic_handler visitorip {
40 | root plugins
41 | config `{
42 | "output": "stdout"
43 | }`
44 | }
45 | }
46 | }
47 | ```
48 |
49 | Access the `/foo` endpoint:
50 |
51 | ```bash
52 | $ curl 'https://localhost:8080/foo'
53 | ```
54 |
55 | Then see the output of Caddy server:
56 |
57 | ```console
58 | ...
59 | 127.0.0.1:55255
60 | ...
61 |
62 | ```
63 |
64 | ## Best Practices
65 |
66 | ### Using [snippets][2] to simplify Caddyfile
67 |
68 | Directory structure:
69 |
70 | ```
71 | ├── Caddyfile
72 | └── plugins
73 | ├── plugin1
74 | │ └── plugin1.go
75 | └── plugin2
76 | └── plugin2.go
77 | ```
78 |
79 | Caddyfile:
80 |
81 | ```
82 | (dynamic) {
83 | dynamic_handler {args.0} {
84 | root plugins
85 | config {args.1}
86 | }
87 | }
88 |
89 | localhost:8080 {
90 | route /foo {
91 | import dynamic plugin1 `{
92 | "arg1": "value1",
93 | "arg2": "value2"
94 | }`
95 | }
96 |
97 | route /bar {
98 | import dynamic plugin2 `` # no config
99 | }
100 | }
101 | ```
102 |
103 |
104 | [1]: https://github.com/traefik/yaegi
105 | [2]: https://caddyserver.com/docs/caddyfile/concepts#snippets
--------------------------------------------------------------------------------
/ratelimit/README.md:
--------------------------------------------------------------------------------
1 | # ratelimit
2 |
3 | A Caddy v2 extension to apply rate-limiting for HTTP requests.
4 |
5 |
6 | ## Installation
7 |
8 | ```bash
9 | $ xcaddy build --with github.com/RussellLuo/caddy-ext/ratelimit
10 | ```
11 |
12 | ## Caddyfile Syntax
13 |
14 | ```
15 | rate_limit [] [ []]
16 | ```
17 |
18 | Parameters:
19 |
20 | - ``: The variable used to differentiate one client from another. Currently supported variables ([Caddy shorthand placeholders][1]):
21 | + `{path.}`
22 | + `{query.}`
23 | + `{header.}`
24 | + `{cookie.}`
25 | + `{body.}` (requires the [requestbodyvar](https://github.com/RussellLuo/caddy-ext/tree/master/requestbodyvar) extension)
26 | + `{remote.host}` (ignores the `X-Forwarded-For` header)
27 | + `{remote.port}`
28 | + `{remote.ip}` (prefers the first IP in the `X-Forwarded-For` header)
29 | + `{remote.host_prefix.}` (CIDR block version of `{remote.host}`)
30 | + `{remote.ip_prefix.}` (CIDR block version of `{remote.ip}`)
31 | - ``: The request rate limit (per key value) specified in requests per second (r/s) or requests per minute (r/m).
32 | - ``: The size (i.e. the number of key values) of the LRU zone that keeps states of these key values. Defaults to 10,000.
33 | - ``: The HTTP status code of the response when a client exceeds the rate limit. Defaults to 429 (Too Many Requests).
34 |
35 |
36 | ## Example
37 |
38 | With the following Caddyfile:
39 |
40 | ```
41 | localhost:8080 {
42 | route /foo {
43 | rate_limit {query.id} 2r/m
44 |
45 | respond 200
46 | }
47 | }
48 | ```
49 |
50 | You can apply the rate of `2 requests per minute` to the path `/foo`, and Caddy will respond with status code 429 when a client exceeds:
51 |
52 | ```bash
53 | $ curl -w "%{http_code}" 'https://localhost:8080/foo?id=1'
54 | 200
55 | $ curl -w "%{http_code}" 'https://localhost:8080/foo?id=1'
56 | 200
57 | $ curl -w "%{http_code}" 'https://localhost:8080/foo?id=1'
58 | 429
59 | ```
60 |
61 | An extra request with other value for the request parameter `id` will not be limited:
62 |
63 | ```bash
64 | $ curl -w "%{http_code}" 'https://localhost:8080/foo?id=2'
65 | 200
66 | ```
67 |
68 |
69 | [1]: https://caddyserver.com/docs/caddyfile/concepts#placeholders
--------------------------------------------------------------------------------
/layer4/README.md:
--------------------------------------------------------------------------------
1 | # layer4
2 |
3 | The layer4 Caddyfile (via [global options](https://github.com/caddyserver/caddy/pull/3990)) for [mholt/caddy-l4](https://github.com/mholt/caddy-l4).
4 |
5 | Currently supported handlers:
6 |
7 | - `echo` (layer4.handlers.echo)
8 | - `proxy_protocol` (layer4.handlers.proxy_protocol)
9 | - `proxy` (layer4.handlers.proxy)
10 | - `tls` (layer4.handlers.tls)
11 |
12 | ## Installation
13 |
14 | ```bash
15 | $ xcaddy build --with github.com/RussellLuo/caddy-ext/layer4
16 | ```
17 |
18 | ## Caddyfile Syntax
19 |
20 | ### The `layer4` global option
21 |
22 | ```
23 | layer4 {
24 | # server 1
25 | {
26 |
27 | ...
28 | }
29 |
30 | # server 2
31 | {
32 |
33 | ...
34 | }
35 | }
36 | ```
37 |
38 | ### Handlers
39 |
40 | The `echo` handler:
41 |
42 | ```
43 | echo
44 | ```
45 |
46 | The `proxy_protocol` handler:
47 |
48 | ```
49 | proxy_protocol {
50 | timeout
51 | allow
52 | }
53 | ```
54 |
55 | The `proxy` handler:
56 |
57 | ```
58 | proxy [] {
59 | # backends
60 | to
61 | ...
62 |
63 | # load balancing
64 | lb_policy []
65 | lb_try_duration
66 | lb_try_interval
67 |
68 | # active health checking
69 | health_port
70 | health_interval
71 | health_timeout
72 |
73 | # sending the PROXY protocol
74 | proxy_protocol
75 | }
76 | ```
77 |
78 | The `tls` handler:
79 |
80 | ```
81 | tls
82 | ```
83 |
84 | ## Example
85 |
86 | With the following Caddyfile:
87 |
88 | ```
89 | {
90 | layer4 {
91 | :8080 {
92 | proxy {
93 | to localhost:8081 localhost:8082
94 | lb_policy round_robin
95 | health_interval 5s
96 | }
97 | }
98 | }
99 | }
100 |
101 | :8081 {
102 | respond "This is 8081"
103 | }
104 |
105 | :8082 {
106 | respond "This is 8082"
107 | }
108 | ```
109 |
110 | Requests to `:8080` will be forwarded to upstreams `:8081` and `:8082` in a round-robin policy:
111 |
112 | ```bash
113 | $ curl http://localhost:8080
114 | This is 8081
115 | $ curl http://localhost:8080
116 | This is 8082
117 | ```
118 |
--------------------------------------------------------------------------------
/dynamichandler/yaegisymbols/github_com-RussellLuo-caddy-ext-dynamichandler-caddymiddleware.go:
--------------------------------------------------------------------------------
1 | // Code generated by 'yaegi extract github.com/RussellLuo/caddy-ext/dynamichandler/caddymiddleware'. DO NOT EDIT.
2 |
3 | package yaegisymbols
4 |
5 | import (
6 | "github.com/RussellLuo/caddy-ext/dynamichandler/caddymiddleware"
7 | "net/http"
8 | "reflect"
9 | )
10 |
11 | func init() {
12 | Symbols["github.com/RussellLuo/caddy-ext/dynamichandler/caddymiddleware/caddymiddleware"] = map[string]reflect.Value{
13 | // type definitions
14 | "Handler": reflect.ValueOf((*caddymiddleware.Handler)(nil)),
15 | "HandlerFunc": reflect.ValueOf((*caddymiddleware.HandlerFunc)(nil)),
16 | "Middleware": reflect.ValueOf((*caddymiddleware.Middleware)(nil)),
17 |
18 | // interface wrapper definitions
19 | "_Handler": reflect.ValueOf((*_github_com_RussellLuo_caddy_ext_dynamichandler_caddymiddleware_Handler)(nil)),
20 | "_Middleware": reflect.ValueOf((*_github_com_RussellLuo_caddy_ext_dynamichandler_caddymiddleware_Middleware)(nil)),
21 | }
22 | }
23 |
24 | // _github_com_RussellLuo_caddy_ext_dynamichandler_caddymiddleware_Handler is an interface wrapper for Handler type
25 | type _github_com_RussellLuo_caddy_ext_dynamichandler_caddymiddleware_Handler struct {
26 | IValue interface{}
27 | WServeHTTP func(a0 http.ResponseWriter, a1 *http.Request) error
28 | }
29 |
30 | func (W _github_com_RussellLuo_caddy_ext_dynamichandler_caddymiddleware_Handler) ServeHTTP(a0 http.ResponseWriter, a1 *http.Request) error {
31 | return W.WServeHTTP(a0, a1)
32 | }
33 |
34 | // _github_com_RussellLuo_caddy_ext_dynamichandler_caddymiddleware_Middleware is an interface wrapper for Middleware type
35 | type _github_com_RussellLuo_caddy_ext_dynamichandler_caddymiddleware_Middleware struct {
36 | IValue interface{}
37 | WCleanup func() error
38 | WProvision func() error
39 | WServeHTTP func(a0 http.ResponseWriter, a1 *http.Request, a2 caddymiddleware.Handler) error
40 | WValidate func() error
41 | }
42 |
43 | func (W _github_com_RussellLuo_caddy_ext_dynamichandler_caddymiddleware_Middleware) Cleanup() error {
44 | return W.WCleanup()
45 | }
46 | func (W _github_com_RussellLuo_caddy_ext_dynamichandler_caddymiddleware_Middleware) Provision() error {
47 | return W.WProvision()
48 | }
49 | func (W _github_com_RussellLuo_caddy_ext_dynamichandler_caddymiddleware_Middleware) ServeHTTP(a0 http.ResponseWriter, a1 *http.Request, a2 caddymiddleware.Handler) error {
50 | return W.WServeHTTP(a0, a1, a2)
51 | }
52 | func (W _github_com_RussellLuo_caddy_ext_dynamichandler_caddymiddleware_Middleware) Validate() error {
53 | return W.WValidate()
54 | }
55 |
--------------------------------------------------------------------------------
/flagr/caddyfile.go:
--------------------------------------------------------------------------------
1 | package flagr
2 |
3 | import (
4 | "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
5 | "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
6 | "github.com/caddyserver/caddy/v2/modules/caddyhttp"
7 | )
8 |
9 | func init() {
10 | httpcaddyfile.RegisterHandlerDirective("flagr", parseCaddyfile)
11 | }
12 |
13 | // parseCaddyfile sets up a handler for flagr from Caddyfile tokens.
14 | func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
15 | f := new(Flagr)
16 | if err := f.UnmarshalCaddyfile(h.Dispenser); err != nil {
17 | return nil, err
18 | }
19 | return f, nil
20 | }
21 |
22 | // UnmarshalCaddyfile implements caddyfile.Unmarshaler. Syntax:
23 | //
24 | // flagr {
25 | // evaluator []
26 | // entity_id
27 | // entity_context {
28 | //
29 | //
30 | // ...
31 | // }
32 | // flag_keys ...
33 | // bind_variant_keys_to
34 | // }
35 | //
36 | func (f *Flagr) UnmarshalCaddyfile(d *caddyfile.Dispenser) (err error) {
37 | for d.Next() {
38 | if !d.NextArg() {
39 | return d.ArgErr()
40 | }
41 | f.URL = d.Val()
42 |
43 | for nesting := d.Nesting(); d.NextBlock(nesting); {
44 | switch d.Val() {
45 | case "evaluator":
46 | if d.NextArg() {
47 | f.Evaluator = d.Val()
48 | }
49 | if f.Evaluator == "local" {
50 | if d.NextArg() {
51 | f.RefreshInterval = d.Val()
52 | }
53 | }
54 |
55 | case "entity_id":
56 | if !d.NextArg() {
57 | return d.ArgErr()
58 | }
59 | f.EntityID = d.Val()
60 |
61 | case "entity_context":
62 | f.EntityContext = make(map[string]interface{})
63 | for nesting := d.Nesting(); d.NextBlock(nesting); {
64 | key := d.Val()
65 | if !d.NextArg() {
66 | return d.ArgErr()
67 | }
68 | value := d.Val()
69 |
70 | if key == "" || value == "" {
71 | return d.Err("empty key or empty value within entity_context")
72 | }
73 | f.EntityContext[key] = value
74 | }
75 |
76 | case "flag_keys":
77 | for d.NextArg() {
78 | f.FlagKeys = append(f.FlagKeys, d.Val())
79 | }
80 | if len(f.FlagKeys) == 0 {
81 | return d.ArgErr()
82 | }
83 |
84 | case "bind_variant_keys_to":
85 | if !d.NextArg() {
86 | return d.ArgErr()
87 | }
88 | f.BindVariantKeysTo = d.Val()
89 | }
90 | }
91 | }
92 |
93 | return nil
94 | }
95 |
96 | // Interface guards
97 | var (
98 | _ caddyfile.Unmarshaler = (*Flagr)(nil)
99 | )
100 |
--------------------------------------------------------------------------------
/flagr/evaluator/remote.go:
--------------------------------------------------------------------------------
1 | package evaluator
2 |
3 | import (
4 | "context"
5 | "sync"
6 |
7 | "github.com/checkr/flagr/swagger_gen/models"
8 | "github.com/checkr/goflagr"
9 | )
10 |
11 | var (
12 | onceRemote sync.Once
13 | singletonRemote *Remote
14 | )
15 |
16 | type Remote struct {
17 | apiClient *goflagr.APIClient
18 | }
19 |
20 | func NewRemote(url string) *Remote {
21 | // Initialize the singleton once.
22 | onceRemote.Do(func() {
23 | singletonRemote = &Remote{
24 | apiClient: goflagr.NewAPIClient(&goflagr.Configuration{
25 | BasePath: url,
26 | DefaultHeader: make(map[string]string),
27 | UserAgent: "Caddy/go",
28 | }),
29 | }
30 | })
31 | return singletonRemote
32 | }
33 |
34 | func (r *Remote) PostEvaluationBatch(ctx context.Context, req *models.EvaluationBatchRequest) (*models.EvaluationBatchResponse, error) {
35 | resp, _, err := r.apiClient.EvaluationApi.PostEvaluationBatch(ctx, toGoFlagrRequest(req))
36 | if err != nil {
37 | return nil, err
38 | }
39 | return fromGoFlagrResponse(resp), nil
40 | }
41 |
42 | func toGoFlagrRequest(req *models.EvaluationBatchRequest) (result goflagr.EvaluationBatchRequest) {
43 | for _, e := range req.Entities {
44 | result.Entities = append(result.Entities, goflagr.EvaluationEntity{
45 | EntityID: e.EntityID,
46 | EntityContext: &e.EntityContext,
47 | })
48 | }
49 | result.FlagKeys = req.FlagKeys
50 | return
51 | }
52 |
53 | func fromGoFlagrResponse(resp goflagr.EvaluationBatchResponse) *models.EvaluationBatchResponse {
54 | result := &models.EvaluationBatchResponse{}
55 | for _, r := range resp.EvaluationResults {
56 | result.EvaluationResults = append(result.EvaluationResults, &models.EvalResult{
57 | FlagID: r.FlagID,
58 | FlagKey: r.FlagKey,
59 | FlagSnapshotID: r.FlagSnapshotID,
60 | SegmentID: r.SegmentID,
61 | VariantID: r.VariantID,
62 | VariantKey: r.VariantKey,
63 | VariantAttachment: *r.VariantAttachment,
64 | EvalContext: &models.EvalContext{
65 | EntityID: r.EvalContext.EntityID,
66 | EntityType: r.EvalContext.EntityType,
67 | EntityContext: *r.EvalContext.EntityContext,
68 | EnableDebug: r.EvalContext.EnableDebug,
69 | FlagID: r.EvalContext.FlagID,
70 | FlagKey: r.EvalContext.FlagKey,
71 | },
72 | Timestamp: r.Timestamp,
73 | EvalDebugLog: &models.EvalDebugLog{
74 | Msg: r.EvalDebugLog.Msg,
75 | SegmentDebugLogs: convertSegmentDebugLogs(r.EvalDebugLog.SegmentDebugLogs),
76 | },
77 | })
78 | }
79 | return result
80 | }
81 |
82 | func convertSegmentDebugLogs(logs []goflagr.SegmentDebugLog) (result []*models.SegmentDebugLog) {
83 | for _, l := range logs {
84 | result = append(result, &models.SegmentDebugLog{
85 | Msg: l.Msg,
86 | SegmentID: l.SegmentID,
87 | })
88 | }
89 | return
90 | }
91 |
--------------------------------------------------------------------------------
/dynamichandler/dynamichandler.go:
--------------------------------------------------------------------------------
1 | package dynamichandler
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "go/build"
7 | "net/http"
8 | "path/filepath"
9 |
10 | "github.com/caddyserver/caddy/v2"
11 | "github.com/caddyserver/caddy/v2/modules/caddyhttp"
12 | "github.com/traefik/yaegi/interp"
13 | "github.com/traefik/yaegi/stdlib"
14 | "github.com/traefik/yaegi/stdlib/unsafe"
15 | "go.uber.org/zap"
16 |
17 | "github.com/RussellLuo/caddy-ext/dynamichandler/caddymiddleware"
18 | "github.com/RussellLuo/caddy-ext/dynamichandler/yaegisymbols"
19 | )
20 |
21 | func init() {
22 | caddy.RegisterModule(DynamicHandler{})
23 | }
24 |
25 | // DynamicHandler implements a handler that can execute plugins (written in Go) dynamically.
26 | type DynamicHandler struct {
27 | // The plugin name (as well as the Go package name).
28 | Name string `json:"name,omitempty"`
29 |
30 | // The root path of the plugin code. Defaults to the directory of the Caddyfile.
31 | //
32 | // The full path of the plugin code:
33 | //
34 | // - Root is an absolute path: `//.go`
35 | // - Root is a relative path: `///.go`
36 | Root string `json:"root,omitempty"`
37 |
38 | // The plugin configuration in JSON format.
39 | Config string `json:"config,omitempty"`
40 |
41 | middleware caddymiddleware.Middleware
42 | logger *zap.Logger
43 | }
44 |
45 | // CaddyModule returns the Caddy module information.
46 | func (DynamicHandler) CaddyModule() caddy.ModuleInfo {
47 | return caddy.ModuleInfo{
48 | ID: "http.handlers.dynamic_handler",
49 | New: func() caddy.Module { return new(DynamicHandler) },
50 | }
51 | }
52 |
53 | // Provision implements caddy.Provisioner.
54 | func (dh *DynamicHandler) Provision(ctx caddy.Context) error {
55 | dh.logger = ctx.Logger(dh)
56 | return dh.provision()
57 | }
58 |
59 | func (dh *DynamicHandler) provision() error {
60 | m, err := dh.eval()
61 | if err != nil {
62 | return err
63 | }
64 |
65 | dh.middleware = m
66 | return dh.middleware.Provision()
67 | }
68 |
69 | // Validate implements caddy.Validator.
70 | func (dh *DynamicHandler) Validate() error {
71 | return dh.middleware.Validate()
72 | }
73 |
74 | // Cleanup implements caddy.CleanerUpper.
75 | func (dh *DynamicHandler) Cleanup() error {
76 | if dh.middleware != nil {
77 | return dh.middleware.Cleanup()
78 | }
79 | return nil
80 | }
81 |
82 | // ServeHTTP implements caddyhttp.MiddlewareHandler.
83 | func (dh *DynamicHandler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
84 | return dh.middleware.ServeHTTP(w, r, next)
85 | }
86 |
87 | func (dh *DynamicHandler) eval() (caddymiddleware.Middleware, error) {
88 | i := interp.New(interp.Options{GoPath: build.Default.GOPATH})
89 | for _, exports := range []interp.Exports{
90 | stdlib.Symbols,
91 | unsafe.Symbols,
92 | yaegisymbols.Symbols,
93 | } {
94 | if err := i.Use(exports); err != nil {
95 | return nil, err
96 | }
97 | }
98 |
99 | pkgPath := filepath.Join(dh.Root, dh.Name, dh.Name+".go")
100 | if _, err := i.EvalPath(pkgPath); err != nil {
101 | return nil, err
102 | }
103 |
104 | newFunc, err := i.Eval(dh.Name + ".New")
105 | if err != nil {
106 | return nil, err
107 | }
108 |
109 | newMiddleware, ok := newFunc.Interface().(func() caddymiddleware.Middleware)
110 | if !ok {
111 | return nil, fmt.Errorf("%s.New does not implement `func() caddymiddleware.Middleware`", dh.Name)
112 | }
113 | middleware := newMiddleware()
114 |
115 | if len(dh.Config) > 0 {
116 | out := middleware.(yaegisymbols.Middleware).IValue
117 | if err := json.Unmarshal([]byte(dh.Config), out); err != nil {
118 | return nil, err
119 | }
120 | }
121 |
122 | return middleware, nil
123 | }
124 |
125 | // Interface guards
126 | var (
127 | _ caddy.Provisioner = (*DynamicHandler)(nil)
128 | _ caddy.Validator = (*DynamicHandler)(nil)
129 | _ caddy.CleanerUpper = (*DynamicHandler)(nil)
130 | _ caddyhttp.MiddlewareHandler = (*DynamicHandler)(nil)
131 | )
132 |
--------------------------------------------------------------------------------
/requestbodyvar/requestbodyvar.go:
--------------------------------------------------------------------------------
1 | package requestbodyvar
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "io"
7 | "io/ioutil"
8 | "net/http"
9 | "strings"
10 |
11 | "github.com/caddyserver/caddy/v2"
12 | "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
13 | "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
14 | "github.com/caddyserver/caddy/v2/modules/caddyhttp"
15 | "go.uber.org/zap"
16 | )
17 |
18 | const (
19 | fullReqBodyReplPrefix = "http.request.body."
20 | shortReqBodyReplPrefix = "body."
21 |
22 | // For the request's buffered body
23 | bodyBufferCtxKey caddy.CtxKey = "body_buffer"
24 | )
25 |
26 | func init() {
27 | caddy.RegisterModule(RequestBodyVar{})
28 | httpcaddyfile.RegisterHandlerDirective("request_body_var", parseCaddyfile)
29 | }
30 |
31 | // RequestBodyVar implements an HTTP handler that replaces {http.request.body.*}
32 | // with the value of the given field from request body, if any.
33 | type RequestBodyVar struct {
34 | logger *zap.Logger
35 | }
36 |
37 | // CaddyModule returns the Caddy module information.
38 | func (RequestBodyVar) CaddyModule() caddy.ModuleInfo {
39 | return caddy.ModuleInfo{
40 | ID: "http.handlers.request_body_var",
41 | New: func() caddy.Module { return new(RequestBodyVar) },
42 | }
43 | }
44 |
45 | // Provision implements caddy.Provisioner.
46 | func (rbv *RequestBodyVar) Provision(ctx caddy.Context) (err error) {
47 | rbv.logger = ctx.Logger(rbv)
48 | return nil
49 | }
50 |
51 | // ServeHTTP implements caddyhttp.MiddlewareHandler.
52 | func (rbv RequestBodyVar) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
53 | bodyVars := func(key string) (interface{}, bool) {
54 | // We need to declare ctx before the use of the `goto`.
55 | // See https://github.com/golang/go/issues/27165 and https://github.com/golang/go/issues/26058.
56 | var ctx context.Context
57 |
58 | key, ok := parseKey(key)
59 | if !ok || key == "" {
60 | rbv.logger.Error("invalid var", zap.String("key", key))
61 | return nil, false
62 | }
63 |
64 | // First of all, try to get the value from the buffered JSON body, if any.
65 | buf, ok := r.Context().Value(bodyBufferCtxKey).(*bytes.Buffer)
66 | if ok {
67 | rbv.logger.Debug("got from the buffer", zap.String("key", key))
68 | goto Query
69 | }
70 |
71 | rbv.logger.Debug("got from the body", zap.String("key", key))
72 |
73 | // Otherwise, try to get the value by reading the request body.
74 | if r == nil || r.Body == nil {
75 | return "", true
76 | }
77 | // Close the real body since we will replace it with a fake one.
78 | defer r.Body.Close()
79 |
80 | // Copy the request body.
81 | buf = new(bytes.Buffer)
82 | if _, err := io.Copy(buf, r.Body); err != nil {
83 | return "", true
84 | }
85 |
86 | // Replace the real body with buffered data.
87 | r.Body = ioutil.NopCloser(buf)
88 |
89 | // Add the buffered JSON body into the context for the request.
90 | ctx = context.WithValue(r.Context(), bodyBufferCtxKey, buf)
91 | r = r.WithContext(ctx)
92 |
93 | Query:
94 | querier, err := newQuerier(buf, r.Header.Get("Content-Type"))
95 | if err != nil {
96 | rbv.logger.Error("failed to new querier", zap.String("key", key), zap.Error(err))
97 | return "", true
98 | }
99 | return querier.Query(key), true
100 | }
101 |
102 | repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
103 | repl.Map(bodyVars)
104 |
105 | return next.ServeHTTP(w, r)
106 | }
107 |
108 | func parseKey(s string) (string, bool) {
109 | switch {
110 | case strings.HasPrefix(s, fullReqBodyReplPrefix):
111 | return s[len(fullReqBodyReplPrefix):], true
112 | case strings.HasPrefix(s, shortReqBodyReplPrefix):
113 | return s[len(shortReqBodyReplPrefix):], true
114 | default:
115 | // unrecognized
116 | return "", false
117 | }
118 | }
119 |
120 | // UnmarshalCaddyfile - this is a no-op
121 | func (rbv *RequestBodyVar) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
122 | return nil
123 | }
124 |
125 | func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
126 | rbv := new(RequestBodyVar)
127 | err := rbv.UnmarshalCaddyfile(h.Dispenser)
128 | if err != nil {
129 | return nil, err
130 | }
131 |
132 | return rbv, nil
133 | }
134 |
135 | // Interface guards
136 | var (
137 | _ caddyhttp.MiddlewareHandler = (*RequestBodyVar)(nil)
138 | _ caddyfile.Unmarshaler = (*RequestBodyVar)(nil)
139 | )
140 |
--------------------------------------------------------------------------------
/flagr/README.md:
--------------------------------------------------------------------------------
1 | # flagr
2 |
3 | A Caddy v2 extension to apply [Feature Flags][1] for HTTP requests by using [Flagr][2].
4 |
5 |
6 | ## Installation
7 |
8 | ```bash
9 | $ xcaddy build --with github.com/RussellLuo/caddy-ext/flagr
10 | ```
11 |
12 | ## Caddyfile Syntax
13 |
14 | ```
15 | flagr {
16 | evaluator []
17 | entity_id
18 | entity_context {
19 |
20 |
21 | ...
22 | }
23 | flag_keys ...
24 | bind_variant_keys_to
25 | }
26 | ```
27 |
28 | Parameters:
29 |
30 | - ``: The address of the Flagr server.
31 | - ``: Which evaluator to use. Defaults to `"local"`. Supported options:
32 | + `"local"`
33 | + `"remote"`
34 | - ``: The refresh interval of the internal eval cache (only used for the `"local"` evaluator). Defaults to `"10s"`.
35 | - ``: The unique ID from the entity, which is used to deterministically at random to evaluate the flag result. Defaults to `""` (Flagr [will randomly generate one][3]), and it must be a [Caddy variable][4] if provided:
36 | + `{path.}`
37 | + `{query.}`
38 | + `{header.}`
39 | + `{cookie.}`
40 | + `{body.}` (requires the [requestbodyvar](https://github.com/RussellLuo/caddy-ext/tree/master/requestbodyvar) extension)
41 | - ``: The context parameters (key-value pairs) of the entity, which is used to match the constraints. The value part may be a Caddy variable (see ``).
42 | - ``: A list of flag keys to look up.
43 | - ``: Which element of the request to bind the evaluated variant keys. Defaults to `"header.X-Flagr-Variant"`. Supported options:
44 | + `"header."`
45 | + `"query."`
46 |
47 |
48 | ## Example
49 |
50 | With the Flagr config and the Caddyfile as below:
51 |
52 | 
53 |
54 | ```
55 | localhost:8080 {
56 | route /foo {
57 | flagr http://127.0.0.1:18000/api/v1 {
58 | entity_id {query.id}
59 | entity_context {
60 | city CD
61 | }
62 | flag_keys demo
63 | }
64 | respond {header.X-Flagr-Variant} 200
65 | }
66 | route /bar {
67 | flagr http://127.0.0.1:18000/api/v1 {
68 | entity_id {query.id}
69 | entity_context {
70 | city BJ
71 | }
72 | flag_keys demo
73 | }
74 | respond {header.X-Flagr-Variant} 200
75 | }
76 | }
77 | ```
78 |
79 | You can get the responses as follows:
80 |
81 | ```bash
82 | $ curl 'https://localhost:8080/foo?id=1'
83 | demo.on
84 | $ curl 'https://localhost:8080/bar?id=1'
85 | ```
86 |
87 |
88 | ## Local Evaluator vs Remote Evaluator
89 |
90 | ### Internals
91 |
92 | 
93 |
94 | ### Benchmarks
95 |
96 | Prerequisites:
97 |
98 | - Run Flagr locally [with docker][5]
99 | - Use the same Flagr config as in Example
100 | - Run Caddy with the following Caddyfile:
101 |
102 | ```
103 | localhost:8080 {
104 | route /local {
105 | flagr http://127.0.0.1:18000/api/v1 {
106 | evaluator local
107 | entity_id {query.id}
108 | entity_context {
109 | city CD
110 | }
111 | flag_keys demo
112 | }
113 | respond 204
114 | }
115 | route /remote {
116 | flagr http://127.0.0.1:18000/api/v1 {
117 | evaluator remote
118 | entity_id {query.id}
119 | entity_context {
120 | city CD
121 | }
122 | flag_keys demo
123 | }
124 | respond 204
125 | }
126 | }
127 | ```
128 | - Install the benchmarking tool [wrk][6]
129 |
130 | Here are the benchmark results I got on my MacBook:
131 |
132 | ```bash
133 | $ wrk -t15 -c200 -d30s 'https://localhost:8080/local?id=1'
134 | Running 30s test @ https://localhost:8080/local?id=1
135 | 15 threads and 200 connections
136 | Thread Stats Avg Stdev Max +/- Stdev
137 | Latency 7.14ms 8.83ms 161.49ms 90.13%
138 | Req/Sec 2.48k 396.11 8.44k 80.79%
139 | 1106860 requests in 30.10s, 83.39MB read
140 | Requests/sec: 36769.32
141 | Transfer/sec: 2.77MB
142 | ```
143 | ```bash
144 | $ wrk -t15 -c200 -d30s 'https://localhost:8080/remote?id=1'
145 | Running 30s test @ https://localhost:8080/remote?id=1
146 | 15 threads and 200 connections
147 | Thread Stats Avg Stdev Max +/- Stdev
148 | Latency 48.68ms 97.87ms 1.20s 89.98%
149 | Req/Sec 778.59 239.79 1.66k 71.50%
150 | 348077 requests in 30.10s, 26.22MB read
151 | Requests/sec: 11564.13
152 | Transfer/sec: 0.87MB
153 | ```
154 |
155 |
156 | [1]: https://martinfowler.com/articles/feature-toggles.html
157 | [2]: https://github.com/checkr/flagr
158 | [3]: https://checkr.github.io/flagr/api_docs/#operation/postEvaluation
159 | [4]: https://caddyserver.com/docs/caddyfile/concepts#placeholders
160 | [5]: https://checkr.github.io/flagr/#/home?id=run
161 | [6]: https://github.com/wg/wrk
--------------------------------------------------------------------------------
/dynamichandler/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/RussellLuo/caddy-ext/dynamichandler
2 |
3 | go 1.17
4 |
5 | require (
6 | github.com/caddyserver/caddy/v2 v2.5.1
7 | github.com/traefik/yaegi v0.13.0
8 | go.uber.org/zap v1.21.0
9 | )
10 |
11 | require (
12 | filippo.io/edwards25519 v1.0.0-rc.1 // indirect
13 | github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
14 | github.com/Masterminds/goutils v1.1.1 // indirect
15 | github.com/Masterminds/semver/v3 v3.1.1 // indirect
16 | github.com/Masterminds/sprig/v3 v3.2.2 // indirect
17 | github.com/antlr/antlr4 v0.0.0-20200503195918-621b933c7a7f // indirect
18 | github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
19 | github.com/beorn7/perks v1.0.1 // indirect
20 | github.com/caddyserver/certmagic v0.16.1 // indirect
21 | github.com/cespare/xxhash v1.1.0 // indirect
22 | github.com/cespare/xxhash/v2 v2.1.2 // indirect
23 | github.com/cheekybits/genny v1.0.0 // indirect
24 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
25 | github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
26 | github.com/dgraph-io/badger v1.6.2 // indirect
27 | github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
28 | github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd // indirect
29 | github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
30 | github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac // indirect
31 | github.com/fsnotify/fsnotify v1.5.1 // indirect
32 | github.com/go-kit/kit v0.10.0 // indirect
33 | github.com/go-logfmt/logfmt v0.5.0 // indirect
34 | github.com/go-sql-driver/mysql v1.6.0 // indirect
35 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
36 | github.com/golang/protobuf v1.5.2 // indirect
37 | github.com/golang/snappy v0.0.4 // indirect
38 | github.com/google/cel-go v0.7.3 // indirect
39 | github.com/google/uuid v1.3.0 // indirect
40 | github.com/huandu/xstrings v1.3.2 // indirect
41 | github.com/imdario/mergo v0.3.12 // indirect
42 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect
43 | github.com/jackc/pgconn v1.10.1 // indirect
44 | github.com/jackc/pgio v1.0.0 // indirect
45 | github.com/jackc/pgpassfile v1.0.0 // indirect
46 | github.com/jackc/pgproto3/v2 v2.2.0 // indirect
47 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
48 | github.com/jackc/pgtype v1.9.0 // indirect
49 | github.com/jackc/pgx/v4 v4.14.0 // indirect
50 | github.com/klauspost/compress v1.15.0 // indirect
51 | github.com/klauspost/cpuid/v2 v2.0.11 // indirect
52 | github.com/libdns/libdns v0.2.1 // indirect
53 | github.com/lucas-clemente/quic-go v0.26.0 // indirect
54 | github.com/manifoldco/promptui v0.9.0 // indirect
55 | github.com/marten-seemann/qpack v0.2.1 // indirect
56 | github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect
57 | github.com/marten-seemann/qtls-go1-17 v0.1.1 // indirect
58 | github.com/marten-seemann/qtls-go1-18 v0.1.1 // indirect
59 | github.com/mattn/go-colorable v0.1.8 // indirect
60 | github.com/mattn/go-isatty v0.0.13 // indirect
61 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
62 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
63 | github.com/mholt/acmez v1.0.2 // indirect
64 | github.com/micromdm/scep/v2 v2.1.0 // indirect
65 | github.com/miekg/dns v1.1.46 // indirect
66 | github.com/mitchellh/copystructure v1.2.0 // indirect
67 | github.com/mitchellh/go-ps v1.0.0 // indirect
68 | github.com/mitchellh/reflectwalk v1.0.2 // indirect
69 | github.com/nxadm/tail v1.4.8 // indirect
70 | github.com/onsi/ginkgo v1.16.4 // indirect
71 | github.com/pkg/errors v0.9.1 // indirect
72 | github.com/prometheus/client_golang v1.12.1 // indirect
73 | github.com/prometheus/client_model v0.2.0 // indirect
74 | github.com/prometheus/common v0.32.1 // indirect
75 | github.com/prometheus/procfs v0.7.3 // indirect
76 | github.com/rs/xid v1.2.1 // indirect
77 | github.com/russross/blackfriday/v2 v2.0.1 // indirect
78 | github.com/shopspring/decimal v1.2.0 // indirect
79 | github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
80 | github.com/sirupsen/logrus v1.8.1 // indirect
81 | github.com/slackhq/nebula v1.5.2 // indirect
82 | github.com/smallstep/certificates v0.19.0 // indirect
83 | github.com/smallstep/cli v0.18.0 // indirect
84 | github.com/smallstep/nosql v0.4.0 // indirect
85 | github.com/smallstep/truststore v0.11.0 // indirect
86 | github.com/spf13/cast v1.4.1 // indirect
87 | github.com/stoewer/go-strcase v1.2.0 // indirect
88 | github.com/tailscale/tscert v0.0.0-20220125204807-4509a5fbaf74 // indirect
89 | github.com/urfave/cli v1.22.5 // indirect
90 | go.etcd.io/bbolt v1.3.6 // indirect
91 | go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
92 | go.step.sm/cli-utils v0.7.0 // indirect
93 | go.step.sm/crypto v0.16.1 // indirect
94 | go.step.sm/linkedca v0.15.0 // indirect
95 | go.uber.org/atomic v1.9.0 // indirect
96 | go.uber.org/multierr v1.6.0 // indirect
97 | golang.org/x/crypto v0.0.0-20220210151621-f4118a5b28e2 // indirect
98 | golang.org/x/mod v0.4.2 // indirect
99 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
100 | golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect
101 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
102 | golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b // indirect
103 | golang.org/x/tools v0.1.7 // indirect
104 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
105 | google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf // indirect
106 | google.golang.org/grpc v1.44.0 // indirect
107 | google.golang.org/protobuf v1.27.1 // indirect
108 | gopkg.in/square/go-jose.v2 v2.6.0 // indirect
109 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
110 | howett.net/plist v1.0.0 // indirect
111 | )
112 |
--------------------------------------------------------------------------------
/ratelimit/ratelimit_test.go:
--------------------------------------------------------------------------------
1 | package ratelimit
2 |
3 | import (
4 | "context"
5 | "net/http"
6 | "net/http/httptest"
7 | "reflect"
8 | "testing"
9 |
10 | "github.com/caddyserver/caddy/v2"
11 | "github.com/caddyserver/caddy/v2/modules/caddyhttp"
12 | "go.uber.org/zap"
13 | )
14 |
15 | func TestRateLimit_ServeHTTP(t *testing.T) {
16 | next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
17 | w.WriteHeader(http.StatusOK)
18 | return nil
19 | })
20 |
21 | cases := []struct {
22 | inRL *RateLimit
23 | inReq *http.Request
24 | wantStatusCodes []int
25 | }{
26 | {
27 | inRL: &RateLimit{
28 | Key: "{query.id}",
29 | Rate: "2r/m",
30 | logger: zap.NewNop(),
31 | },
32 | inReq: httptest.NewRequest(http.MethodGet, "/foo?id=1", nil),
33 | wantStatusCodes: []int{
34 | http.StatusOK,
35 | http.StatusOK,
36 | http.StatusTooManyRequests,
37 | },
38 | },
39 | }
40 | for _, c := range cases {
41 | _ = c.inRL.provision()
42 | //c.inRL.Cleanup()
43 |
44 | var gotStatusCodes []int
45 | for i := 0; i < len(c.wantStatusCodes); i++ {
46 | // Build the request object.
47 | repl := caddyhttp.NewTestReplacer(c.inReq)
48 | ctx := context.WithValue(c.inReq.Context(), caddy.ReplacerCtxKey, repl)
49 | req := c.inReq.WithContext(ctx)
50 |
51 | // Build the response object.
52 | w := httptest.NewRecorder()
53 |
54 | _ = c.inRL.ServeHTTP(w, req, next)
55 |
56 | // Collect the response status code.
57 | resp := w.Result()
58 | gotStatusCodes = append(gotStatusCodes, resp.StatusCode)
59 | }
60 | if !reflect.DeepEqual(gotStatusCodes, c.wantStatusCodes) {
61 | t.Fatalf("StatusCodes: got (%#v), want (%#v)", gotStatusCodes, c.wantStatusCodes)
62 | }
63 | }
64 | }
65 |
66 | func TestParseVar(t *testing.T) {
67 | cases := []struct {
68 | in string
69 | want *Var
70 | wantErrStr string
71 | }{
72 | {
73 | in: "{path.id}",
74 | want: &Var{
75 | Raw: "{path.id}",
76 | Name: "{http.request.uri.path.id}",
77 | },
78 | },
79 | {
80 | in: "{query.id}",
81 | want: &Var{
82 | Raw: "{query.id}",
83 | Name: "{http.request.uri.query.id}",
84 | },
85 | },
86 | {
87 | in: "{header.id}",
88 | want: &Var{
89 | Raw: "{header.id}",
90 | Name: "{http.request.header.id}",
91 | },
92 | },
93 | {
94 | in: "{cookie.id}",
95 | want: &Var{
96 | Raw: "{cookie.id}",
97 | Name: "{http.request.cookie.id}",
98 | },
99 | },
100 | {
101 | in: "{remote.host}",
102 | want: &Var{
103 | Raw: "{remote.host}",
104 | Name: "{http.request.remote.host}",
105 | },
106 | },
107 | {
108 | in: "{remote.port}",
109 | want: &Var{
110 | Raw: "{remote.port}",
111 | Name: "{http.request.remote.port}",
112 | },
113 | },
114 | {
115 | in: "{remote.ip}",
116 | want: &Var{
117 | Raw: "{remote.ip}",
118 | Name: "{http.request.remote.ip}",
119 | },
120 | },
121 | {
122 | in: "{remote.host_prefix.24}",
123 | want: &Var{
124 | Raw: "{remote.host_prefix.24}",
125 | Name: "{http.request.remote.host_prefix}",
126 | Bits: 24,
127 | },
128 | },
129 | {
130 | in: "{remote.ip_prefix.64}",
131 | want: &Var{
132 | Raw: "{remote.ip_prefix.64}",
133 | Name: "{http.request.remote.ip_prefix}",
134 | Bits: 64,
135 | },
136 | },
137 | {
138 | in: "{remote.host_prefix}",
139 | wantErrStr: `invalid key variable: "{remote.host_prefix}"`,
140 | },
141 | {
142 | in: "{remote.ip_prefix.xx}",
143 | wantErrStr: `invalid key variable: "{remote.ip_prefix.xx}"`,
144 | },
145 | {
146 | in: "{http.request.uri.path.id}",
147 | want: &Var{
148 | Raw: "{http.request.uri.path.id}",
149 | Name: "{http.request.uri.path.id}",
150 | },
151 | },
152 | {
153 | in: "{unknown.id}",
154 | wantErrStr: `unrecognized key variable: "{unknown.id}"`,
155 | },
156 | {
157 | in: "{unknown}",
158 | wantErrStr: `invalid key variable: "{unknown}"`,
159 | },
160 | }
161 |
162 | for _, c := range cases {
163 | t.Run("", func(t *testing.T) {
164 | v, err := ParseVar(c.in)
165 | if err != nil && err.Error() != c.wantErrStr {
166 | t.Fatalf("ErrStr: got (%#v), want (%#v)", err.Error(), c.wantErrStr)
167 | }
168 | if !reflect.DeepEqual(v, c.want) {
169 | t.Fatalf("Out: got (%#v), want (%#v)", v, c.want)
170 | }
171 | })
172 | }
173 | }
174 |
175 | func TestVar_Evaluate(t *testing.T) {
176 | cases := []struct {
177 | name string
178 | inVar *Var
179 | inReq func() *http.Request
180 | wantValue string
181 | wantErrStr string
182 | }{
183 | {
184 | name: "query",
185 | inVar: &Var{
186 | Name: "{http.request.uri.query.id}",
187 | },
188 | inReq: func() *http.Request {
189 | return httptest.NewRequest(http.MethodGet, "/foo?id=1", nil)
190 | },
191 | wantValue: "1",
192 | },
193 | {
194 | name: "peer ip",
195 | inVar: &Var{
196 | Name: "{http.request.remote.host}",
197 | },
198 | inReq: func() *http.Request {
199 | req := httptest.NewRequest(http.MethodGet, "/", nil)
200 | req.Header.Set("X-Forwarded-For", "192.168.0.1, 192.168.0.2")
201 | return req
202 | },
203 | wantValue: "192.0.2.1",
204 | },
205 | {
206 | name: "peer port",
207 | inVar: &Var{
208 | Name: "{http.request.remote.port}",
209 | },
210 | inReq: func() *http.Request {
211 | return httptest.NewRequest(http.MethodGet, "/", nil)
212 | },
213 | wantValue: "1234",
214 | },
215 | {
216 | name: "forwarded ip",
217 | inVar: &Var{
218 | Name: "{http.request.remote.ip}",
219 | },
220 | inReq: func() *http.Request {
221 | req := httptest.NewRequest(http.MethodGet, "/", nil)
222 | req.Header.Set("X-Forwarded-For", "192.168.0.1, 192.168.0.2")
223 | return req
224 | },
225 | wantValue: "192.168.0.1",
226 | },
227 | {
228 | name: "ipv4 prefix",
229 | inVar: &Var{
230 | Name: "{http.request.remote.ip_prefix}",
231 | Bits: 24,
232 | },
233 | inReq: func() *http.Request {
234 | return httptest.NewRequest(http.MethodGet, "/", nil)
235 | },
236 | wantValue: "192.0.2.0/24",
237 | },
238 | {
239 | name: "ipv6 prefix",
240 | inVar: &Var{
241 | Name: "{http.request.remote.ip_prefix}",
242 | Bits: 64,
243 | },
244 | inReq: func() *http.Request {
245 | req := httptest.NewRequest(http.MethodGet, "/", nil)
246 | req.RemoteAddr = "[2001:db8:85a3:8d3:1319:8a2e:370:7348]:443"
247 | return req
248 | },
249 | wantValue: "2001:db8:85a3:8d3::/64",
250 | },
251 | {
252 | name: "bad ipv4 prefix",
253 | inVar: &Var{
254 | Name: "{http.request.remote.ip_prefix}",
255 | Bits: 64,
256 | },
257 | inReq: func() *http.Request {
258 | return httptest.NewRequest(http.MethodGet, "/", nil)
259 | },
260 | wantErrStr: "prefix length 64 too large for IPv4",
261 | },
262 | }
263 |
264 | for _, c := range cases {
265 | t.Run(c.name, func(t *testing.T) {
266 | // Build the request object.
267 | r := c.inReq()
268 | repl := caddyhttp.NewTestReplacer(r)
269 | ctx := context.WithValue(r.Context(), caddy.ReplacerCtxKey, repl)
270 | req := r.WithContext(ctx)
271 |
272 | value, err := c.inVar.Evaluate(req)
273 | if err != nil && err.Error() != c.wantErrStr {
274 | t.Fatalf("ErrStr: got (%#v), want (%#v)", err.Error(), c.wantErrStr)
275 | }
276 | if value != c.wantValue {
277 | t.Fatalf("Value: got (%#v), want (%#v)", value, c.wantValue)
278 | }
279 | })
280 | }
281 | }
282 |
--------------------------------------------------------------------------------
/flagr/flagr.go:
--------------------------------------------------------------------------------
1 | package flagr
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net/http"
7 | "regexp"
8 | "strings"
9 | "time"
10 |
11 | "github.com/caddyserver/caddy/v2"
12 | "github.com/caddyserver/caddy/v2/modules/caddyhttp"
13 | "github.com/checkr/flagr/swagger_gen/models"
14 | "go.uber.org/zap"
15 |
16 | "github.com/RussellLuo/caddy-ext/flagr/evaluator"
17 | )
18 |
19 | var (
20 | regexpFullVar = regexp.MustCompile(`^\{http\.request\..+\}$`)
21 | regexpShortVar = regexp.MustCompile(`^\{(\w+)\.(.+)\}$`)
22 | )
23 |
24 | func init() {
25 | caddy.RegisterModule(Flagr{})
26 | }
27 |
28 | type Evaluator interface {
29 | PostEvaluationBatch(ctx context.Context, req *models.EvaluationBatchRequest) (*models.EvaluationBatchResponse, error)
30 | }
31 |
32 | type ContextValue struct {
33 | Value interface{}
34 | IsCaddyVar bool
35 | Converters []Converter `json:"-"`
36 | }
37 |
38 | // Flagr implements a handler for applying Feature Flags for HTTP requests
39 | // by using checkr/flagr.
40 | type Flagr struct {
41 | // The address of the flagr server.
42 | URL string `json:"url,omitempty"`
43 |
44 | // Which evaluator to use.
45 | // Supported options: "local" or "remote".
46 | Evaluator string `json:"evaluator,omitempty"`
47 | // The refresh interval of the internal eval cache (only used for the "local" evaluator).
48 | RefreshInterval string `json:"refresh_interval,omitempty"`
49 |
50 | // The unique ID from the entity, which is used to deterministically at
51 | // random to evaluate the flag result. Must be a Caddy variable.
52 | EntityID string `json:"entity_id,omitempty"`
53 |
54 | // The context parameters (key-value pairs) from the entity, which is used
55 | // to match the constraints.
56 | EntityContext map[string]interface{} `json:"entity_context,omitempty"`
57 |
58 | // A list of flag keys to look up.
59 | FlagKeys []string `json:"flag_keys,omitempty"`
60 |
61 | // Which element of the request to bind the evaluated variant keys.
62 | // Supported options: "header.NAME" or "query.NAME".
63 | BindVariantKeysTo string `json:"bind_variant_keys_to,omitempty"`
64 |
65 | logger *zap.Logger
66 | evaluator Evaluator
67 | entityIDVar string
68 | entityContext map[string]ContextValue
69 | bindToLocation string
70 | bindToName string
71 | }
72 |
73 | // CaddyModule returns the Caddy module information.
74 | func (Flagr) CaddyModule() caddy.ModuleInfo {
75 | return caddy.ModuleInfo{
76 | ID: "http.handlers.flagr",
77 | New: func() caddy.Module { return new(Flagr) }, // return a singleton.
78 | }
79 | }
80 |
81 | // Provision implements caddy.Provisioner.
82 | func (f *Flagr) Provision(ctx caddy.Context) (err error) {
83 | f.logger = ctx.Logger(f)
84 | return f.provision()
85 | }
86 |
87 | func (f *Flagr) provision() (err error) {
88 | if f.URL == "" {
89 | return fmt.Errorf("empty url")
90 | }
91 |
92 | refreshInterval := 10 * time.Second
93 | if f.RefreshInterval != "" {
94 | d, err := time.ParseDuration(f.RefreshInterval)
95 | if err != nil {
96 | return err
97 | }
98 | refreshInterval = d
99 | }
100 |
101 | if f.Evaluator == "" {
102 | f.Evaluator = "local"
103 | }
104 | switch f.Evaluator {
105 | case "local":
106 | f.evaluator, err = evaluator.NewLocal(refreshInterval, f.URL)
107 | if err != nil {
108 | return err
109 | }
110 | case "remote":
111 | f.evaluator = evaluator.NewRemote(f.URL)
112 | default:
113 | return fmt.Errorf("unsupported evaluator %q", f.Evaluator)
114 | }
115 |
116 | if f.EntityID != "" {
117 | f.entityIDVar, err = parseVar(f.EntityID)
118 | if err != nil {
119 | return err
120 | }
121 | }
122 |
123 | f.entityContext = make(map[string]ContextValue)
124 | for k, v := range f.EntityContext {
125 | cv := ContextValue{Value: v}
126 |
127 | // Handle string values specially.
128 | if s, ok := v.(string); ok {
129 | parts := strings.Split(s, "|")
130 | val := parts[0]
131 |
132 | if p, err := parseVar(val); err == nil {
133 | cv.Value = p
134 | cv.IsCaddyVar = true
135 | } else {
136 | cv.Value = val
137 | }
138 |
139 | for _, name := range parts[1:] {
140 | c, err := GetConverter(name)
141 | if err != nil {
142 | return err
143 | }
144 | cv.Converters = append(cv.Converters, c)
145 | }
146 | }
147 |
148 | f.entityContext[k] = cv
149 | }
150 |
151 | if f.BindVariantKeysTo == "" {
152 | f.BindVariantKeysTo = "header.X-Flagr-Variant"
153 | }
154 |
155 | parts := strings.SplitN(f.BindVariantKeysTo, ".", 2)
156 | if len(parts) != 2 {
157 | return fmt.Errorf("invalid bind_variant_key_to")
158 | }
159 | f.bindToLocation, f.bindToName = parts[0], parts[1]
160 |
161 | return nil
162 | }
163 |
164 | // Cleanup cleans up the resources made by rl during provisioning.
165 | func (f *Flagr) Cleanup() error {
166 | return nil
167 | }
168 |
169 | // Validate implements caddy.Validator.
170 | func (f *Flagr) Validate() error {
171 | if len(f.entityContext) == 0 {
172 | return fmt.Errorf("invalid entity_context")
173 | }
174 | if len(f.FlagKeys) == 0 {
175 | return fmt.Errorf("empty flag_keys")
176 | }
177 |
178 | if f.bindToLocation != "header" && f.bindToLocation != "query" {
179 | return fmt.Errorf("invalid location %q from bind_variant_key_to", f.bindToLocation)
180 | }
181 | if f.bindToName == "" {
182 | return fmt.Errorf("emtpy name from bind_variant_key_to")
183 | }
184 |
185 | return nil
186 | }
187 |
188 | // ServeHTTP implements caddyhttp.MiddlewareHandler.
189 | func (f *Flagr) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
190 | repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
191 | entityID := repl.ReplaceAll(f.entityIDVar, "")
192 | if f.entityIDVar != "" && entityID == "" {
193 | f.logger.Info("entityID is evaluated to be empty",
194 | zap.Any("entityIDVar", f.entityIDVar),
195 | zap.Any("originalEntityContext", f.EntityContext),
196 | )
197 | return next.ServeHTTP(w, r)
198 | }
199 |
200 | entityContext, err := evalEntityContext(f.entityContext, repl)
201 | if err != nil {
202 | f.logger.Error("failed to evaluate the entity context",
203 | zap.String("entityID", entityID),
204 | zap.Any("originalEntityContext", f.EntityContext),
205 | zap.Error(err),
206 | )
207 | return next.ServeHTTP(w, r)
208 | }
209 |
210 | f.logger.Debug("ready to evaluate the request entity by Flagr",
211 | zap.String("entityID", entityID),
212 | zap.Any("entityContext", entityContext),
213 | )
214 |
215 | resp, err := f.evaluator.PostEvaluationBatch(context.Background(), &models.EvaluationBatchRequest{
216 | Entities: []*models.EvaluationEntity{
217 | {
218 | EntityID: entityID,
219 | EntityContext: entityContext,
220 | },
221 | },
222 | FlagKeys: f.FlagKeys,
223 | })
224 | if err != nil {
225 | f.logger.Error("failed to evaluate the request entity by Flagr",
226 | zap.String("entityID", entityID),
227 | zap.Any("originalEntityContext", f.EntityContext),
228 | zap.Error(err),
229 | )
230 | return next.ServeHTTP(w, r)
231 | }
232 |
233 | for _, er := range resp.EvaluationResults {
234 | if er.VariantKey != "" {
235 | variant := er.FlagKey + "." + er.VariantKey
236 | switch f.bindToLocation {
237 | case "header":
238 | r.Header.Add(f.bindToName, variant)
239 | case "query":
240 | r.URL.Query().Add(f.bindToName, variant)
241 | }
242 | }
243 | }
244 |
245 | return next.ServeHTTP(w, r)
246 | }
247 |
248 | func evalEntityContext(entityCtx map[string]ContextValue, repl *caddy.Replacer) (interface{}, error) {
249 | out := make(map[string]interface{})
250 | for k, cv := range entityCtx {
251 | v := cv.Value
252 | if cv.IsCaddyVar {
253 | // Use evaluated values for placeholders.
254 | v = repl.ReplaceAll(cv.Value.(string), "")
255 | }
256 | out[k] = v
257 |
258 | // If v is of type string, convert it by a list of converters, if any.
259 | if s, ok := v.(string); ok {
260 | for _, c := range cv.Converters {
261 | r, err := c(s)
262 | if err != nil {
263 | return nil, err
264 | }
265 | out[k] = r
266 | }
267 | }
268 | }
269 | return out, nil
270 | }
271 |
272 | // parseVar transforms shorthand variables into Caddy-style placeholders.
273 | // Copied from ratelimit/ratelimit.go.
274 | func parseVar(s string) (v string, err error) {
275 | if regexpFullVar.MatchString(s) {
276 | // If the variable is already a fully-qualified Caddy placeholder,
277 | // return it as is.
278 | return s, nil
279 | }
280 |
281 | result := regexpShortVar.FindStringSubmatch(s)
282 | if len(result) != 3 {
283 | return "", fmt.Errorf("invalid key variable: %q", s)
284 | }
285 | location, name := result[1], result[2]
286 |
287 | switch location {
288 | case "path":
289 | v = fmt.Sprintf("{http.request.uri.path.%s}", name)
290 | case "query":
291 | v = fmt.Sprintf("{http.request.uri.query.%s}", name)
292 | case "header":
293 | v = fmt.Sprintf("{http.request.header.%s}", name)
294 | case "cookie":
295 | v = fmt.Sprintf("{http.request.cookie.%s}", name)
296 | case "body":
297 | v = fmt.Sprintf("{http.request.body.%s}", name)
298 | default:
299 | err = fmt.Errorf("unrecognized key variable: %q", s)
300 | }
301 |
302 | return
303 | }
304 |
305 | // Interface guards
306 | var (
307 | _ caddy.Provisioner = (*Flagr)(nil)
308 | _ caddy.CleanerUpper = (*Flagr)(nil)
309 | _ caddy.Validator = (*Flagr)(nil)
310 | _ caddyhttp.MiddlewareHandler = (*Flagr)(nil)
311 | )
312 |
--------------------------------------------------------------------------------
/ratelimit/ratelimit.go:
--------------------------------------------------------------------------------
1 | package ratelimit
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "net/http"
7 | "net/netip"
8 | "regexp"
9 | "strconv"
10 | "strings"
11 | "time"
12 |
13 | "github.com/caddyserver/caddy/v2"
14 | "github.com/caddyserver/caddy/v2/modules/caddyhttp"
15 | "go.uber.org/zap"
16 | )
17 |
18 | var (
19 | regexpFullVar = regexp.MustCompile(`^\{http\.request\..+\}$`)
20 | regexpShortVar = regexp.MustCompile(`^\{(\w+)\.(.+)\}$`)
21 | // "host_prefix." or "ip_prefix."
22 | regexpPrefixVar = regexp.MustCompile(`^(host_prefix|ip_prefix)\.([0-9]+)$`)
23 | regexpRate = regexp.MustCompile(`^(\d+)r/(s|m)$`)
24 | )
25 |
26 | func init() {
27 | caddy.RegisterModule(RateLimit{})
28 | }
29 |
30 | // RateLimit implements a handler for rate-limiting.
31 | //
32 | // If a client exceeds the rate limit, an HTTP error with status `` will
33 | // be returned. This error can be handled using the conventional error handlers.
34 | // See [handle_errors](https://caddyserver.com/docs/caddyfile/directives/handle_errors)
35 | // for how to set up error handlers.
36 | type RateLimit struct {
37 | // The variable used to differentiate one client from another.
38 | //
39 | // Currently supported variables:
40 | //
41 | // - `{path.}`
42 | // - `{query.}`
43 | // - `{header.}`
44 | // - `{cookie.}`
45 | // - `{body.}` (requires the [requestbodyvar](https://github.com/RussellLuo/caddy-ext/tree/master/requestbodyvar) extension)
46 | // - `{remote.host}` (ignores the `X-Forwarded-For` header)
47 | // - `{remote.port}`
48 | // - `{remote.ip}` (prefers the first IP in the `X-Forwarded-For` header)
49 | // - `{remote.host_prefix.}` (CIDR block version of `{remote.host}`)
50 | // - `{remote.ip_prefix.}` (CIDR block version of `{remote.ip}`)
51 | Key string `json:"key,omitempty"`
52 |
53 | // The request rate limit (per key value) specified in requests
54 | // per second (r/s) or requests per minute (r/m).
55 | Rate string `json:"rate,omitempty"`
56 |
57 | // The size (i.e. the number of key values) of the LRU zone that
58 | // keeps states of these key values. Defaults to 10,000.
59 | ZoneSize int `json:"zone_size,omitempty"`
60 |
61 | // The HTTP status code of the response when a client exceeds the rate.
62 | // Defaults to 429 (Too Many Requests).
63 | RejectStatusCode int `json:"reject_status,omitempty"`
64 |
65 | keyVar *Var
66 | zone *Zone
67 |
68 | logger *zap.Logger
69 | }
70 |
71 | // CaddyModule returns the Caddy module information.
72 | func (RateLimit) CaddyModule() caddy.ModuleInfo {
73 | return caddy.ModuleInfo{
74 | ID: "http.handlers.rate_limit",
75 | New: func() caddy.Module { return new(RateLimit) },
76 | }
77 | }
78 |
79 | // Provision implements caddy.Provisioner.
80 | func (rl *RateLimit) Provision(ctx caddy.Context) (err error) {
81 | rl.logger = ctx.Logger(rl)
82 | return rl.provision()
83 | }
84 |
85 | func (rl *RateLimit) provision() (err error) {
86 | rl.keyVar, err = ParseVar(rl.Key)
87 | if err != nil {
88 | return err
89 | }
90 |
91 | rateSize, rateLimit, err := parseRate(rl.Rate)
92 | if err != nil {
93 | return err
94 | }
95 |
96 | if rl.ZoneSize == 0 {
97 | rl.ZoneSize = 10000 // At most 10,000 keys by default
98 | }
99 |
100 | rl.zone, err = NewZone(rl.ZoneSize, rateSize, int64(rateLimit))
101 | if err != nil {
102 | return err
103 | }
104 |
105 | if rl.RejectStatusCode == 0 {
106 | rl.RejectStatusCode = http.StatusTooManyRequests
107 | }
108 |
109 | return nil
110 | }
111 |
112 | // Cleanup cleans up the resources made by rl during provisioning.
113 | func (rl *RateLimit) Cleanup() error {
114 | if rl.zone != nil {
115 | rl.zone.Purge()
116 | }
117 | return nil
118 | }
119 |
120 | // Validate implements caddy.Validator.
121 | func (rl *RateLimit) Validate() error {
122 | if rl.keyVar == nil {
123 | return fmt.Errorf("no key variable")
124 | }
125 | if rl.zone == nil {
126 | return fmt.Errorf("no zone created")
127 | }
128 | if http.StatusText(rl.RejectStatusCode) == "" {
129 | return fmt.Errorf("unknown code reject_status: %d", rl.RejectStatusCode)
130 | }
131 | return nil
132 | }
133 |
134 | // ServeHTTP implements caddyhttp.MiddlewareHandler.
135 | func (rl *RateLimit) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
136 | keyValue, err := rl.keyVar.Evaluate(r)
137 | if err != nil {
138 | rl.logger.Error("failed to evaluate variable",
139 | zap.String("variable", rl.keyVar.Raw),
140 | zap.Error(err),
141 | )
142 | return next.ServeHTTP(w, r)
143 | }
144 |
145 | w.Header().Add("RateLimit-Policy", rl.zone.RateLimitPolicyHeader())
146 |
147 | if keyValue != "" && !rl.zone.Allow(keyValue) {
148 | rl.logger.Debug("request is rejected",
149 | zap.String("variable", rl.keyVar.Raw),
150 | zap.String("value", keyValue),
151 | )
152 |
153 | w.WriteHeader(rl.RejectStatusCode)
154 | // Return an error to invoke possible error handlers.
155 | return caddyhttp.Error(rl.RejectStatusCode, nil)
156 | }
157 |
158 | return next.ServeHTTP(w, r)
159 | }
160 |
161 | type Var struct {
162 | Raw string
163 | Name string
164 | Bits int
165 | }
166 |
167 | // ParseVar transforms shorthand variables into Caddy-style placeholders.
168 | //
169 | // Examples for shorthand variables:
170 | //
171 | // - `{path.}`
172 | // - `{query.}`
173 | // - `{header.}`
174 | // - `{cookie.}`
175 | // - `{body.}`
176 | // - `{remote.host}`
177 | // - `{remote.port}`
178 | // - `{remote.ip}`
179 | // - `{remote.host_prefix.}`
180 | // - `{remote.ip_prefix.}`
181 | func ParseVar(s string) (*Var, error) {
182 | v := &Var{Raw: s}
183 | if regexpFullVar.MatchString(s) {
184 | // If the variable is already a fully-qualified Caddy placeholder,
185 | // return it as is.
186 | v.Name = s
187 | return v, nil
188 | }
189 |
190 | result := regexpShortVar.FindStringSubmatch(s)
191 | if len(result) != 3 {
192 | return nil, fmt.Errorf("invalid key variable: %q", s)
193 | }
194 | location, name := result[1], result[2]
195 |
196 | switch location {
197 | case "path":
198 | v.Name = fmt.Sprintf("{http.request.uri.path.%s}", name)
199 | case "query":
200 | v.Name = fmt.Sprintf("{http.request.uri.query.%s}", name)
201 | case "header":
202 | v.Name = fmt.Sprintf("{http.request.header.%s}", name)
203 | case "cookie":
204 | v.Name = fmt.Sprintf("{http.request.cookie.%s}", name)
205 | case "body":
206 | v.Name = fmt.Sprintf("{http.request.body.%s}", name)
207 | case "remote":
208 | if name == "host" || name == "port" || name == "ip" {
209 | v.Name = fmt.Sprintf("{http.request.remote.%s}", name)
210 | return v, nil
211 | }
212 |
213 | r := regexpPrefixVar.FindStringSubmatch(name)
214 | if len(r) != 3 {
215 | return nil, fmt.Errorf("invalid key variable: %q", s)
216 | }
217 |
218 | v.Name = fmt.Sprintf("{http.request.remote.%s}", r[1])
219 |
220 | if r[2] == "" {
221 | return nil, fmt.Errorf("invalid key variable: %q", s)
222 | }
223 | bits, err := strconv.Atoi(r[2])
224 | if err != nil {
225 | return nil, err
226 | }
227 | v.Bits = bits
228 | default:
229 | return nil, fmt.Errorf("unrecognized key variable: %q", s)
230 | }
231 |
232 | return v, nil
233 | }
234 |
235 | func (v *Var) Evaluate(r *http.Request) (value string, err error) {
236 | switch v.Name {
237 | case "{http.request.remote.ip}":
238 | ip, err := getClientIP(r, true)
239 | if err != nil {
240 | return "", err
241 | }
242 | return ip.String(), nil
243 | case "{http.request.remote.host_prefix}":
244 | return v.evaluatePrefix(r, false)
245 | case "{http.request.remote.ip_prefix}":
246 | return v.evaluatePrefix(r, true)
247 | default:
248 | repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
249 | value = repl.ReplaceAll(v.Name, "")
250 | return value, nil
251 | }
252 | }
253 |
254 | func (v *Var) evaluatePrefix(r *http.Request, forwarded bool) (value string, err error) {
255 | ip, err := getClientIP(r, forwarded)
256 | if err != nil {
257 | return "", err
258 | }
259 | prefix, err := ip.Prefix(v.Bits)
260 | if err != nil {
261 | return "", err
262 | }
263 | return prefix.Masked().String(), nil
264 | }
265 |
266 | func getClientIP(r *http.Request, forwarded bool) (netip.Addr, error) {
267 | remote := r.RemoteAddr
268 | if forwarded {
269 | if fwdFor := r.Header.Get("X-Forwarded-For"); fwdFor != "" {
270 | remote = strings.TrimSpace(strings.Split(fwdFor, ",")[0])
271 | }
272 | }
273 | ipStr, _, err := net.SplitHostPort(remote)
274 | if err != nil {
275 | ipStr = remote // OK; probably didn't have a port
276 | }
277 | return netip.ParseAddr(ipStr)
278 | }
279 |
280 | func parseRate(rate string) (size time.Duration, limit int, err error) {
281 | if rate == "" {
282 | return 0, 0, fmt.Errorf("missing rate")
283 | }
284 |
285 | result := regexpRate.FindStringSubmatch(rate)
286 | if len(result) != 3 {
287 | return 0, 0, fmt.Errorf("invalid rate: %s", rate)
288 | }
289 | limitStr, sizeStr := result[1], result[2]
290 |
291 | switch sizeStr {
292 | case "s":
293 | size = time.Second
294 | case "m":
295 | size = time.Minute
296 | }
297 |
298 | limit, err = strconv.Atoi(limitStr)
299 | if err != nil {
300 | return 0, 0, fmt.Errorf("size-limit must be an integer; invalid: %v", err)
301 | }
302 |
303 | return
304 | }
305 |
306 | // Interface guards
307 | var (
308 | _ caddy.Provisioner = (*RateLimit)(nil)
309 | _ caddy.CleanerUpper = (*RateLimit)(nil)
310 | _ caddy.Validator = (*RateLimit)(nil)
311 | _ caddyhttp.MiddlewareHandler = (*RateLimit)(nil)
312 | )
313 |
--------------------------------------------------------------------------------
/layer4/caddyfile.go:
--------------------------------------------------------------------------------
1 | package layer4
2 |
3 | import (
4 | "encoding/json"
5 | "strconv"
6 |
7 | "github.com/caddyserver/caddy/v2"
8 | "github.com/caddyserver/caddy/v2/caddyconfig"
9 | "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
10 | "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
11 | "github.com/mholt/caddy-l4/layer4"
12 | "github.com/mholt/caddy-l4/modules/l4echo"
13 | "github.com/mholt/caddy-l4/modules/l4proxy"
14 | "github.com/mholt/caddy-l4/modules/l4proxyprotocol"
15 | "github.com/mholt/caddy-l4/modules/l4tls"
16 | )
17 |
18 | func init() {
19 | httpcaddyfile.RegisterGlobalOption("layer4", parseLayer4)
20 | }
21 |
22 | // parseLayer4 sets up the "layer4" global option from Caddyfile tokens. Syntax:
23 | //
24 | // layer4 {
25 | // {
26 | // l4echo
27 | // }
28 | //
29 | // {
30 | // l4proxy []
31 | // }
32 | // }
33 | //
34 | func parseLayer4(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
35 | app := &layer4.App{Servers: make(map[string]*layer4.Server)}
36 |
37 | for d.Next() {
38 | for i := 0; d.NextBlock(0); i++ {
39 | server := new(layer4.Server)
40 |
41 | server.Listen = append(server.Listen, d.Val())
42 | for _, arg := range d.RemainingArgs() {
43 | server.Listen = append(server.Listen, arg)
44 | }
45 |
46 | for d.NextBlock(1) {
47 | switch d.Val() {
48 | case "l4echo", "echo":
49 | server.Routes = append(server.Routes, &layer4.Route{
50 | HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(new(l4echo.Handler), "handler", "echo", nil)},
51 | })
52 |
53 | case "proxy_protocol":
54 | handler, err := parseProxyProtocol(d)
55 | if err != nil {
56 | return nil, err
57 | }
58 | server.Routes = append(server.Routes, &layer4.Route{
59 | HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(handler, "handler", "proxy_protocol", nil)},
60 | })
61 |
62 | case "l4proxy", "proxy":
63 | handler, err := parseProxy(d)
64 | if err != nil {
65 | return nil, err
66 | }
67 | server.Routes = append(server.Routes, &layer4.Route{
68 | HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(handler, "handler", "proxy", nil)},
69 | })
70 | case "l4tls", "tls":
71 | handler, err := parseTLS(d)
72 | if err != nil {
73 | return nil, err
74 | }
75 | server.Routes = append(server.Routes, &layer4.Route{
76 | HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(handler, "handler", "tls", nil)},
77 | })
78 | }
79 | }
80 |
81 | app.Servers["srv"+strconv.Itoa(i)] = server
82 | }
83 | }
84 |
85 | // tell Caddyfile adapter that this is the JSON for an app
86 | return httpcaddyfile.App{
87 | Name: "layer4",
88 | Value: caddyconfig.JSON(app, nil),
89 | }, nil
90 | }
91 |
92 | // parseTLS sets up a "tls" handler from Caddyfile tokens. The implementation does
93 | // not support `connection_policies` because it does not implement `caddyfile.Unmarshaler`. Syntax:
94 | //
95 | // tls
96 | func parseTLS(_ *caddyfile.Dispenser) (*l4tls.Handler, error) {
97 | h := new(l4tls.Handler)
98 | return h, nil
99 | }
100 |
101 | // parseProxyProtocol sets up a "proxy_protocol" handler from Caddyfile tokens. Syntax:
102 | //
103 | // proxy_protocol {
104 | // timeout
105 | // allow
106 | // }
107 | //
108 | func parseProxyProtocol(d *caddyfile.Dispenser) (*l4proxyprotocol.Handler, error) {
109 | h := new(l4proxyprotocol.Handler)
110 |
111 | // No same-line options are supported
112 | if len(d.RemainingArgs()) > 0 {
113 | return nil, d.ArgErr()
114 | }
115 |
116 | for nesting := d.Nesting(); d.NextBlock(nesting); {
117 | switch d.Val() {
118 | case "timeout":
119 | if !d.NextArg() {
120 | return nil, d.ArgErr()
121 | }
122 | timeout, err := caddy.ParseDuration(d.Val())
123 | if err != nil {
124 | return nil, d.Errf("parsing proxy_protocol timeout duration: %v", err)
125 | }
126 | h.Timeout = caddy.Duration(timeout)
127 |
128 | case "allow":
129 | args := d.RemainingArgs()
130 | if len(args) == 0 {
131 | return nil, d.ArgErr()
132 | }
133 | h.Allow = append(h.Allow, args...)
134 |
135 | default:
136 | return nil, d.ArgErr()
137 | }
138 | }
139 |
140 | return h, nil
141 | }
142 |
143 | // parseL4proxy sets up a "proxy" handler from Caddyfile tokens. Syntax:
144 | //
145 | // proxy [] {
146 | // # backends
147 | // to
148 | // ...
149 | //
150 | // # load balancing
151 | // lb_policy []
152 | // lb_try_duration
153 | // lb_try_interval
154 | //
155 | // # active health checking
156 | // health_port
157 | // health_interval
158 | // health_timeout
159 | //
160 | // # sending the PROXY protocol
161 | // proxy_protocol
162 | // }
163 | //
164 | func parseProxy(d *caddyfile.Dispenser) (*l4proxy.Handler, error) {
165 | h := new(l4proxy.Handler)
166 |
167 | appendUpstream := func(addresses ...string) {
168 | for _, addr := range addresses {
169 | h.Upstreams = append(h.Upstreams, &l4proxy.Upstream{
170 | Dial: []string{addr},
171 | })
172 | }
173 | }
174 |
175 | appendUpstream(d.RemainingArgs()...)
176 |
177 | for nesting := d.Nesting(); d.NextBlock(nesting); {
178 | switch d.Val() {
179 | case "to":
180 | args := d.RemainingArgs()
181 | if len(args) == 0 {
182 | return nil, d.ArgErr()
183 | }
184 | appendUpstream(args...)
185 |
186 | case "lb_policy":
187 | if !d.NextArg() {
188 | return nil, d.ArgErr()
189 | }
190 | if h.LoadBalancing != nil && h.LoadBalancing.SelectionPolicyRaw != nil {
191 | return nil, d.Err("load balancing selection policy already specified")
192 | }
193 | if h.LoadBalancing == nil {
194 | h.LoadBalancing = new(l4proxy.LoadBalancing)
195 | }
196 |
197 | name := d.Val()
198 | modID := "layer4.proxy.selection_policies." + name
199 | mod, err := UnmarshalL4proxySelectionModule(d, modID)
200 | if err != nil {
201 | return nil, err
202 | }
203 |
204 | sel, ok := mod.(l4proxy.Selector)
205 | if !ok {
206 | return nil, d.Errf("module %s (%T) is not a l4proxy.Selector", modID, mod)
207 | }
208 | h.LoadBalancing.SelectionPolicyRaw = caddyconfig.JSONModuleObject(sel, "policy", name, nil)
209 |
210 | case "lb_try_duration":
211 | if !d.NextArg() {
212 | return nil, d.ArgErr()
213 | }
214 | if h.LoadBalancing == nil {
215 | h.LoadBalancing = new(l4proxy.LoadBalancing)
216 | }
217 |
218 | dur, err := caddy.ParseDuration(d.Val())
219 | if err != nil {
220 | return nil, d.Errf("bad duration value %s: %v", d.Val(), err)
221 | }
222 | h.LoadBalancing.TryDuration = caddy.Duration(dur)
223 |
224 | case "lb_try_interval":
225 | if !d.NextArg() {
226 | return nil, d.ArgErr()
227 | }
228 | if h.LoadBalancing == nil {
229 | h.LoadBalancing = new(l4proxy.LoadBalancing)
230 | }
231 |
232 | dur, err := caddy.ParseDuration(d.Val())
233 | if err != nil {
234 | return nil, d.Errf("bad interval value '%s': %v", d.Val(), err)
235 | }
236 | h.LoadBalancing.TryInterval = caddy.Duration(dur)
237 |
238 | case "health_port":
239 | if !d.NextArg() {
240 | return nil, d.ArgErr()
241 | }
242 | if h.HealthChecks == nil {
243 | h.HealthChecks = new(l4proxy.HealthChecks)
244 | }
245 | if h.HealthChecks.Active == nil {
246 | h.HealthChecks.Active = new(l4proxy.ActiveHealthChecks)
247 | }
248 |
249 | portNum, err := strconv.Atoi(d.Val())
250 | if err != nil {
251 | return nil, d.Errf("bad port number '%s': %v", d.Val(), err)
252 | }
253 | h.HealthChecks.Active.Port = portNum
254 |
255 | case "health_interval":
256 | if !d.NextArg() {
257 | return nil, d.ArgErr()
258 | }
259 | if h.HealthChecks == nil {
260 | h.HealthChecks = new(l4proxy.HealthChecks)
261 | }
262 | if h.HealthChecks.Active == nil {
263 | h.HealthChecks.Active = new(l4proxy.ActiveHealthChecks)
264 | }
265 |
266 | dur, err := caddy.ParseDuration(d.Val())
267 | if err != nil {
268 | return nil, d.Errf("bad interval value %s: %v", d.Val(), err)
269 | }
270 | h.HealthChecks.Active.Interval = caddy.Duration(dur)
271 |
272 | case "health_timeout":
273 | if !d.NextArg() {
274 | return nil, d.ArgErr()
275 | }
276 | if h.HealthChecks == nil {
277 | h.HealthChecks = new(l4proxy.HealthChecks)
278 | }
279 | if h.HealthChecks.Active == nil {
280 | h.HealthChecks.Active = new(l4proxy.ActiveHealthChecks)
281 | }
282 |
283 | dur, err := caddy.ParseDuration(d.Val())
284 | if err != nil {
285 | return nil, d.Errf("bad timeout value %s: %v", d.Val(), err)
286 | }
287 | h.HealthChecks.Active.Timeout = caddy.Duration(dur)
288 |
289 | case "proxy_protocol":
290 | if !d.NextArg() {
291 | return nil, d.ArgErr()
292 | }
293 | h.ProxyProtocol = d.Val()
294 | }
295 | }
296 |
297 | return h, nil
298 | }
299 |
300 | // UnmarshalL4proxySelectionModule is like `caddyfile.UnmarshalModule`, but for
301 | // l4proxy's selection modules, which do not implement `caddyfile.Unmarshaler` yet.
302 | func UnmarshalL4proxySelectionModule(d *caddyfile.Dispenser, moduleID string) (caddy.Module, error) {
303 | mod, err := caddy.GetModule(moduleID)
304 | if err != nil {
305 | return nil, d.Errf("getting module named '%s': %v", moduleID, err)
306 | }
307 | inst := mod.New()
308 |
309 | if err = UnmarshalL4ProxySelectionCaddyfile(inst, d.NewFromNextSegment()); err != nil {
310 | return nil, err
311 | }
312 | return inst, nil
313 | }
314 |
315 | func UnmarshalL4ProxySelectionCaddyfile(inst caddy.Module, d *caddyfile.Dispenser) error {
316 | switch sel := inst.(type) {
317 | case *l4proxy.RandomSelection,
318 | *l4proxy.LeastConnSelection,
319 | *l4proxy.RoundRobinSelection,
320 | *l4proxy.FirstSelection,
321 | *l4proxy.IPHashSelection:
322 |
323 | for d.Next() {
324 | if d.NextArg() {
325 | return d.ArgErr()
326 | }
327 | }
328 |
329 | case *l4proxy.RandomChoiceSelection:
330 | for d.Next() {
331 | if !d.NextArg() {
332 | return d.ArgErr()
333 | }
334 | chooseStr := d.Val()
335 | choose, err := strconv.Atoi(chooseStr)
336 | if err != nil {
337 | return d.Errf("invalid choice value '%s': %v", chooseStr, err)
338 | }
339 | sel.Choose = choose
340 | }
341 | }
342 |
343 | return nil
344 | }
345 |
--------------------------------------------------------------------------------