Repository: caddyserver/caddy (language: go)

Issue:
Using multiple path_regexp matchers does not error 
Caddy version: v2.6.0-beta.3
GOOS: linux
GOARCH: amd64

I found some interesting behavior where specifying multiple `path_regexp` matchers in a named matcher will not error when the config file is parsed. Instead, the last `path_regexp` matcher will be used and the previous matchers will silently be discarded.

Example Caddyfile:

```
http://localhost:9000 {
	log {
		output stdout
		level INFO
	}

	@dockerAPI {
		method GET
		path_regexp ^/v\d\.\d+/*
		path_regexp ^/asdf/*
	}

	handle @dockerAPI {
		reverse_proxy unix//var/run/docker.sock
	}
	handle {
		respond 427
	}
}
```

In this example, requests that begin with `/v1.41/` do not match `@dockerAPI`, but requests that begin with `/asdf/` do. I think an error should be returned when parsing the config file.

As a side note, I discovered this while trying to use Caddy to limit access to a Docker API socket, I only want certain API endpoints to be allowed and passed through by Caddy. It's difficult to do such a thing without the ability to use multiple `path_regexp` matchers, is there a workaround for this or should I be using a 3rd party module/a separate tool entirely?


Discussion / hints:
You're right that in the Caddyfile, only the last `path_regexp` matcher will apply. I agree we should make that an explicit error to be clear that it's bad config.

To use multiple regexps, you can use the `expression` matcher, which has `path_regexp` as a function. But I'm not sure I understand what exactly you're trying to match.

```
@dockerAPI expression `method('GET') && path_regexp('^/v\d\.\d+/*') && path_regexp('^/asdf/*')`
```

Keep in mind you could also just use a regular `path` matcher for a prefix match, like `path /asdf/*` which won't conflict with the other `path_regexp` since it's a matcher of a different type. But it will be an `AND` between each in a named matcher. If you need an `OR` then using `expression` is easier.

Also in v2.6, you'll be able to omit the `expression` token since we implemented a shortcut:

```
@dockerAPI `method('GET') && path_regexp('^/v\d\.\d+/*') && path_regexp('^/asdf/*')`
```
> You're right that in the Caddyfile, only the last path_regexp matcher will apply. I agree we should make that an explicit error to be clear that it's bad config.

Good to hear!

> To use multiple regexps, you can use the expression matcher, which has path_regexp as a function. But I'm not sure I understand what exactly you're trying to match.

Thanks for the help! Note for future readers backslashes have to be escaped to work correctly when using `path_regexp` in an `expression`, ie

```
@dockerAPI expression `method('GET') && path_regexp('^/v\\d\\.\\d+/*')`
```

will work. 

Sorry, I tried to make my example as simple as possible to reproduce which made it not make much sense as an actual config. I'm trying to match a few Docker API endpoints, namely:
- `/v1.41/events*`
- `/v1.41/containers/json*`
- `/v1.41/containers/<some_hash>/json`
Where the `/v1.41` prefix can change.

Is using a long `expression` the only way to match on multiple `path_regexp`s at once?
> Is using a long expression the only way to match on multiple path_regexps at once?

Yes, which is a limitation of the JSON config's design. Run `caddy adapt --pretty` on your config, and you'll see that `path_regexp` is an object key in a matcher set, which means there can only matcher of each type, and unfortunately the value of the matcher is an object and not an array so there can only be one value. Other matchers like `path` take an array of paths so it can OR all of the input paths.

Fix patch:
diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go
index f86ce0a4547..3d5791f7d6b 100644
--- a/modules/caddyhttp/matchers.go
+++ b/modules/caddyhttp/matchers.go
@@ -1001,6 +1001,12 @@ func (m *MatchHeaderRE) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 			val = second
 		}
 
+		// If there's already a pattern for this field
+		// then we would end up overwriting the old one
+		if (*m)[field] != nil {
+			return d.Errf("header_regexp matcher can only be used once per named matcher, per header field: %s", field)
+		}
+
 		(*m)[field] = &MatchRegexp{Pattern: val, Name: name}
 
 		if d.NextBlock(0) {
@@ -1469,6 +1475,13 @@ func (mre *MatchRegexp) Match(input string, repl *caddy.Replacer) bool {
 // UnmarshalCaddyfile implements caddyfile.Unmarshaler.
 func (mre *MatchRegexp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 	for d.Next() {
+		// If this is the second iteration of the loop
+		// then there's more than one path_regexp matcher
+		// and we would end up overwriting the old one
+		if mre.Pattern != "" {
+			return d.Err("regular expression can only be used once per named matcher")
+		}
+
 		args := d.RemainingArgs()
 		switch len(args) {
 		case 1:


Test patch: