123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169 | // Package httpd matches request paths the way OpenBSD's httpd(8) matches
// the locations of its httpd.conf(5), so that the tests can check
// examples/httpd.conf against a rendered site.
package httpd
import (
"fmt"
"strconv"
"strings"
)
// A Location is what follows "location" in httpd.conf(5): a GlobLocation
// or a PatternLocation.
type Location interface {
// Match reports whether a request for p takes this location, and its
// captures, %0 (the whole match) then %1 and on, for a rewrite.
Match(p string) (caps []string, ok bool)
// Check reports why httpd would refuse the location, or why Match
// would not match as httpd does. A location that fails it matches
// nothing.
Check() error
// String is the location as httpd.conf writes it, after "location".
String() string
}
// GlobLocation is `location "<glob>"`. httpd matches it with fnmatch(3),
// FNM_CASEFOLD and no FNM_PATHNAME, so case is ignored and * also matches
// /. Only * is supported: Check refuses ?, [ and \, which mean more to
// fnmatch than they would here.
type GlobLocation string
func (g GlobLocation) Match(p string) ([]string, bool) {
if g.Check() != nil {
return nil, false
}
return nil, globMatch(lowerASCII(string(g)), lowerASCII(p))
}
func (g GlobLocation) Check() error {
if i := strings.IndexAny(string(g), `?[\`); i >= 0 {
return fmt.Errorf("glob %q: %q is not supported, only *", string(g), g[i])
}
return checkQuotable(string(g))
}
func (g GlobLocation) String() string { return `"` + string(g) + `"` }
// PatternLocation is `location match "<pattern>"`: a Lua pattern, as
// patterns(7) describes it, found anywhere in the path unless anchored
// with ^. Case matters. Check refuses %b, %f, back references and
// position captures, which Match does not support.
type PatternLocation string
func (l PatternLocation) Match(p string) ([]string, bool) {
if l.Check() != nil {
return nil, false
}
return find(p, string(l))
}
func (l PatternLocation) Check() error {
if _, err := checkPattern(string(l)); err != nil {
return fmt.Errorf("pattern %q: %w", string(l), err)
}
return checkQuotable(string(l))
}
func (l PatternLocation) String() string { return `match "` + string(l) + `"` }
// checkQuotable refuses what a quoted httpd.conf string cannot hold as it
// is: quotes, backslashes and control characters.
func checkQuotable(s string) error {
for _, c := range []byte(s) {
if c == '"' || c == '\\' || c < ' ' || c == 0x7f {
return fmt.Errorf("%q cannot be written in httpd.conf", s)
}
}
return nil
}
// Rewrite is the path a `request rewrite` to target sends a request to,
// with each %n replaced by capture n of the location that took it. A %n
// past the captures is left as it is.
func Rewrite(target string, caps []string) string {
return expand(target, func(n int, ref string) string {
if n < len(caps) {
return caps[n]
}
return ref
})
}
// CheckRewrite reports why target cannot be a rewrite for loc: a glob has
// no captures, and a pattern has %0, the whole match, and one for each of
// its parentheses.
func CheckRewrite(loc Location, target string) error {
caps := 0
if l, ok := loc.(PatternLocation); ok {
n, err := checkPattern(string(l))
if err != nil {
return err
}
caps = 1 + n
}
var err error
expand(target, func(n int, ref string) string {
if n >= caps && err == nil {
err = fmt.Errorf("rewrite %q: location %s has no %s", target, loc, ref)
}
return ref
})
if err != nil {
return err
}
return checkQuotable(target)
}
// expand replaces each %n in s, a % and digits, with f(n, "%n").
func expand(s string, f func(n int, ref string) string) string {
var b strings.Builder
for {
i := strings.IndexByte(s, '%')
if i < 0 {
break
}
j := i + 1
for j < len(s) && '0' <= s[j] && s[j] <= '9' {
j++
}
b.WriteString(s[:i])
if n, err := strconv.Atoi(s[i+1 : j]); err == nil {
b.WriteString(f(n, s[i:j]))
} else {
b.WriteString(s[i:j])
}
s = s[j:]
}
b.WriteString(s)
return b.String()
}
// globMatch is fnmatch(3) without FNM_PATHNAME for a pattern of literal
// text and *.
func globMatch(pattern, s string) bool {
star := strings.IndexByte(pattern, '*')
if star < 0 {
return pattern == s
}
if !strings.HasPrefix(s, pattern[:star]) {
return false
}
for i := star; i <= len(s); i++ {
if globMatch(pattern[star+1:], s[i:]) {
return true
}
}
return false
}
// lowerASCII folds case as FNM_CASEFOLD does in the C locale.
func lowerASCII(s string) string {
b := []byte(s)
for i, c := range b {
if 'A' <= c && c <= 'Z' {
b[i] = c + 'a' - 'A'
}
}
return string(b)
}
|