Protect Direct download of contents
Goal of this feature is to ensure that the contents (especially the ones used in courses) are not directly downloadable by unauthorized users.
Summary
Bug#0003852 has a brief descripiton of this issue.
Note: “contents” below is refered to as only documents, videos and audio files.
Right now all the contents can be directly downloaded from the url on which it is available. A user can view the html source of the page, get hold of the content url and directly paste the URL in the browser to download it. This means that any contents which is is part of a paid course can be easily downloaded and circulated. We need to prevent this or at least make it difficult for users to be able to directly download the contents.
Implementation Details
Earlier the contents that were uploaded by the user was getting created under rails_root/public/system/uploaded_datas folder. Since the files in public folder is directly served by the web server (without going via Rails request cycle), we are now saving the uploaded contents under rails_root/assets folder.
A rails action “/documents/download” will now serve the contents from “rails_root/assets” folder. This action does following checks
Checks if the current user is present OR if the site is public. If not then does not allow to download the file. This ensures that the files can be downloaded after login to the application. However, for sites that are not private it is possible that the contents is embeded into a blog, and a blog of a public site can be viewed without login. So basically for public sites, the check for current user is not done.
If the site is not public then it also checks if the current user has proper permissions to download the file. Generally if a user the owner of the content or is enrolled to a course which has the given content then he has the permission to download the file.
Checks if the download request is has valid token+timestamp. The
URL to download a content should also have a valid combination of token+secret in query string. This token+secret combination is generated dynamically by using SHA1 generated tokens. The whole idea behind this technique is that we will associate a timestamp with the url. Once the timestamp is expired / past the url will not be accessible. Right now the timestamp limit is set to expire after 1 minute. So, if the user views the html source and copys the content related url from the html source and tries to access it directly in the browser all within 1 minute then he can still download the content directly.
Blog posts can also have contents embeded into it. With this new implementation the url to serve the content is changed. However the existing blog posts that already have some contents embeded will still point to old url. To fix this a rake task is available that will fix the urls in existing blogs.
The above checks apply for contents other then dropbox items. For the dropbox contents we just check if the user is logged in. The user is allowed to directly download the contents if logged in.
Open Items
Videos that are avaibale via rtmp url (Streaming server) are not protected. The user can directly link to rtmp url and play the videos on their desktop players like VLC Some video players like VLC player allow to play videos that are available on a network stream. For example I can directly play a video located at rtmp://184.106.199.92/recorder/uploaded/652-vlc-record-2012-04-20-17h33m.mp4 (this is one of the video that is uploaded on dev01) via the rtmp url from VLC player. Such requests do not go via the rails stack (as its not http), so I guess some thing need to be done inside the streaming server itself to protect it.
In our case if the streaming server is not available then we serve the video directly via an http url (progressive download). In such cases, the following use case will fail
User opens the course player, which shows the video. The user has not yet started playing the video (Note that we expire the download urls after 1 minute)
User starts playing the video after 1 minute. Once user hits play, the request to download the video will be sent, however by this time the download url is expired, so the request to download the video will fail.
The content can still be downloaded directly if the user tries to directly download the content within the set expiry time (1 minute).
Scorm contents are not protected as it is finally served as html which we anyway cannot protect.
“.png” files that are generated from a document (for serving on mobile devices) are not protected.
“.ts” and “.m3u8” files that are generated for a video (for serving on mobile devices) are not protected.
Testing
Ensure that all the existing contents are loaded properly (both in desktop and mobile device).
Try to directly download the contents by getting hold of the content urls from html source. The html source can be viewed via tools like Firebug.
Existing blog posts that have contents (videos/docs) embeded into it should show up correctly.
Creation and viewing of new blog posts after embeding contents should work correctly. Note that there is 1 open item here. If the blog has video embeded into it and if the video is not played within couple of minutes then it dosent play. The page need to be refreshed to play it again.
Take backup
To ensure that we can rollback to previous setup take a backup of /uploads directory.
Also take a backup of posts table as the rake task will modify the contents of posts table. e.g “mysqldump -udevuser -p cb_devel posts > posts_sept102013.sql”
Update the Folder structure
Create a public folder inside /uploads and move the existing contents of /uploads to /uploads/public folder
cd /uploads
mkdir public
mv * public/
Create /uploads/protected folder. Will house all the protected contents.
mkdir protected
Create a symlink /deploy/crossbow/shared/assets/protected which points to /uploads/protected
cd /deploy/crossbow/shared/assets
ln -nfs /uploads/protected
Update the existing symlink /deploy/crossbow/shared/system/uploaded_datas to point to /uploads/public
cd /deploy/crossbow/shared/system
ln -nfs /uploads/public uploaded_datas
Change the deploy script
Modify the deploy.rb to add following line in the :symlink task
run "ln -nfs #{shared_path}/assets/protected #{release_path}/assets"
Rake tasks
bundle exec rake content:secure_user_contents
bundle exec rake content:fix_content_url_in_blogs