Secret URLs and file downloads


#1

I’m looking to do something similar to the “secret URLs” in Rails
Recipes, however, with file downloads. I want to avoid providing direct
URLs to people if possible.

I have files on disk in public/, so it would be good if each user got a
“unique” URL to each file that they are permitted to see (dealt with by
a user_id/file_id table). Then they are only allowed to download the
file if they are logged in and the secret matches. Otherwise, anybody
could simply point to http://…/example.pdf and download the file,
whether logged in or not.

I could do this with redirect_to I believe, but as far as I know that
just sends a 3xx redirect to the browser which would point to the real
file.

Is there any easy way to do this?

Cheers.


#2

first off, if you don’t want someone to have direct access to the files
(ie,
http://www.yoursite.com/file1.xyz, where fie1.xyz is stored in the
public
dir), then don’t store them in public. move them outside of that
directory
and store the file information (path, name, etc) in the database.

second, for a simple example, you could store a hash of the
filename/mtime/etc in the database along with the file information. in
the
controller, make sure you have some sort of security in place that says
the
person has to be logged in and have access to the file, then you could
have
a url something like:

http://www.yoursite.com/media/download/

so you might have:

class MediaController < ApplicationController::Base
def download
media = Media.find_by_md5_hash(@params[:md5_hash])
unless media.nil?
#make sure that the user has permission to get at this file
send_file media.filepath
else
#oops, no file!
end
end
end

and your route might be:

map.connect “/media/download/:md5_hash”, :controller => “media”, :action
=>
“download”, :md5_hash => /[0-9a-zA-Z]/


#3

What if you don’t place them in public and use a pfd controller with a
send_data
action to push the image to the browser…


#4

I you use lighttpd, then I would use mod_secdownload, which does all you
want and even more (the generated URLs are only valid for a certain
time,
you can set)


#5

On Mar 20, 2006, at 10:58 AM, Tom M. wrote:

Never write a line of code if you can avoid it. :slight_smile:
Fixing a typo!

…and the hash would end up in params[:id]…

P.S. Do not use @params! That directly accesses the params hash, but
I’ve heard core
team members recommend the params method call to insulate
yourself from possible
future change to the internal representations.


– Tom M.


#6

On Mar 20, 2006, at 7:34 AM, Chris H. wrote:

access to the file, then you could have a url something like:
send_file media.filepath
else
#oops, no file!
end
end
end

and your route might be:

map.connect “/media/download/:md5_hash”, :controller =>
“media”, :action => “download”, :md5_hash => /[0-9a-zA-Z]/

This is a great solution, IMHO.

Only question is: Why make a new route?

The default route would work just as well, and the hash would end up
in param[:id]

Never write a line of code if you can avoid it. :slight_smile:


#7

On Mar 20, 2006, at 10:58 AM, Tom M. wrote:

information. in the controller, make sure you have some sort of
unless media.nil?
map.connect “/media/download/:md5_hash”, :controller =>


– Tom M.

Keep in mind that using send_file like this with large files will ty

up one fcgi listsner for each download that is happening for the
length of the download. SO if you have five users downloading one
large file each then you will have 5 fcgi processes tied up until
finished! So mod_secdownload is a good option to look at if you are
suing lighttpd. Otherwise you can easily run out of fcgi’s and lock
up your whole app!

Cheers-
-Ezra


#8

never heard of mod_secdownload before this. neat. much better than my
suggestion! :slight_smile:

kudos.

Chris


#9

On Mar 20, 2006, at 4:20 PM, Tom M. wrote:

information (path, name, etc) in the database.

end
The default route would work just as well, and the hash would end
one large file each then you will have 5 fcgi processes tied up
backend process quickly.

Does anybody know how large Lighttpd’s FCGI buffer is, and whether
or not it’s configurable?


– Tom M.

I'm fairly certain that send_file with lighttpd will just ty up the

fcgi proc’s but I would love to be proven wrong. I haven’t seen any
options to configure fcgi buffers anywhere.

-Ezra


#10

On Mar 20, 2006, at 2:09 PM, Ezra Z. wrote:

second, for a simple example, you could store a hash of the
def download
and your route might be:

suing lighttpd. Otherwise you can easily run out of fcgi’s and lock
up your whole app!

If you’re using a front end proxy, this may not be the case.

I seem to remember that Apache+mod_proxy allows you to specify the
buffer size, and if the
buffer was large enough, you could deliver large files into that
buffer and free up the
backend process quickly.

Does anybody know how large Lighttpd’s FCGI buffer is, and whether or
not it’s configurable?


#11

On Mar 20, 2006, at 4:38 PM, Ezra Z. wrote:

Only question is: Why make a new route?
will ty up one fcgi listsner for each download that is happening
buffer was large enough, you could deliver large files into that
the fcgi proc’s but I would love to be proven wrong. I haven’t seen
any options to configure fcgi buffers anywhere.

I cannot prove this directly, but if Lighty properly implements the
FCGI spec as recommended, then the FCGI response should be buffered
by the HTTP server:

from: http://www.fastcgi.com/devkit/doc/fcgi-spec.html


#12

I’ve written an RSS feed parser/downloader using Ruby and ActiveRecord
and
http-access2 without Rails. I want this code to always be running in a
loop and downloading feeds on a schedule. I started by coding the
back-end (downloading feeds and putting the titles, links, and
descriptions into a database). Now, I’m wondering, is there an easier
way…

I suppose it is possible to invoke this code as needed when a user loads
a
page, however, it seems like it’d be easier if there was a rails
front-end
that managed the feed urls, but not the actual feed data itself.

I guess my question is, can a pure ruby process to handle this perpetual
munging of rss data be spawned from within rails? Or is it better to
just
spawn this back end code and run a database updater in an infinite loop?
Does anyone know how Odeo handles this?

Tony
http://involution.com


#13

Chris H. wrote:

first off, if you don’t want someone to have direct access to the files
(ie,
http://www.yoursite.com/file1.xyz, where fie1.xyz is stored in the
public
dir), then don’t store them in public. move them outside of that
directory
and store the file information (path, name, etc) in the database.

second, for a simple example, you could store a hash of the
filename/mtime/etc in the database along with the file information. in
the
controller, make sure you have some sort of security in place that says
the
person has to be logged in and have access to the file, then you could
have
a url something like:

http://www.yoursite.com/media/download/

so you might have:

class MediaController < ApplicationController::Base
def download
media = Media.find_by_md5_hash(@params[:md5_hash])
unless media.nil?
#make sure that the user has permission to get at this file
send_file media.filepath
else
#oops, no file!
end
end

Thanks very much for this. The site will be very low usage, so it
doesn’t matter too much if the process is tied up for a while. I will
investigate the other modules suggested too, to see whether they’re easy
enough to use for the site.

Thanks to everybody who responded,

David


#14

On 3/21/06, Tony P. removed_email_address@domain.invalid wrote:

I guess my question is, can a pure ruby process to handle this perpetual
munging of rss data be spawned from within rails? Or is it better to just
spawn this back end code and run a database updater in an infinite loop?
Does anyone know how Odeo handles this?

I think that the wiki page
http://wiki.rubyonrails.com/rails/pages/HowToRunBackgroundJobsInRails
can help you some.

Tony
http://involution.com

Sincerely,

Tom L.
http://AllTom.com/
http://GadgetLife.org/


#15

I may be missing something but here is how I may be solving a similar
problem for an upcoming problem. Downloading files will be a large part
of my application.

Have two web servers, mapped to different IP’s.

The application server
IP: 192.168.2.1
myapp.mydomain.com
will use one of the yet-to-be-determined scalable Rails implementations

The download server
IP: 192.168.2.2
Will use IIS or Apache

I’ll use one of the Apache plugins to “expire” certain downloads or, in
the case of IIS, will write a simple scheduled task to crawl the site
and delete old files.

For my purposes, I’m finding it easier to manage downloads on a separate
non-Rails server.


#16

If you’re using Apache, mod_auth_token supports the same functionality
and interface as mod_secdownload, eg. allowing you to secure and expire
downloads
without having to pipe the file through your script.

http://www.synd.info/downloads/releases/

//Mikael

David wrote:

I’m looking to do something similar to the “secret URLs” in Rails
Recipes, however, with file downloads. I want to avoid providing direct
URLs to people if possible.

I have files on disk in public/, so it would be good if each user got a
“unique” URL to each file that they are permitted to see (dealt with by
a user_id/file_id table). Then they are only allowed to download the
file if they are logged in and the secret matches. Otherwise, anybody
could simply point to http://…/example.pdf and download the file,
whether logged in or not.

I could do this with redirect_to I believe, but as far as I know that
just sends a 3xx redirect to the browser which would point to the real
file.

Is there any easy way to do this?

Cheers.


#17

sorry for the noob question, but when you say “logged in” all that means
is that you have a session file that is insync with the rails
application?