Design for a plugin with 'global' state and 'route based' state?

Hey,

Been designing a Caddy module for https://paypi.dev, a site which lets you charge for access to APIs. The idea is that we provide you with a ‘SecretKey’ and then you make simple requests to our site to authenticate, charge & monitor users. Only issue is I’m struggling to figure out the most friendly layout for the plugin. Wondered whether I could ask some experts here?

For a request, say an ‘echo’ api call, we might want something that looks like:

@post_echo {
  method POST
  path /api/echo
}
paypi @post_echo {
  secret_key some_key_1
  charge_id some_id_1
}
reverse_proxy @post_echo 127.0.0.1:9000

This works fine. Our plugin gets all the information it needs to successfully authenticate a request. However, most people don’t have one endpoint / one charge but dozens / hundreds. That’s a lot of duplication of the secret_key, which is almost always the same. In fact, what would look better is if we could remove the secret key altogether:

paypi @post_echo some_id_1
reverse_proxy @post_echo 127.0.0.1:9000

With only one config option it’d be easy to put it all on one line. However, is there some way of keeping the global ‘secret_key’ accessible within my plugin? Can I have something like…

some_domain {
  paypi set secret_key some_key_1

  paypi @post_echo some_id_1
  paypi @post_reflection some_id_2
  paypi @post_resounding some_id_3
  ... and so on

  reverse_proxy @post_echo 127.0.0.1:9000
}

Are there any other examples of where plugins that have requirements like this? Looking down the module list I can’t see anything like this.

2 Likes

Just thought of an alternative syntax, which would be:

paypi {
  set secret_key some_secret_1

  route {
    match method POST
    match path /api/echo
    charge_id some_charge_1
  }

  route {
    match method POST
    match path /api/reverberation
    charge_id some_charge_2
  }
}

But that also seems clunky and would mean I’d have to reimplement matchers.

1 Like

Yes, actually. There’s a vars map that can be used to shove arbitrary data for later use.

Take a look at how the root directive works in Caddy’s source. Essentially all does it set up a vars request handler which sets a variable. That’s it. Later, other handlers or matchers like file_server or try_files will grab from vars to use that value as their default (and most of them allow explicitly configuring root on themselves, if no vars root is set).

Relevant code is in here:

And the root Caddyfile directive (which just makes a VarsMiddleware):

2 Likes

I enjoyed how everytime I refreshed the page you added another paragraph to your answer with more information, :slight_smile:.

Seems like with this I could have just the syntax I wanted. Will give it a try!

2 Likes

What can I say, I’m a “submit first, edit later” kind of person :joy:

Glad I could help :+1:

Possibly a ‘Caddy Help’ question instead of a plugin question, but directly related to this so will reply to this topic regardless… Can post a separate topic if this is wrong.

Syntax like paypi set secret_key some_secret_1 works well for setting the global secret, but what’s the best syntax for handling auth? Should I be doing something along the lines of…

paypi set secret_key some_secret_1

route @post_echo {
  paypi set charge_id some_id_1
}

route @post_reflection {
  paypi set charge_id some_id_1
}

// no authorization header provided...
respond not { header Authorization } "Unauthorized" 401 { close }

// handle authentication
paypi handle

// check that the user isn't accessing an unhandled url
respond not { ?handled? } "Path not found" 404 { close }

// everything else seems to have succeeded...
reverse_proxy some_backend_server:some_port

But, with this I have several issues:

  1. I’m struggling to figure out a nice way of ensuring that one of the above routes has actually matched, because it seems you can’t do recursive matchers (e.g. @one_matched { @post_echo @post_reflection }.
  2. This assumes PayPI is implementing the caddyauth.Authenticator type under the command paypi handle

Alternatively, there’s the option to handle it individually:

handle @post_echo {
  paypi set charge_id some_id_1
  reverse_proxy some_backend_server:some_port
}

handle @post_echo {
  paypi set charge_id some_id_1
  reverse_proxy some_backend_server:some_port
}

handle {
  respond "Access denied" 403 { close }
}

But with dozens of endpoints this might result in a lot of duplication of things like reverse_proxy!

Not sure if I’m just abusing Caddy for things it wasn’t intended to be used for. Advice would be appreciated on what the nicest syntax for users is.

1 Like

FYI, that’s not valid syntax.

Braces must be at the end or beginning of a line, you can’t inline blocks.

Also, request matchers can’t be inlined either, you need to use a named matcher for anything other than simple path matchers.

Also, you don’t really need close, all that does is close the connection after the response, and all that’ll do is slow down the next connection and handshake from the client if they fix the request and send again. You can let the connection close naturally without issue.

I guess I’m not entirely sure I follow the intended flow/pipeline you’re trying to set up.

At what point is the request considered “authorized”? Is it when paypi set charge_id happens?

So is the question "how do I check if charge_id is set before I run reverse_proxy"?

If so, you could use the expression matcher to check if something in vars is set for example. Maybe for example:

@notCharged expression `{http.vars.paypi_charge_id} == ""`
respond @notCharged "Nope" 404

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