Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ Support status of mESI features across all server integrations.
| `<esi:remove>` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| `<esi:comment>` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| `<!--esi ... -->` (inline) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| `<esi:inline>`, `<esi:choose>`, `<esi:try>`, `<esi:vars>` | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ |
| `<esi:inline>`, `<esi:choose>`, `<esi:try>` | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ |
| `<esi:vars>` / `$(...)` variable substitution | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| `src` / `alt` attributes | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| `fetch-mode="fallback"` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| `fetch-mode="ab"` (A/B testing) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Expand Down Expand Up @@ -47,7 +48,8 @@ Support status of mESI features across all server integrations.

## Notes

- **`<esi:inline>`, `<esi:choose>`, `<esi:try>`, `<esi:vars>`** – Recognized by the tokenizer (no parse errors), but their content is silently dropped from output. Full support planned.
- **`<esi:inline>`, `<esi:choose>`, `<esi:try>`** – Recognized by the tokenizer (no parse errors), but their content is silently dropped from output. Full support planned.
- **`<esi:vars>` / `$(...)` variable substitution** – Fully supported. Variables are defined via `<esi:variable>` in `<esi:vars>` blocks and resolved via `$(NAME)` syntax in include URLs, text content, and test expressions. Supports `$(HTTP_HEADER{Name})`, `$(HTTP_COOKIE{name})`, and `$(QUERY_STRING{param})` via config fields.
- **Nginx** – Uses the `Parse` function from `libgomesi` which does not enable `BlockPrivateIPs` (defaults to `false`). No SSRF protection.
- **PHP Extension** – Exposes only `\mesi\parse(input, max_depth, default_url)`. No security configuration.
- **Caddy / FrankenPHP** – FrankenPHP uses the Caddy plugin, identical functionality.
Expand Down
11 changes: 11 additions & 0 deletions mesi/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ type EsiParserConfig struct {
// Static tokens do not count against this limit.
MaxWorkers int
requestSemaphore chan struct{} // semaphore for limiting HTTP requests

// Variables holds ESI variable definitions from <esi:vars> blocks and
// can be pre-populated by callers. Variables are resolved via $(NAME)
// syntax in include URLs, text content, and test expressions.
Variables map[string]string
// RequestHeaders are available for $(HTTP_HEADER{Name}) resolution.
RequestHeaders http.Header
// RequestCookies are available for $(HTTP_COOKIE{name}) resolution.
RequestCookies map[string]string
// RequestQuery is available for $(QUERY_STRING{param}) resolution.
RequestQuery map[string]string
}

func (c EsiParserConfig) getSemaphore() chan struct{} {
Expand Down
17 changes: 15 additions & 2 deletions mesi/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,19 @@ func MESIParse(input string, config EsiParserConfig) string {
var results []Response

for index, token := range tokens {
if !token.isEsi() {
results = append(results, Response{token.staticContent, index})
if token.esiTagType == ESI_VARS {
vars := parseVarsBlock(token.esiTagContent)
if config.Variables == nil {
config.Variables = vars
} else {
for k, v := range vars {
config.Variables[k] = v
}
}
results = append(results, Response{"", index})
} else if !token.isEsi() {
content := evaluateExpression(token.staticContent, config)
results = append(results, Response{content, index})
} else {
esiJobs = append(esiJobs, esiJob{index, token})
}
Expand Down Expand Up @@ -128,6 +139,8 @@ func MESIParse(input string, config EsiParserConfig) string {
ch <- res
continue
}
include.Src = evaluateExpression(include.Src, config)
include.Alt = evaluateExpression(include.Alt, config)
newConfig := config.OverrideConfig(include).WithElapsedTime(time.Since(start))
content, isEsiResponse := include.toString(newConfig)

Expand Down
86 changes: 86 additions & 0 deletions mesi/vars.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package mesi

import (
"regexp"
"strings"
)

var varPattern = regexp.MustCompile(`\$\(([^)]+)\)`)

func evaluateExpression(expr string, config EsiParserConfig) string {
return varPattern.ReplaceAllStringFunc(expr, func(match string) string {
inner := match[2 : len(match)-1]

if config.Variables != nil {
if val, ok := config.Variables[inner]; ok {
return val
}
}

if strings.HasPrefix(inner, "HTTP_HEADER{") && strings.HasSuffix(inner, "}") {
key := inner[12 : len(inner)-1]
if config.RequestHeaders != nil {
if val := config.RequestHeaders.Get(key); val != "" {
return val
}
}
if config.Variables != nil {
if val, ok := config.Variables[match]; ok {
return val
}
}
return ""
}

if strings.HasPrefix(inner, "HTTP_COOKIE{") && strings.HasSuffix(inner, "}") {
key := inner[12 : len(inner)-1]
if config.RequestCookies != nil {
if val, ok := config.RequestCookies[key]; ok {
return val
}
}
return ""
}

if strings.HasPrefix(inner, "QUERY_STRING{") && strings.HasSuffix(inner, "}") {
key := inner[13 : len(inner)-1]
if config.RequestQuery != nil {
if val, ok := config.RequestQuery[key]; ok {
return val
}
}
return ""
}

return ""
})
}

var varDefPattern = regexp.MustCompile(`<esi:variable\s+name="([^"]*)"\s+value="([^"]*)"\s*/>`)

func parseVarsBlock(rawContent string) map[string]string {
vars := make(map[string]string)

start := strings.Index(rawContent, ">")
if start == -1 {
return vars
}
end := strings.LastIndex(rawContent, "</")
if end == -1 || end <= start {
return vars
}
inner := rawContent[start+1 : end]

matches := varDefPattern.FindAllStringSubmatch(inner, -1)
for _, m := range matches {
if len(m) >= 3 {
name := m[1]
value := m[2]
if name != "" {
vars[name] = value
}
}
}

return vars
}
Loading
Loading