implement imap authentication
[coquelicot.git] / coquelicot_app.rb
1 $:.unshift File.join(File.dirname(__FILE__), 'lib')
2
3 require 'sinatra/base'
4 require 'haml'
5 require 'sass'
6 require 'digest/sha1'
7 require 'gettext'
8 require 'coquelicot'
9 require 'haml_gettext'
10
11 module Coquelicot
12   class StoredFile
13     def lockfile
14       @lockfile ||= Lockfile.new "#{File.expand_path(@path)}.lock", :timeout => 4
15     end
16
17     def each
18       # output content
19       yield @initial_content
20       @initial_content = nil
21       until (buf = @file.read(BUFFER_LEN)).nil?
22         yield @cipher.update(buf)
23       end
24       yield @cipher.final
25       @fully_sent = true
26     end
27
28     def close
29       if @cipher
30         @cipher.reset
31         @cipher = nil
32       end
33       @file.close
34       if one_time_only?
35         empty! if @fully_sent
36         lockfile.unlock
37       end
38     end
39   end
40
41   class << self
42     def settings
43       (class << self; Application; end)
44     end
45     def depot
46       @depot = Depot.new(settings.depot_path) if @depot.nil? || settings.depot_path != @depot.path
47       @depot
48     end
49   end
50
51   class Application < Sinatra::Base
52     def self.authentication_method(method,options={})
53       require "coquelicot/auth/#{method}"
54       set :auth_method, method
55       include (eval "Coquelicot::Auth::#{method.to_s.capitalize}")
56       options.each{|k,v| set k,v }
57     end
58
59     set :app_file, __FILE__
60     set :default_expire, 60
61     set :maximum_expire, 60 * 24 * 30 # 1 month
62     set :gone_period, 10080
63     set :filename_length, 20
64     set :random_pass_length, 16
65     set :depot_path, Proc.new { File.join(root, 'files') }
66
67     authentication_method :simplepass, :upload_password => 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3'
68
69     GetText::bindtextdomain('coquelicot')
70     before do
71       GetText::set_current_locale(params[:lang] || request.env['HTTP_ACCEPT_LANGUAGE'] || 'en')
72     end
73
74     get '/style.css' do
75       content_type 'text/css', :charset => 'utf-8'
76       sass :style
77     end
78
79     get '/' do
80       haml :index
81     end
82
83     get '/random_pass' do
84       "#{Coquelicot.gen_random_pass}"
85     end
86
87     get '/ready/:link' do |link|
88       link, pass = link.split '-' if link.include? '-'
89       begin
90         file = Coquelicot.depot.get_file(link, nil)
91       rescue Errno::ENOENT => ex
92         not_found
93       end
94       @expire_at = file.expire_at
95       @base = request.url.gsub(/\/ready\/[^\/]*$/, '')
96       @name = "#{link}"
97       unless pass.nil?
98         @name << "-#{pass}"
99         @unprotected = true
100       end
101       @url = "#{@base}/#{@name}"
102       haml :ready
103     end
104
105     post '/authenticate' do
106       pass unless request.xhr?
107       unless authenticate(params) then
108         error 403, "Forbidden"
109       end
110       'OK'
111     end
112
113     post '/upload' do
114       # if JS is disabled upload_token might be nil
115       params['upload_token'] = JSON.parse(params['upload_token']) unless params['upload_token'].nil?
116       unless authenticate(params) then
117         error 403
118       end
119       if params[:file] then
120         tmpfile = params[:file][:tempfile]
121         name = params[:file][:filename]
122       end
123       if tmpfile.nil? || name.nil? then
124         @error = "No file selected"
125         return haml(:index)
126       end
127       if tmpfile.lstat.size == 0 then
128         @error = "#{name} is empty"
129         return haml(:index)
130       end
131       if params[:expire].nil? or params[:expire].to_i == 0 then
132         params[:expire] = settings.default_expire
133       elsif params[:expire].to_i > settings.maximum_expire then
134         error 403
135       end
136       expire_at = Time.now + 60 * params[:expire].to_i
137       one_time_only = params[:one_time] and params[:one_time] == 'true'
138       if params[:file_key].nil? or params[:file_key].empty?then
139         pass = Coquelicot.gen_random_pass
140       else
141         pass = params[:file_key]
142       end
143       src = params[:file][:tempfile]
144       link = Coquelicot.depot.add_file(
145          src, pass,
146          { "Expire-at" => expire_at.to_i,
147            "One-time-only" => one_time_only,
148            "Filename" => params[:file][:filename],
149            "Length" => src.stat.size,
150            "Content-Type" => params[:file][:type],
151          })
152       redirect "ready/#{link}-#{pass}" if params[:file_key].nil? or params[:file_key].empty?
153       redirect "ready/#{link}"
154     end
155
156     def expired
157       throw :halt, [410, haml(:expired)]
158     end
159
160     def send_stored_file(file)
161       last_modified file.created_at.httpdate
162       attachment file.meta['Filename']
163       response['Content-Length'] = "#{file.meta['Length']}"
164       response['Content-Type'] = file.meta['Content-Type'] || 'application/octet-stream'
165       throw :halt, [200, file]
166     end
167
168     def send_link(link, pass)
169       file = Coquelicot.depot.get_file(link, pass)
170       return false if file.nil?
171       return expired if file.expired?
172
173       if file.one_time_only?
174         begin
175           # unlocking done in file.close
176           file.lockfile.lock
177         rescue Lockfile::TimeoutLockError
178           error 409, "Download currently in progress"
179         end
180       end
181       send_stored_file(file)
182     end
183
184     get '/:link-:pass' do |link, pass|
185       link = Coquelicot.remap_base32_extra_characters(link)
186       pass = Coquelicot.remap_base32_extra_characters(pass)
187       not_found unless send_link(link, pass)
188     end
189
190     get '/:link' do |link|
191       link = Coquelicot.remap_base32_extra_characters(link)
192       not_found unless Coquelicot.depot.file_exists? link
193       @link = link
194       haml :enter_file_key
195     end
196
197     post '/:link' do |link|
198       pass = params[:file_key]
199       return 403 if pass.nil? or pass.empty?
200       begin
201         # send Forbidden even if file is not found
202         return 403 unless send_link(link, pass)
203       rescue Coquelicot::BadKey => ex
204         403
205       end
206     end
207
208     helpers do
209       def base_href
210         url = request.scheme + "://"
211         url << request.host
212         if request.scheme == "https" && request.port != 443 ||
213             request.scheme == "http" && request.port != 80
214           url << ":#{request.port}"
215         end
216         url << request.script_name
217         "#{url}/"
218       end
219
220       def auth_method
221         Coquelicot.settings.auth_method
222       end
223     end
224   end
225 end
226
227 Coquelicot::Application.run! if __FILE__ == $0