Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API / BUG - Introduce new request resolver middleware and fix broken forceWWW / forceSSL #7520

Merged

Conversation

tractorcow
Copy link
Contributor

@tractorcow tractorcow commented Oct 26, 2017

Fixes #7492

Requires silverstripe/silverstripe-cms#2007 to fix regressions in Director::makeRelative

* Allows events to be registered and passed through middleware.
* Useful for event registered prior to the beginning of a middleware chain.
*/
class RegisteredEventsMiddleware implements HTTPMiddleware
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have expected a DomainCanoncialisationMiddleware instead of this more abstract one?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And then a DomainCanoncialisationMiddleware.force_www config option or something...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thing that weirds me out about this a bit is that a "registered event" and a "middleware" are two different abstractions that do very different things, in addition to the legacy "request filter" abstraction.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The alternative approach would be to turn the forceWWW logic into its own middleware, and then add that middleware to Director instead of registering an event.

@tractorcow
Copy link
Contributor Author

@chillu do you have any thoughts on this pr?

@tractorcow
Copy link
Contributor Author

tractorcow commented Oct 26, 2017

I think if we were to move to a less-generic middleware, I'd probably just have a separate WWW and SSL redirect middleware, rather than trying to find a middle ground. What do you think about this @sminnee ?

If we were to cater to some yet-unknown "third" redirect type, it would be hard to pre-code this into such a middleware, thus the over-generalisation of the initial solution.

The question is, is "whenRequestIsAvailable" going to be useful in cases OTHER than just ssl / www redirection?

In the future I'd like to implement PHP promises officially. See silverstripe/silverstripe-assets#88. I had in mind that we could shift the ssl / www redirection into the promise api eventually too.

@chillu
Copy link
Member

chillu commented Oct 26, 2017

I agree with Sam, this adds unnecessary complexity. In my opinion, the benefits of middleware and the SS4 bootstrap is the relative simplicity and predictability of the logic flow (request in, request out). Modifying this behaviour with procedural logic (like calling Director::force_ssl() from _config.php) should be considered legacy behaviour, rather than encouraged through some kind of middleware events system. If you need influence over the request that early, you can add custom middleware in your index.php.

Laravel has three ways you can do this: App::before() (which looks like inlined middleware), global route filtering, or route specific "before" settings. If you need SSL redirection in your logic flow, you can use Redirect::secure() directly and return a HTTPresponse. This makes a lot of sense to me. https://stackoverflow.com/questions/19967788/laravel-redirect-all-requests-to-https

So, as a short term fix for keeping support for procedural invocation of Director::force_ssl(), I'd follow Sam's lead and suggest to set a config value in a domain canonicalisation middleware singleton. That's assuming injector is already booted at the time we parse/exec _config.php.

@tractorcow
Copy link
Contributor Author

Ok, let's go with the DomainCanonicalisationMiddleware

@sminnee
Copy link
Member

sminnee commented Oct 27, 2017

In terms of API for the middleware, I would suggest:

  • Force WWW can be set to "always" (maybe a "*" or true) or "if the hostname is in the following list". This lets a sysadmin be explicit about which hostnames are have their WWW DNS configured on the machine in question.
  • Force SSL can be set to "always" (maybe a "*" or true) or "if the hostname is in the following list". This lets a sysadmin be explicit about which hostnames are actually configured for SSL on the machine in question.

Beyond the requirements of the issue at hand I would also suggest:

  • A canonical hostname to redirect to
  • A list of other allowed hostnames that won't redirect to the canonical hostname. This lets you have canonicalisation in most cases while also allowing a few special cases (I remember when working on a pre-production site I needed to access it directly by its IP, so I added that as a 2nd acceptable domain)

In terms of processing order I would do:

  • Auto-add www
  • Check canonical hostname
  • Auto-add HTTPS

Outside the scope of this, but for subsites I'd probably look at a whole separate implementation of this.

Additionally, if one was to build a vanity domain -> redirection mapping system, I would probably include that as a middleware that is processed prior to this one.

@chillu
Copy link
Member

chillu commented Oct 27, 2017

I'd prefer to limit the implementation (at least for 4.0) to something that allows use of the Director::forceSSL() API, which means implementing the $patterns argument (for regex path checks), and the $secureDomain argument. If domain whitelists and maps are little implementation overhead, go ahead. But it's creating an API where I'm not sure we need one.

Overall I agree with Dan that this stuff is better handled by the web server layer (.htaccess or nginx rules). forceSSL() made sense at a time when it was considered a special case (e.g. ensuring a form displayed on SSL for a secure submission). Best practice across the web now is to auto-redirect to SSL for every request, which makes those initial non-SSL requests to the homepage slower if you're doing them in a PHP layer. And conditional logic (only some paths, only some domains) is becoming less common over time as SSL adoption grows.

@tractorcow
Copy link
Contributor Author

Director::forceSSL() would still need to work after middleware has been initialised... e.g. in Controller::init(), so we would need it still, even if we implement it at the middleware level. My intention is to retain the "if request is available" condition to determine which is activated.

Copy link
Member

@sminnee sminnee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactoring as discussed

@tractorcow tractorcow force-pushed the pulls/4.0/config-redirect-works branch from e06157d to 3c3e94f Compare October 27, 2017 04:37
@tractorcow
Copy link
Contributor Author

Updated and pushed... another rather major refactor, so look at it with a fresh mind from the last solution. :)

}

// Already on SSL
if (Director::is_https($request)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As reported in #7492 this will provide a false positive if the SS_BASE_URL is set to an https scheme even if the request isn't. I recommend checking against the request only.

Copy link
Contributor Author

@tractorcow tractorcow Oct 29, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's nothing to do with SS_BASE_URL; Director::is_https() prefers the Request object before checking SS_BASE_URL.

When this line is called in CanonicalURLMiddleware, the $request will always be populated (since this middleware only activates when a request is available), so the SS_BASE_URL will never be checked by this code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in that case, it shouldn't be functionally incorrect to do as you suggest, and just inline the small part of Director::is_https(). I think I see your point. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, this identified a painful flaw in Director::makeRelative(). For sites with multiple domains this method wouldn't work, meaning that Director::mockRequest() was failing in tests.

After a bit of work I've fixed and tested this, however. Now we can directly use the request instead of needing Director for getting request components.


// Check if already on www
$host = Director::host($request);
if (strpos($host, 'www') === 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we check for www.?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah true, will update.

@tractorcow tractorcow force-pushed the pulls/4.0/config-redirect-works branch from 3c3e94f to a21b1fb Compare October 29, 2017 23:44
@tractorcow
Copy link
Contributor Author

All updated. :)

API Introduce CanonicalURLMiddleware
BUG Fix Director::makeRelative() failing on multi-domain sites
@tractorcow tractorcow force-pushed the pulls/4.0/config-redirect-works branch from a21b1fb to 9d3277f Compare October 30, 2017 01:43
@tractorcow
Copy link
Contributor Author

Fixed up some regressions in Director::makeRelative(). I've added a note that user code should check is_site_url() BEFORE calling makeRelative(), if using un-trusted urls.

Added silverstripe/silverstripe-cms#2007

@flamerohr flamerohr dismissed stale reviews from sminnee and dhensby October 31, 2017 22:25

Suggestion implemented

Copy link
Contributor

@flamerohr flamerohr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've reviewed the feedback and suggestions - they looks to be implemented.

The changes look good and I'm quite excited to see this included.

I'll leave merging for tomorrow morning (2 Nov - 10am) In case @sminnee or @dhensby would like to review the new changes again.

@flamerohr flamerohr merged commit cd55a03 into silverstripe:4.0 Nov 1, 2017
@flamerohr flamerohr deleted the pulls/4.0/config-redirect-works branch November 1, 2017 21:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants