One thing that bugged me from the beginning was the reliance on Rack::Request. I touched a bit on that in #6, but the gist of it is that
- You have this weird chicken-and-egg problem, where you need to first create a request to build the signature, that you then put into another request that you then actually perform. But more importantly:
- You cannot actually make outgoing requests with
rack, so when you only want to make requests, why should you have to deal with rack?
But I thought it kind of works, so I guess it is OK. Until I encountered a problem:
The second parameter of Linzer.new_request is called uri, but in fact it MUST be a path. You can pass a full URI, but then all kinds of weird things can happen. This parameter is passed as PATH_INFO into rack's environment, so it absolutely should never contain anything but the path according to https://github.com/rack/rack/blob/main/SPEC.rdoc
So how do you create a request that includes everything you might want to sign or verify? It is possible, but not straight-forward at all. I guess you would need to pass at least the Host header, something like rack.url_scheme and possibly QUERY_STRING if one is part of the URI.
Having all this information is especially important if you want to sign or verify the @target-uri derived component or query parameters. This also touches on #9.
I do not think users of linzer should have to worry about this. This is cumbersome, redundant and error-prone.
I am, however, not sure how to fix this. I see three different options:
1. Quick fix
Linzer::Request.build could parse the passed uri parameter, e.g. with URI.parse and build a more full-featured rack environment from that.
2. Small Refactor
Linzer::Request could become a very simple class, that gets passed method, URI and headers and offers simple accessor methods to query the components that Linzer::Message needs to know. Kind of a smaller, simpler version of Rack::Request that encapsulates only what is needed to create or verify signatures.
This could offer a class method, e.g. ::from_rack, that accepts a Rack::Request and extracts the necessary information from there, making it reasonably easy to use e.g. in the included rack middleware.
This approach would make it straight-forward to construct a request object that includes all information necessary for signing and verification. It would also mean you could sign requests without using rack at all.
3. Large Refactor
This is the idea I outlined in #6: linzer could have a Linzer::Request base class that defines the necessary accessor methods and a couple of adapter classes that inherit from that and encapsulate a request object from a different library. To begin with there could be adapters for Rack::Request and Net::HTTPRequest, but this should be easy to extend so everyone can create adapters for their preferred HTTP library.
This would, among other things, unlock the one-step signing proposed in #6.
(Note that I only mentioned requests here for brevity, but of course the same applies to responses as well.)
I would be happy to work on this, just let me know which direction you would prefer or if you have any other ideas.
One thing that bugged me from the beginning was the reliance on
Rack::Request. I touched a bit on that in #6, but the gist of it is thatrack, so when you only want to make requests, why should you have to deal withrack?But I thought it kind of works, so I guess it is OK. Until I encountered a problem:
The second parameter of
Linzer.new_requestis calleduri, but in fact it MUST be a path. You can pass a full URI, but then all kinds of weird things can happen. This parameter is passed asPATH_INFOinto rack's environment, so it absolutely should never contain anything but the path according to https://github.com/rack/rack/blob/main/SPEC.rdocSo how do you create a request that includes everything you might want to sign or verify? It is possible, but not straight-forward at all. I guess you would need to pass at least the
Hostheader, something likerack.url_schemeand possiblyQUERY_STRINGif one is part of the URI.Having all this information is especially important if you want to sign or verify the
@target-uriderived component or query parameters. This also touches on #9.I do not think users of linzer should have to worry about this. This is cumbersome, redundant and error-prone.
I am, however, not sure how to fix this. I see three different options:
1. Quick fix
Linzer::Request.buildcould parse the passeduriparameter, e.g. withURI.parseand build a more full-featured rack environment from that.2. Small Refactor
Linzer::Requestcould become a very simple class, that gets passed method, URI and headers and offers simple accessor methods to query the components thatLinzer::Messageneeds to know. Kind of a smaller, simpler version ofRack::Requestthat encapsulates only what is needed to create or verify signatures.This could offer a class method, e.g.
::from_rack, that accepts aRack::Requestand extracts the necessary information from there, making it reasonably easy to use e.g. in the included rack middleware.This approach would make it straight-forward to construct a request object that includes all information necessary for signing and verification. It would also mean you could sign requests without using
rackat all.3. Large Refactor
This is the idea I outlined in #6: linzer could have a
Linzer::Requestbase class that defines the necessary accessor methods and a couple of adapter classes that inherit from that and encapsulate a request object from a different library. To begin with there could be adapters forRack::RequestandNet::HTTPRequest, but this should be easy to extend so everyone can create adapters for their preferred HTTP library.This would, among other things, unlock the one-step signing proposed in #6.
(Note that I only mentioned requests here for brevity, but of course the same applies to responses as well.)
I would be happy to work on this, just let me know which direction you would prefer or if you have any other ideas.