11

I'm trying to find the best way to make paperclip urls secure, but only for secure pages.

For instance, the homepage, which shows images stored in S3, is http://mydomain.com and the image url is http://s3.amazonaws.com/mydomainphotos/89/thisimage.JPG?1284314856.

I have secure pages like https://mydomain.com/users/my_stuff/49 that has images stored in S3, but the S3 protocol is http and not https, so the user gets a warning from the browser saying that some elements on the page are not secure, blah blah blah.

I know that I can specify :s3_protocol in the model, but this makes everything secure even when it isn't necessary. So, I'm looking for the best way to change the protocol to https on the fly, only for secure pages.

One (probably bad) way would be to create a new url method like:

def custom_url(style = default_style, ssl = false)
  ssl ? self.url(style).gsub('http', 'https') : self.url(style)
end

One thing to note is that I'm using the ssl_requirement plugin, so there might be a way to tie it in with that.

I'm sure there is some simple, standard way to do this that I'm overlooking, but I can't seem to find it.

Shagymoe
  • 1,296
  • 1
  • 15
  • 22

4 Answers4

17

If anyone stumbles upon this now: There is a solution in Paperclip since April 2012! Simply write:

Paperclip::Attachment.default_options[:s3_protocol] = ""

in an initializer or use the s3_protocol option inside your model.

Thanks to @Thomas Watson for initiating this.

emrass
  • 6,253
  • 3
  • 35
  • 57
  • This is the official way to do scheme-less URLs now. – Karew Oct 10 '12 at 14:29
  • 1
    Make sure you are using version 3.1.4 or greater. Earlier versions have a bug which includes the protocol colon, which will break all your image links. Also, be sure to restart your server when upgrading versions. – vansan Jul 30 '13 at 18:28
  • See also http://www.rubydoc.info/github/thoughtbot/paperclip/Paperclip/Storage/S3 – Topher Hunt Nov 20 '14 at 13:37
  • This worked for me with paperclip 3.0.4, putting the line in the paperclip ninitializer, but not in the model. – Nick Ellis Mar 29 '18 at 15:43
  • Thank your for the feedback, @NickEllis. I'll wait for someone to confirm and will then update the answer if required. – emrass Apr 02 '18 at 22:48
7

If using Rails 2.3.x or newer, you can use Rails middleware to filter the response before sending it back to the user. This way you can detect if the current request is an HTTPS request and modify the calls to s3.amazonaws.com accordingly.

Create a new file called paperclip_s3_url_rewriter.rb and place it inside a directory that's loaded when the server starts. The lib direcotry will work, but many prefer to create an app/middleware directory and add this to the Rails application load path.

Add the following class to the new file:

class PaperclipS3UrlRewriter
  def initialize(app)
    @app = app
  end

  def call(env)
    status, headers, response = @app.call(env)
    if response.is_a?(ActionController::Response) && response.request.protocol == 'https://' && headers["Content-Type"].include?("text/html")
      body = response.body.gsub('http://s3.amazonaws.com', 'https://s3.amazonaws.com')
      headers["Content-Length"] = body.length.to_s
      [status, headers, body]
    else
      [status, headers, response]
    end
  end
end

Then just register the new middleware:

Rails 2.3.x: Add the line below to environment.rb in the beginning of the Rails::Initializer.run block.
Rails 3.x: Add the line below to application.rb in the beginning of the Application class.

config.middleware.use "PaperclipS3UrlRewriter"

UPDATE:
I just edited my answer and added a check for response.is_a?(ActionController::Response) in the if statement. In some cases (maybe caching related) the response object is an empty array(?) and hence fails when request is called upon it.

UPDATE 2: I edited the Rack/Middleware code example above to also update the Content-Length header. Otherwise the HTML body will be truncated by most browsers.

Thomas Watson
  • 6,507
  • 5
  • 33
  • 43
  • 3
    It seems unfortunate to inject an S3 url-rewriter into every single request your application serves instead of encapsulating logic to generate the proper URLs in the first place. – Winfield Oct 26 '11 at 17:22
  • Indeed @Winfield, evaluating an unbound regular expression across every response body is backwards. Use a solution that generates the correct URL's in the first place. – Mars Aug 10 '15 at 17:01
1

Use the following code in a controller class:

# locals/arguments/methods you must define or have available:
#   attachment - the paperclip attachment object, not the ActiveRecord object
#   request - the Rack/ActionController request
AWS::S3::S3Object.url_for \
  attachment.path,
  attachment.options[:bucket].to_s,
  :expires_in => 10.minutes, # only necessary for private buckets
  :use_ssl => request.ssl?

You can of course wrap this up nicely into a method.

yfeldblum
  • 65,165
  • 12
  • 129
  • 169
0

FYI - some of the answers above do not work with Rails 3+, because ActionController::Response has been deprecated. Use the following:

class PaperclipS3UrlRewriter
  def initialize(app)
    @app = app
  end

  def call(env)
    status, headers, response = @app.call(env)
    if response.is_a?(ActionDispatch::BodyProxy) && headers && headers.has_key?("Content-Type") && headers["Content-Type"].include?("text/html")
    body_string = response.body[0]
    response.body[0] = body_string.gsub('http://s3.amazonaws.com', 'https://s3.amazonaws.com')
    headers["Content-Length"] = body_string.length.to_s
    [status, headers, response]
  else
    [status, headers, response]
  end
end

end

And make sure that you add the middleware in a good place in the stack (I added it after Rack::Runtime)

config.middleware.insert_after Rack::Runtime, "PaperclipS3UrlRewriter" 
Eyal Kedem
  • 206
  • 2
  • 5