4

To start with, this sounds more like a bug then anything else.

My rails application is served by Unicorn. Then, using Nginx as a reverse proxy, I serve the application to the outside world using SSL.

So far so good, no problem. I'm using relative paths (Restful path helpers), so there should be no problem to produce this (for https://www.example.com):

new_entry_path => https://www.example.com/entries/new

This works fine in most cases.

The problem however appears when in a controller I try to redirect to a "show" action (using resources), let's say after a successful update (suppose Entry with id 100):

redirect_to @entry, flash: {success: "Entry has been updated"}

or

redirect_to entry_path(@entry), flash: {success: "Entry has been updated"}

they both produce a redirect to:

http://www.example.com/entries/100 # missing 's' in https...

instead of

/entries/100 # implying https://www.example.com/entries/100

As far as I've noticed, this only happens with show action and only in controller redirects.

I'm bypassing this by doing something horrible and disgusting:

redirect_to entry_url(@entry).sub(/^http\:/,"https:"), flash: {success: "Entry has been updated"}

Has anyone ever confronted something similar? Any ideas will be gratefully accepted...

Ruby Racer
  • 5,690
  • 1
  • 26
  • 43
  • have your tried setting the default host? http://stackoverflow.com/questions/2660172/how-do-i-set-default-host-for-url-helpers-in-rails – max Sep 26 '14 at 21:48
  • I've seen that, but I'm trying to make the object_path helpers work, not the object_url helpers... Is this going to help me? – Ruby Racer Sep 26 '14 at 21:50
  • I believe so since `redirect_to` uses `url_for` [under the hood](https://github.com/rails/rails/blob/3e006d5076a393c827fba69bf72bc36b7abf921b/actionpack/lib/action_controller/metal/redirecting.rb#L65) – max Sep 26 '14 at 21:58
  • Btw. what does `ENV["HTTP_HOST"]` output on your server? – max Sep 26 '14 at 22:04
  • running it in ERB it gives out nil... I have however found a good workaround in Nginx, which I'll post as answer, but it's not 100% satisfactory... "object_path" should be `"/objects/nnn"`, not `"http://whatever/objects/nnn"`... – Ruby Racer Sep 26 '14 at 22:09

3 Answers3

7

I had a similar problem. By default Rails will use the current protocol for the *_url helpers.

We use nginx as web server and unicorn as application server. Nginx takes the request, unwrapps the SSL part and then passes it on to unicorn. Hence, unicorn always receives a http request. If we now want Rails to know about the original protocol, we can need to add the X-Forwarded-Proto header.

Example config:

upstream app {
    server unix:/var/run/myserver/unicorn.sock fail_timeout=0;
}
server {
    listen       443 ssl;
    server_name  myserver;

    ssl_certificate "/etc/pki/nginx/myserver.crt";
    ssl_certificate_key "/etc/pki/nginx/private/myserver.key";

    root /myserver/public;

    try_files $uri/index.html $uri @app;
    sendfile on;

    location @app {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https; # <--- will be used
        proxy_set_header Host $http_host;
        proxy_redirect off;
        proxy_pass http://app;
   }
}
Motine
  • 1,638
  • 18
  • 18
  • I've solved a similar issue but using apache2 as the web server adding `RequestHeader set X-Forwarded-Proto "https"` to the configuration. Thanks! – Vargas Dec 05 '19 at 20:48
1

So far, I've managed to get a workaround by adding a rewrite rule to Nginx, under plain http:

rewrite ^/(.*)$  https://www.example.com/$1? permanent;

Which redirects all plain http requests to the https server.

Update:

Apparently, as I want the app not to care about how the front web server is serving it, it's up to the same web server to "clean the mess" of redirections the app by itself cannot handle unless it's been specifically configured (not desired). So, I will stick to this answer (workaround rather...)

UPDATE
Following papirtiger's answer, I saw that I ended up missing the flashes, which should be added to the overriding redirect_to as a parameter.

But I've found a way to do my life a lot easier, simply by overriding a different function, that is called from within redirect_to.

def _compute_redirect_to_location(options) #:nodoc:
    case options
    # The scheme name consist of a letter followed by any combination of
    # letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
    # characters; and is terminated by a colon (":").
    # See http://tools.ietf.org/html/rfc3986#section-3.1
    # The protocol relative scheme starts with a double slash "//".
    when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/i
        options
    ## WHEN STRING: THIS IS REMOVED TO AVOID ADDING PROTOCOL AND HOST ##
    # when String
    #   request.protocol + request.host_with_port + options
    when :back
        request.headers["Referer"] or raise RedirectBackError
    when Proc
        _compute_redirect_to_location options.call
    else
        url_for(options)
    end.delete("\0\r\n")
end

Thus, without having to change anything else in my code, I have a working relative redirect.

Ruby Racer
  • 5,690
  • 1
  • 26
  • 43
1

I think force_ssl is what you are looking for.

class AccountsController < ApplicationController
  force_ssl if: :ssl_configured?

  def ssl_configured?
    !Rails.env.development?
  end
end

Edit, if you really want to do redirects to relative paths you could always create your own helper:

module ActionController
  module RelativeRedirectingHelper
    extend ActiveSupport::Concern

    include AbstractController::Logger
    include ActionController::RackDelegation
    include ActionController::UrlFor

    def redirect_to_relative(path, response_status = 302) #:doc:
      raise ActionControllerError.new("Cannot redirect to nil!") unless options
      raise ActionControllerError.new("Cannot redirect to a parameter hash!") if options.is_a?(ActionController::Parameters)
      raise AbstractController::DoubleRenderError if response_body

      self.status        = response_status
      self.location      = path
      self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.h(location)}\">redirected</a>.</body></html>"
    end
  end
end

This is a quick'n'dirty copy paste job . Will take a little more effort if you want have the same signature as redirect_to

max
  • 96,212
  • 14
  • 104
  • 165
  • This is another workaround... But I don't mean for the application to "care" if it's ssl or not, I leave to the reverse proxy to do the ssl-thingy... What I'm looking for, is for `something_path` to work as `something_path` and not as `something_url`... – Ruby Racer Sep 27 '14 at 14:32
  • 1
    `redirect_to` will try to make it an absolute url since according to the current version of the HTTP/1.1 standard, RFC 2616, the value of the Location header must be an absolute URI. – max Sep 27 '14 at 16:47
  • 1
    You could always make your own redirect helper, take a look at https://github.com/rails/rails/blob/3e006d5076a393c827fba69bf72bc36b7abf921b/actionpack/lib/action_controller/metal/redirecting.rb#L65 – max Sep 27 '14 at 16:51
  • Added example of redirect helper – max Sep 27 '14 at 17:02
  • I see your point... So there is no *legitimate* working solution other than using the proxy to do the dirty work. I understand your points perfectly and it seems RFC get in the way of what I'm trying to do. Upvoting for the great answer nonetheless. Thanks. – Ruby Racer Sep 27 '14 at 22:42
  • 1
    No Ruby knowledge here, but there is such a thing as Protocol Relative URL: http://stackoverflow.com/questions/4831741/can-i-change-all-my-http-links-to-just –  Sep 29 '14 at 07:18
  • @Melvyn, thanks for the tip. I've updated my answer to use relative redirects. – Ruby Racer Sep 30 '14 at 20:49
  • @papirtiger, thanks a lot, your assistance has been more than valuable. You can check my updated answer to see what I've done. – Ruby Racer Sep 30 '14 at 20:50