Once again, I found myself beating my way through a website todo, and again I painfully managed to complete the task. Perhaps I can spare you some of that pain with this discussion of SSL.
A nice convenience with the price of two late nights spent forcing my way through the seemingly most ridiculous bugs. What objective snatched away those precious hours of sleep?
Forcing SSL. That's it. I implemented an administrator interface to my blog so I can easily post, comment, etc. (or else I'd never get time to write) however I was bugged every time I saw the basic http auth dialog with its warning: Your password will be sent unencrypted.
Naturally, I'm a conspiracy theorist and anticipate some foreign nation overtaking my blog and using it to bring about the end of the world (ok not really). However, having to type in
https:// every time I want to securely do my magic jumbo gets really irritating, and too many times have I authenticated without SSL. Way too many times.
It was time to forcefully redirect to SSL sessions...sounds simple enough. I started out (well, actually this was after a few revisions) with the following skeleton-code for the application controller.
class ApplicationController < ActionController::Base ... private def authenticate unless require_ssl authenticate_or_request_with_http_basic do |user, password| session[:admin] = user == 'user' && password == 'secret' end else return false end end def require_ssl # SSL needs to be forced if the server is in production and the request is not already SSL ssl_required = Rails.env.production? and not request.ssl? flash.keep if ssl_required redirect_to :protocol => "https://" if ssl_required ssl_required end end
I was already calling
before_filter :authenticate in my controllers, so it seemed sensible to extend that before filter with an SSL redirect. However, by the first night I could not get past the dreaded "Too many redirects" error. I checked the code logic, and all seemed well — in fact, all was! But after probing the return value of
request.ssl?, I found it never returned true. Why? After some searching, I found the code definition for the ssl? qualifier:
# File actionpack/lib/action_dispatch/http/url.rb, line 20 def ssl? @env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https' end
Passenger and SSL
So that was useless…until I did some Googling. I deploy all my apps with Passenger (which up until now has been awesome) however there is an active bug that seems to spring up every other version in which SSL headers are not transferred from Apache to the Rails app via Passenger — which means my app keeps trying to redirect to SSL, but never gets feedback that it is in SSL. So with a little more research, I found temporary fix that modified the
# lib/url.rb module ActionDispatch module Http module URL def ssl? @env['SERVER_PORT'].to_s == '443' || @env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https' end end end end
I was pretty sure the Rails app was getting the port number, and SSL is typically handled over port 443, so I rewrote the method to take that into account. Admittedly it's a less than ideal solution, but it was the best I could do without messing up all my clean Application.rb code. So I uploaded, did a
touch tmp/restart.txt, and…
…I still got the dreaded too many redirects error! That was too much for one night, so I picked it back up the next day, this time probing the output of the
ssl_required evaluation. To my astonishment,
Rails.env.production? and not request.ssl? always evaluates to true! Obviously, the first operand is working properly on the production server, but after probing just the
request.ssl? part, it appears to be working correctly as well.
Of all things, it appears Ruby 1.9.2 has some boolean bug, because I rewrote the
ssl_required = Rails.env.production? and not request.ssl? # ...to... ssl_required = !request.ssl? and Rails.env.production?
Essentially, I dropped the more readable "and not" syntax and went back to the more terse "!" operator. After a restart, bingo! I am still befuddled as to why my original syntax doesn't work properly, but in the meantime I am quite satisfied to have working SSL redirection.
The initial SSL markup was pretty easy to implement, but if you're looking for a gemified way to setup SSL requirements, I recommend the
ssl_requirement gem. The gem's approach is almost identical to the mine, however it has some "prettier" methods you can call. As I already had some basic authentication in place and have not migrated to full user accounts yet, I needed a custom solution that would automatically be called alongside the authenticate filter.
request.ssl? override I wrote will not load automatically in Rails 3 since it is a core class — to autoload lib files, take a look at the
auto_require post to auto-override the core
Finally, if anyone has any insight on the syntax quirk or quick/easy solutions to get Passenger 3.0.7 to pass along the SSL headers, you are welcome to enlighten me in the comments.