Plugin that runs on every request without directive

I am trying to write a plugin that modifies HTTP requests (headers) on every request, so far I have got everything working, but I still need to include the directive for my handler for it to be run. I am probably overseeing something. How do I need to configure my plugin so that it just runs on every request without the directive being needed?

func init() {
	caddy.RegisterModule(Module{})
	httpcaddyfile.RegisterHandlerDirective("test", parseCaddyfile)
}

type Module struct {
}

func (m Module) ServeHTTP(response http.ResponseWriter, request *http.Request, next caddyhttp.Handler) error {
	fmt.Println("TEST")


	response.Header().Add("Test-Header", "test")
	request.Header.Add("Test-Header", "test")

	return next.ServeHTTP(response, request)
}

That’s not possible. Caddy doesn’t provide hooks for always including things without config. The config is the core thing in Caddy, and what drives what actually happens.

1 Like

Okay, so I’d have to patch Caddy for that? It is not a public plugin in case you’re scared about that, and also that being the reason for not allowing it. It’s a internal plugin I need to develop and it running on every request is a requirement to us, we do not want it to be possible to be disabled on accident (it’s going to be responsible for some essential security things)

Personally, I think a better security posture would be to control what the config is allowed to be, rather than forking the Caddy code.

All plugins (including the built-in ones) need to be used/enabled by adding them to the config somehow.

And you can already add headers to the request and response of every request with config, no extra plugins needed.

2 Likes

You can define a snippet at the top of your Caddyfile, then just put a quick import line in each site.

3 Likes

Yes, the HTTP header was just an example of modifying the request.

In reality this is more about attack defense, which then includes doing analytics for detecting new attacks, that we would want to run every time, no matter what. I feel really really uncomfortable with the thought that someone internally who has not written this code will miss to include the directive somewhere and then that whole host is basically without protection (from our perspective).

We will probably go with the direct JSON configuration instead of the Caddyfile, so the import thing doesn’t really help, and it still could be missed to be included as I said above.

I think patching the Caddy source isn’t the best thing to do as well, yeah. But I really see no other option, that’s why I hoped that there was an option to make a plugin run every time in the first place. We’re going to think about it once more before going ahead with the patching, maybe there is another solution, but so far I haven’t come up with any idea that we feel comfortable with.

I imagine the reason for everything needing a config directive is so that users who download a caddy version compiled with plugins from caddyserver.com don’t get any unwanted surprises? Would you consider adding the option for that if you self-compile, but forbid including plugins that request that functionality online for caddyserver.com? Or is it a no-go in any case for you?

That’s one reason, but there are many:

  • Handlers are a middleware chain, so order matters. So something that “always runs” could only ever be ordered first, and that doesn’t always make sense.

  • Something needs to add it to the middleware chain, so it needs to be registered somehow. The core architecture of Caddy is entirely designed around the config, where you can change the config in-place gracefully, and everything is provisioned based on what’s in the config. Pulling stuff from outside of the config would be a leaky abstraction. Architecture — Caddy Documentation

  • Yes, implicit behaviour is not a good user experience. It would be incredibly hard to inspect what’s going on inside your app in that case because the config won’t reflect the actual behaviour.

Honestly, what you should be concerned about is untrusted access to changing the config. You can write some validation scripts to ensure the config always has your HTTP handler in each site, and throw up alarms if it doesn’t.

If feels like you’re going at this the wrong way.

Yes, this is what we have thought about so far as well.

Well, I am not sure if it is the wrong way, companies like Cloudflare/Fastly etc. do not include their request analytics / DDoS defense / other security stuff inside a configuration for every of their million hosts they have, they just have their proxy run some of that stuff for anything that is incoming (and not already filtered by previous things up the stack). Why do you think that is wrong?

And regarding this, it is more about eliminating the possibility for mistakes when I in theory can, it just feels wrong to me to leave open the possibility for it to be not included, when we want it to run every time anyways. There is no case where we would not include it, so adding the possibility for configuration errors is just unnecessary hazard from our perspective.

That’s apples and oranges though. Caddy isn’t that. Caddy is a general purpose webserver, not a CDN/firewall. The goals are completely different.

We want to keep things simple and focused, avoid bloat. Functionality like this is completely orthogonal to the project’s core architecture, so it doesn’t make sense to support.

1 Like

Okay, I understand that.

Even though that makes it harder for companies who want to do stuff like Cloudflare, or even just not use Cloudflare and want to implement a little bit of protection themselves, to use Caddy in my opinion. It also limits the user-base to the people and organizations who do not need such, I admit advanced, functionality from my perspective?

We’re currently trying to get away from NGINX, where we had some C modules do what I roughly described, and I know a few other companies who do the same with either NGINX or HAProxy, just so that you understand our situation.

I fully understand where you are coming from though and what the priorities are.

Just looked at how it’s done in NGINX and noticed that we need to enable our modules in the configuration as well, but not for every server { } block, but instead once in the global http {} block with our_module on;. That is something I would feel comfortable with, but I couldn’t determine if this is possible with Caddy in the documentation? Could you guide me on where I can find it, in case I missed it?

If you will be replacing NGINX with Caddy, you will have to create a new Caddyfile (or use the NGINX adapter).

Do you have an issue:

  1. Writing a plugin that does exactly what you need
  2. adding one line in the config file to activate this plugin? you can activate this plugin from the commandline Caddy as well
1 Like

I think you misunderstood what I need, it has nothing to do directly with NGINX, I just need a plugin to run every time without configuring it, or configure it in the least amount possible. Which would be one time, like it is possible in NGINX with the http {} block like I stated above.

  1. No, I can write the plugin according to my needs
  2. If I, for example, have 2000 servers / hosts, I need to add it 2000 times to every host. I’d much rather add it one time somewhere (like it is possible with NGINX), or not add it at all and still have it run. But it seems like that is not possible, and also not wanted by the Caddy authors, or do you mean with your comment

that that is actually possible? If so, how? I can’t find it in the documentation.

You can absolutely do this just once, I am not sure why you would have to add it 2000 times to your config? :thinking: Caddy’s routing config is very composable and flexible.

To explain what Matt means, take this Caddyfile for example:

a.example.com {
	respond "a"
}

b.example.com {
	respond "b"
}

c.example.com {
	respond "c"
}

It produces this adapted JSON:

{
	"apps": {
		"http": {
			"servers": {
				"srv0": {
					"listen": [
						":443"
					],
					"routes": [
						{
							"match": [
								{
									"host": [
										"a.example.com"
									]
								}
							],
							"handle": [
								{
									"handler": "subroute",
									"routes": [
										{
											"handle": [
												{
													"body": "a",
													"handler": "static_response"
												}
											]
										}
									]
								}
							],
							"terminal": true
						},
						{
							"match": [
								{
									"host": [
										"b.example.com"
									]
								}
							],
							"handle": [
								{
									"handler": "subroute",
									"routes": [
										{
											"handle": [
												{
													"body": "b",
													"handler": "static_response"
												}
											]
										}
									]
								}
							],
							"terminal": true
						},
						{
							"match": [
								{
									"host": [
										"c.example.com"
									]
								}
							],
							"handle": [
								{
									"handler": "subroute",
									"routes": [
										{
											"handle": [
												{
													"body": "c",
													"handler": "static_response"
												}
											]
										}
									]
								}
							],
							"terminal": true
						}
					]
				}
			}
		}
	}
}

Notice that routes is an array of subroutes, one for each site.

What you can do, is add another route (via JSON only, Caddyfile won’t let you do this) above those three routes, with no matcher which will always run before all the other routes that are per-site:

					"routes": [
						{
							"handle": [
								{
									"handler": "my_special_plugin"
								}
							]
						},
						{
							"match": [
								{
									"host": [
										"a.example.com"
									]
								}
							],
							"handle": [
								{
									"handler": "subroute",
									"routes": [
										{
											"handle": [
												{
													"body": "a",
													"handler": "static_response"
												}
											]
										}
									]
								}
							],
							"terminal": true
						},
		...
1 Like

Yep, that’s one great way to do it! Thanks for going to the work of creating the example, @francislavoie .

There are other ways too. I just don’t have the time to draft up more examples. Kind of depends on specific requirements. But the JSON route is very flexible and expressive for advanced stuff like that. Can probably be done in Caddyfile too.

1 Like

Oh, that is indeed great. This is basically what I was searching for, sorry for not being able to figure it out myself

1 Like

This topic was automatically closed after 30 days. New replies are no longer allowed.