add license headers on source files
[coquelicot.git] / lib / coquelicot / app.rb
1 # Coquelicot: "one-click" file sharing with a focus on users' privacy.
2 # Copyright © 2010-2012 potager.org <jardiniers@potager.org>
3 #           © 2011 mh / immerda.ch <mh+coquelicot@immerda.ch>
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU Affero General Public License for more details.
14 #
15 # You should have received a copy of the GNU Affero General Public License
16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18 require 'lockfile'
19 require 'sinatra/base'
20 require 'sinatra/config_file'
21 require 'haml'
22 require 'haml/magic_translations'
23 require 'sass'
24 require 'digest/sha1'
25 require 'fast_gettext'
26
27 module Coquelicot
28   class << self
29     def settings
30       (class << self; Application; end)
31     end
32     def depot
33       @depot = Depot.new(settings.depot_path) if @depot.nil? || settings.depot_path != @depot.path
34       @depot
35     end
36   end
37
38   class Application < Sinatra::Base
39     register Sinatra::ConfigFile
40     register Coquelicot::Auth::Extension
41
42     set :root, Proc.new { app_file && File.expand_path('../../..', app_file) }
43     set :depot_path, Proc.new { File.join(root, 'files') }
44     set :default_expire, 60
45     set :maximum_expire, 60 * 24 * 30 # 1 month
46     set :gone_period, 60 * 24 * 7 # 1 week
47     set :filename_length, 20
48     set :random_pass_length, 16
49     set :about_text, ''
50     set :additional_css, ''
51     set :authentication_method, :name => :simplepass,
52                                 :upload_password => 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3'
53
54     config_file File.expand_path('../../../conf/settings.yml', __FILE__)
55
56     FastGettext.add_text_domain 'coquelicot', :path => 'po', :type => 'po'
57     FastGettext.available_locales = [ 'en', 'fr', 'de' ]
58     Haml::MagicTranslations.enable(:fast_gettext)
59     before do
60       FastGettext.text_domain = 'coquelicot'
61       FastGettext.locale = params[:lang] || request.env['HTTP_ACCEPT_LANGUAGE'] || 'en'
62     end
63
64     not_found do
65       'Not found'
66     end
67
68     get '/style.css' do
69       content_type 'text/css', :charset => 'utf-8'
70       sass :style
71     end
72
73     get '/' do
74       haml :index
75     end
76
77     get '/random_pass' do
78       "#{Coquelicot.gen_random_pass}"
79     end
80
81     get '/ready/:link' do |link|
82       not_found if link.nil?
83
84       link, pass = link.split '-' if link.include? '-'
85       begin
86         file = Coquelicot.depot.get_file(link, nil)
87       rescue Errno::ENOENT => ex
88         not_found
89       end
90       @expire_at = file.expire_at
91       @base = request.url.gsub(/\/ready\/[^\/]*$/, '')
92       @name = "#{link}"
93       unless pass.nil?
94         @name << "-#{pass}"
95         @unprotected = true
96       end
97       @url = "#{@base}/#{@name}"
98       haml :ready
99     end
100
101     post '/authenticate' do
102       pass unless request.xhr?
103       begin
104         unless authenticate(params)
105           error 403, "Forbidden"
106         end
107         'OK'
108       rescue Coquelicot::Auth::Error => ex
109         error 503, ex.message
110       end
111     end
112
113     post '/upload' do
114       begin
115         unless authenticate(params)
116           error 403, "Forbidden"
117         end
118       rescue Coquelicot::Auth::Error => ex
119         error 503, ex.message
120       end
121
122       if params[:file] then
123         tmpfile = params[:file][:tempfile]
124         name = params[:file][:filename]
125       end
126       if tmpfile.nil? || name.nil? then
127         @error = "No file selected"
128         return haml(:index)
129       end
130       if tmpfile.lstat.size == 0 then
131         @error = "#{name} is empty"
132         return haml(:index)
133       end
134       if params[:expire].nil? or params[:expire].to_i == 0 then
135         params[:expire] = settings.default_expire
136       elsif params[:expire].to_i > settings.maximum_expire then
137         error 403
138       end
139       expire_at = Time.now + 60 * params[:expire].to_i
140       one_time_only = params[:one_time] and params[:one_time] == 'true'
141       if params[:file_key].nil? or params[:file_key].empty?then
142         pass = Coquelicot.gen_random_pass
143       else
144         pass = params[:file_key]
145       end
146       src = params[:file][:tempfile]
147       link = Coquelicot.depot.add_file(
148          src, pass,
149          { "Expire-at" => expire_at.to_i,
150            "One-time-only" => one_time_only,
151            "Filename" => params[:file][:filename],
152            "Length" => src.stat.size,
153            "Content-Type" => params[:file][:type],
154          })
155       redirect "ready/#{link}-#{pass}" if params[:file_key].nil? or params[:file_key].empty?
156       redirect "ready/#{link}"
157     end
158
159     def expired
160       throw :halt, [410, haml(:expired)]
161     end
162
163     def send_stored_file(file)
164       last_modified file.created_at.httpdate
165       attachment file.meta['Filename']
166       response['Content-Length'] = "#{file.meta['Length']}"
167       response['Content-Type'] = file.meta['Content-Type'] || 'application/octet-stream'
168       throw :halt, [200, file]
169     end
170
171     def send_link(link, pass)
172       file = Coquelicot.depot.get_file(link, pass)
173       return false if file.nil?
174       return expired if file.expired?
175
176       if file.one_time_only?
177         begin
178           # unlocking done in file.close
179           file.lockfile.lock
180         rescue Lockfile::TimeoutLockError
181           error 409, "Download currently in progress"
182         end
183       end
184       send_stored_file(file)
185     end
186
187     get '/:link-:pass' do |link, pass|
188       not_found if link.nil? || pass.nil?
189
190       link = Coquelicot.remap_base32_extra_characters(link)
191       pass = Coquelicot.remap_base32_extra_characters(pass)
192       not_found unless send_link(link, pass)
193     end
194
195     get '/:link' do |link|
196       not_found if link.nil?
197
198       link = Coquelicot.remap_base32_extra_characters(link)
199       not_found unless Coquelicot.depot.file_exists? link
200       @link = link
201       haml :enter_file_key
202     end
203
204     post '/:link' do |link|
205       pass = params[:file_key]
206       return 403 if pass.nil? or pass.empty?
207       begin
208         # send Forbidden even if file is not found
209         return 403 unless send_link(link, pass)
210       rescue Coquelicot::BadKey => ex
211         403
212       end
213     end
214
215     helpers do
216       def base_href
217         url = request.scheme + "://"
218         url << request.host
219         if request.scheme == "https" && request.port != 443 ||
220             request.scheme == "http" && request.port != 80
221           url << ":#{request.port}"
222         end
223         url << request.script_name
224         "#{url}/"
225       end
226
227       def clone_url
228         settings.respond_to?(:clone_url) ? settings.clone_url : "#{base_href}coquelicot.git"
229       end
230
231       def authenticate(params)
232         Coquelicot.settings.authenticator.authenticate(params)
233       end
234
235       def auth_method
236         Coquelicot.settings.authenticator.class.name.gsub(/Coquelicot::Auth::([A-z0-9]+)Authenticator$/, '\1').downcase
237       end
238     end
239   end
240 end
241
242 Coquelicot::Application.run! if __FILE__ == $0