diff --git a/.traefik.yml b/.traefik.yml new file mode 100644 index 0000000..fca315d --- /dev/null +++ b/.traefik.yml @@ -0,0 +1,6 @@ +displayName: ESI +type: middleware +import: github.com/darkweak/go-esi/middleware/traefik +summary: 'Pure implementation of the ESI process' + +testData: {} \ No newline at end of file diff --git a/README.md b/README.md index 5a910ca..3bf574c 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ func functionToParseESITags(b []byte, r *http.Request) []byte { ## Available as middleware - [x] Caddy +- [x] Træfik ### Caddy middleware ```bash @@ -52,6 +53,34 @@ xcaddy build --with github.com/darkweak/go-esi/middleware/caddy ``` Refer to the [sample Caddyfile](https://github.com/darkweak/go-esi/blob/master/middleware/caddy/Caddyfile) to know how to configure that. +### Træfik middleware +```bash +# anywhere/traefik.yml +experimental: + plugins: + souin: + moduleName: github.com/darkweak/go-esi + version: v0.0.4 +``` +```bash +# anywhere/dynamic-configuration +http: + routers: + whoami: + middlewares: + - esi + service: whoami + rule: Host(`domain.com`) + middlewares: + esi: + plugin: + esi: + # We don't care about the configuration but we have ot declare that block + # due to shitty træfik empty configuration handle. + disable: false +``` +Refer to the [sample Caddyfile](https://github.com/darkweak/go-esi/blob/master/middleware/caddy/Caddyfile) to know how to configure that. + ## TODO - [x] choose tag - [x] comment tag diff --git a/esi/include.go b/esi/include.go index 238a14a..dcdf95b 100644 --- a/esi/include.go +++ b/esi/include.go @@ -53,11 +53,12 @@ func (i *includeTag) process(b []byte, req *http.Request) ([]byte, int) { } rq, _ := http.NewRequest(http.MethodGet, i.src, nil) - response, err := clientPool.Get().(*http.Client).Do(rq) + client := &http.Client{} + response, err := client.Do(rq) if err != nil || response.StatusCode >= 400 { rq, _ = http.NewRequest(http.MethodGet, i.src, nil) - response, err = clientPool.Get().(*http.Client).Do(rq) + response, err = client.Do(rq) if err != nil || response.StatusCode >= 400 { return nil, len(b) diff --git a/esi/type.go b/esi/type.go index 40892ed..232f2f8 100644 --- a/esi/type.go +++ b/esi/type.go @@ -2,15 +2,8 @@ package esi import ( "net/http" - "sync" ) -var clientPool = &sync.Pool{ - New: func() any { - return &http.Client{} - }, -} - type ( tag interface { process([]byte, *http.Request) ([]byte, int) diff --git a/middleware/caddy/go.mod b/middleware/caddy/go.mod index a1291fe..d5763e2 100644 --- a/middleware/caddy/go.mod +++ b/middleware/caddy/go.mod @@ -4,7 +4,7 @@ go 1.18 require ( github.com/caddyserver/caddy/v2 v2.5.2 - github.com/darkweak/go-esi v0.0.3 + github.com/darkweak/go-esi v0.0.4 ) require ( @@ -111,4 +111,4 @@ require ( howett.net/plist v1.0.0 // indirect ) -replace github.com/darkweak/go-esi v0.0.3 => ../.. +replace github.com/darkweak/go-esi v0.0.4 => ../.. diff --git a/middleware/traefik/docker-compose.yml b/middleware/traefik/docker-compose.yml new file mode 100644 index 0000000..e0ef1f2 --- /dev/null +++ b/middleware/traefik/docker-compose.yml @@ -0,0 +1,20 @@ +version: '3.8' + +services: + traefik: + image: traefik:latest + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ../..:/plugins-local/src/github.com/darkweak/go-esi + - ./traefik.yml:/traefik.yml + - ./esi-configuration.yml:/esi-configuration.yml + environment: + GOPATH: /plugins-local + ports: + - 80:80 + - 8080:8080 + + whoami: + image: traefik/whoami + labels: + - traefik.http.routers.whoami.rule=Host(`domain.com`) \ No newline at end of file diff --git a/middleware/traefik/esi-configuration.yml b/middleware/traefik/esi-configuration.yml new file mode 100644 index 0000000..40cd7b2 --- /dev/null +++ b/middleware/traefik/esi-configuration.yml @@ -0,0 +1,24 @@ +http: + routers: + whoami: + middlewares: + - esi + entrypoints: + - http + service: whoami + rule: Host(`domain.com`) + + services: + whoami: + loadBalancer: + servers: + - url: http://whoami + passHostHeader: false + + middlewares: + esi: + plugin: + esi: + # We don't care about the configuration but we have ot declare that block + # due to shitty træfik empty configuration handle. + disable: false \ No newline at end of file diff --git a/middleware/traefik/esi.go b/middleware/traefik/esi.go new file mode 100644 index 0000000..7145238 --- /dev/null +++ b/middleware/traefik/esi.go @@ -0,0 +1,35 @@ +package traefik + +import ( + "context" + "net/http" + + "github.com/darkweak/go-esi/esi" +) + +// Config the plugin configuration. +type Config struct{} + +// CreateConfig creates the default plugin configuration. +func CreateConfig() *Config { + return &Config{} +} + +// Demo a Demo plugin. +type Demo struct { + next http.Handler + name string +} + +// New created a new Demo plugin. +func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) { + return &Demo{ + next: next, + name: name, + }, nil +} + +func (a *Demo) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + esi.Parse([]byte{}, req) + a.next.ServeHTTP(rw, req) +} diff --git a/middleware/traefik/go.mod b/middleware/traefik/go.mod new file mode 100644 index 0000000..e6747d7 --- /dev/null +++ b/middleware/traefik/go.mod @@ -0,0 +1,7 @@ +module github.com/darkweak/go-esi/middleware/traefik + +go 1.18 + +require github.com/darkweak/go-esi v0.0.4 + +replace github.com/darkweak/go-esi v0.0.4 => ../.. diff --git a/middleware/traefik/go.sum b/middleware/traefik/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/middleware/traefik/traefik.yml b/middleware/traefik/traefik.yml new file mode 100644 index 0000000..5365086 --- /dev/null +++ b/middleware/traefik/traefik.yml @@ -0,0 +1,22 @@ +providers: + file: + filename: /esi-configuration.yml + watch: true + +api: + dashboard: true + debug: true + insecure: true + +pilot: + token: 12926953-a7d1-4223-a092-d7dd95a91fa3 + +experimental: + localPlugins: + esi: + moduleName: github.com/darkweak/go-esi + +log: + level: DEBUG + +accessLog: {} \ No newline at end of file diff --git a/middleware/traefik/vendor/github.com/darkweak/go-esi/LICENSE b/middleware/traefik/vendor/github.com/darkweak/go-esi/LICENSE new file mode 100644 index 0000000..78e9611 --- /dev/null +++ b/middleware/traefik/vendor/github.com/darkweak/go-esi/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 darkweak + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/choose.go b/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/choose.go new file mode 100644 index 0000000..38a24f7 --- /dev/null +++ b/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/choose.go @@ -0,0 +1,58 @@ +package esi + +import ( + "net/http" + "regexp" +) + +const choose = "choose" + +var ( + closeChoose = regexp.MustCompile("") + whenRg = regexp.MustCompile(`(?s)(.+?)`) + otherwiseRg = regexp.MustCompile(`(?s)(.+?)`) +) + +type chooseTag struct { + *baseTag +} + +// Input (e.g. +// +// +// +// +// +// +// +// +// +// +// +// ). +func (c *chooseTag) process(b []byte, req *http.Request) ([]byte, int) { + found := closeChoose.FindIndex(b) + if found == nil { + return nil, len(b) + } + + c.length = found[1] + tagIdxs := whenRg.FindAllSubmatch(b, -1) + + var res []byte + + for _, v := range tagIdxs { + if validateTest(v[1], req) { + res = Parse(v[2], req) + + break + } + } + + tagIdx := otherwiseRg.FindSubmatch(b) + if tagIdx != nil { + res = Parse(tagIdx[1], req) + } + + return res, c.length +} diff --git a/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/comment.go b/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/comment.go new file mode 100644 index 0000000..1e0b596 --- /dev/null +++ b/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/comment.go @@ -0,0 +1,24 @@ +package esi + +import ( + "net/http" + "regexp" +) + +const comment = "comment" + +var closeComment = regexp.MustCompile("/>") + +type commentTag struct { + *baseTag +} + +// Input (e.g. comment text="This is a comment." />). +func (c *commentTag) process(b []byte, req *http.Request) ([]byte, int) { + found := closeComment.FindIndex(b) + if found == nil { + return nil, len(b) + } + + return []byte{}, found[1] +} diff --git a/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/errors.go b/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/errors.go new file mode 100644 index 0000000..e2de145 --- /dev/null +++ b/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/errors.go @@ -0,0 +1,5 @@ +package esi + +import "errors" + +var errNotFound = errors.New("not found") diff --git a/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/escape.go b/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/escape.go new file mode 100644 index 0000000..7a8ece2 --- /dev/null +++ b/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/escape.go @@ -0,0 +1,30 @@ +package esi + +import ( + "net/http" + "regexp" +) + +const escape = "" + +var ( + escapeRg = regexp.MustCompile("") + closeEscape = regexp.MustCompile("") +) + +type escapeTag struct { + *baseTag +} + +func (e *escapeTag) process(b []byte, req *http.Request) ([]byte, int) { + closeIdx := closeEscape.FindIndex(b) + + if closeIdx == nil { + return nil, len(b) + } + + e.length = closeIdx[1] + b = b[:closeIdx[0]] + + return b, e.length +} diff --git a/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/esi.go b/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/esi.go new file mode 100644 index 0000000..b20d45a --- /dev/null +++ b/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/esi.go @@ -0,0 +1,80 @@ +package esi + +import ( + "net/http" +) + +func findTagName(b []byte) tag { + name := tagname.FindSubmatch(b) + if name == nil { + return nil + } + + switch string(name[1]) { + case comment: + return &commentTag{ + baseTag: newBaseTag(), + } + case choose: + return &chooseTag{ + baseTag: newBaseTag(), + } + case escape: + return &escapeTag{ + baseTag: newBaseTag(), + } + case include: + return &includeTag{ + baseTag: newBaseTag(), + } + case remove: + return &removeTag{ + baseTag: newBaseTag(), + } + case try: + case vars: + return &varsTag{ + baseTag: newBaseTag(), + } + default: + return nil + } + + return nil +} + +func Parse(b []byte, req *http.Request) []byte { + pointer := 0 + + for pointer < len(b) { + var escapeTag bool + + next := b[pointer:] + tagIdx := esi.FindIndex(next) + + if escIdx := escapeRg.FindIndex(next); escIdx != nil && (tagIdx == nil || escIdx[0] < tagIdx[0]) { + tagIdx = escIdx + tagIdx[1] = escIdx[0] + escapeTag = true + } + + if tagIdx == nil { + break + } + + esiPointer := tagIdx[1] + t := findTagName(next[esiPointer:]) + + if escapeTag { + esiPointer += 7 + } + + res, p := t.process(next[esiPointer:], req) + esiPointer += p + + b = append(b[:pointer], append(next[:tagIdx[0]], append(res, next[esiPointer:]...)...)...) + pointer += len(res) + tagIdx[0] + } + + return b +} diff --git a/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/include.go b/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/include.go new file mode 100644 index 0000000..dcdf95b --- /dev/null +++ b/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/include.go @@ -0,0 +1,73 @@ +package esi + +import ( + "io" + "net/http" + "regexp" +) + +const include = "include" + +var ( + closeInclude = regexp.MustCompile("/>") + srcAttribute = regexp.MustCompile(`src="?(.+?)"?( |/>)`) + altAttribute = regexp.MustCompile(`alt="?(.+?)"?( |/>)`) +) + +type includeTag struct { + *baseTag + src string + alt string +} + +func (i *includeTag) loadAttributes(b []byte) error { + src := srcAttribute.FindSubmatch(b) + if src == nil { + return errNotFound + } + + i.src = string(src[1]) + + alt := altAttribute.FindSubmatch(b) + if alt != nil { + i.alt = string(alt[1]) + } + + return nil +} + +// Input (e.g. include src="https://domain.com/esi-include" alt="https://domain.com/alt-esi-include" />) +// With or without the alt +// With or without a space separator before the closing +// With or without the quotes around the src/alt value. +func (i *includeTag) process(b []byte, req *http.Request) ([]byte, int) { + closeIdx := closeInclude.FindIndex(b) + + if closeIdx == nil { + return nil, len(b) + } + + i.length = closeIdx[1] + if e := i.loadAttributes(b[8:i.length]); e != nil { + return nil, len(b) + } + + rq, _ := http.NewRequest(http.MethodGet, i.src, nil) + client := &http.Client{} + response, err := client.Do(rq) + + if err != nil || response.StatusCode >= 400 { + rq, _ = http.NewRequest(http.MethodGet, i.src, nil) + response, err = client.Do(rq) + + if err != nil || response.StatusCode >= 400 { + return nil, len(b) + } + } + + defer response.Body.Close() + x, _ := io.ReadAll(response.Body) + b = Parse(x, req) + + return b, i.length +} diff --git a/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/remove.go b/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/remove.go new file mode 100644 index 0000000..c73b1f7 --- /dev/null +++ b/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/remove.go @@ -0,0 +1,25 @@ +package esi + +import ( + "net/http" + "regexp" +) + +const remove = "remove" + +var closeRemove = regexp.MustCompile("") + +type removeTag struct { + *baseTag +} + +func (r *removeTag) process(b []byte, req *http.Request) ([]byte, int) { + closeIdx := closeRemove.FindIndex(b) + if closeIdx == nil { + return []byte{}, len(b) + } + + r.length = closeIdx[1] + + return []byte{}, r.length +} diff --git a/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/tags.go b/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/tags.go new file mode 100644 index 0000000..346108d --- /dev/null +++ b/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/tags.go @@ -0,0 +1,14 @@ +package esi + +import "regexp" + +const ( + try = "try" +) + +var ( + esi = regexp.MustCompile(""). +) diff --git a/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/try.go b/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/try.go new file mode 100644 index 0000000..6a8d63c --- /dev/null +++ b/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/try.go @@ -0,0 +1 @@ +package esi diff --git a/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/type.go b/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/type.go new file mode 100644 index 0000000..232f2f8 --- /dev/null +++ b/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/type.go @@ -0,0 +1,23 @@ +package esi + +import ( + "net/http" +) + +type ( + tag interface { + process([]byte, *http.Request) ([]byte, int) + } + + baseTag struct { + length int + } +) + +func newBaseTag() *baseTag { + return &baseTag{length: 0} +} + +func (b *baseTag) process(content []byte, _ *http.Request) ([]byte, int) { + return []byte{}, len(content) +} diff --git a/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/vars.go b/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/vars.go new file mode 100644 index 0000000..a3bdc26 --- /dev/null +++ b/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/vars.go @@ -0,0 +1,89 @@ +package esi + +import ( + "net/http" + "regexp" + "strings" +) + +const ( + httpAcceptLanguage = "HTTP_ACCEPT_LANGUAGE" + httpCookie = "HTTP_COOKIE" + httpHost = "HTTP_HOST" + httpReferrer = "HTTP_REFERER" + httpUserAgent = "HTTP_USER_AGENT" + httpQueryString = "QUERY_STRING" + + vars = "vars" +) + +var ( + interpretedVar = regexp.MustCompile(`\$\((.+?)(\{(.+)\}(.+)?)?\)`) + defaultExtractor = regexp.MustCompile(`\|('|")(.+?)('|")`) + stringExtractor = regexp.MustCompile(`('|")(.+)('|")`) + + closeVars = regexp.MustCompile("") +) + +func parseVariables(b []byte, req *http.Request) string { + interprets := interpretedVar.FindSubmatch(b) + + if interprets != nil { + switch string(interprets[1]) { + case httpAcceptLanguage: + if strings.Contains(req.Header.Get("Accept-Language"), string(interprets[3])) { + return "true" + } + case httpCookie: + if c, e := req.Cookie(string(interprets[3])); e == nil && c.Value != "" { + return c.Value + } + case httpHost: + return req.Host + case httpReferrer: + return req.Referer() + case httpUserAgent: + return req.UserAgent() + case httpQueryString: + if q := req.URL.Query().Get(string(interprets[3])); q != "" { + return q + } + } + + if len(interprets) > 3 { + defaultValues := defaultExtractor.FindSubmatch(interprets[4]) + + if len(defaultValues) > 2 { + return string(defaultValues[2]) + } + + return "" + } + } else { + strs := stringExtractor.FindSubmatch(b) + + if len(strs) > 2 { + return string(strs[2]) + } + } + + return string(b) +} + +type varsTag struct { + *baseTag +} + +// Input (e.g. comment text="This is a comment." />). +func (c *varsTag) process(b []byte, req *http.Request) ([]byte, int) { + found := closeVars.FindIndex(b) + if found == nil { + return nil, len(b) + } + + c.length = found[1] + + return interpretedVar.ReplaceAllFunc(b[5:found[0]], func(b []byte) []byte { + return []byte(parseVariables(b, req)) + }), c.length +} diff --git a/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/when.go b/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/when.go new file mode 100644 index 0000000..b4573de --- /dev/null +++ b/middleware/traefik/vendor/github.com/darkweak/go-esi/esi/when.go @@ -0,0 +1,50 @@ +package esi + +import ( + "net/http" + "regexp" + "strings" +) + +var ( + unaryNegation = regexp.MustCompile(`!\((\$\((.+)\)|(.+))\)`) + comparison = regexp.MustCompile(`(.+)(==|!=|<=|>=|<|>)(.+)`) + logicalAnd = regexp.MustCompile(`\((.+?)\)&\((.+?)\)`) + logicalOr = regexp.MustCompile(`\((.+?)\)\|\((.+?)\)`) +) + +func validateTest(b []byte, req *http.Request) bool { + if r := unaryNegation.FindSubmatch(b); r != nil { + return !validateTest(r[1], req) + } else if r := logicalAnd.FindSubmatch(b); r != nil { + return validateTest(r[1], req) && validateTest(r[2], req) + } else if r := logicalOr.FindSubmatch(b); r != nil { + return validateTest(r[1], req) || validateTest(r[2], req) + } else if r := comparison.FindSubmatch(b); r != nil { + r1 := strings.TrimSpace(parseVariables(r[1], req)) + r2 := strings.TrimSpace(parseVariables(r[3], req)) + switch string(r[2]) { + case "==": + return r1 == r2 + case "!=": + return r1 != r2 + case "<": + return r1 < r2 + case ">": + return r1 > r2 + case "<=": + return r1 <= r2 + case ">=": + return r1 >= r2 + } + } else { + vars := interpretedVar.FindSubmatch(b) + if vars == nil { + return false + } + + return parseVariables(vars[0], req) == "true" + } + + return false +} diff --git a/middleware/traefik/vendor/modules.txt b/middleware/traefik/vendor/modules.txt new file mode 100644 index 0000000..78e242b --- /dev/null +++ b/middleware/traefik/vendor/modules.txt @@ -0,0 +1,3 @@ +# github.com/darkweak/go-esi v0.0.4 => ../.. +## explicit; go 1.18 +github.com/darkweak/go-esi/esi