10 enable :inline_templates
12 set :upload_password, '0e5f7d398e6f9cd1f6bac5cc823e363aec636495'
13 set :filename_length, 20
14 set :random_pass_length, 16
15 set :lockfile_options, { :timeout => 60,
23 def self.open(path, pass)
24 StoredFile.new(path, pass)
29 yield @initial_content
30 @initial_content = nil
31 until (buf = @file.read(BUFFER_LEN)).nil?
32 yield @cipher.update(buf)
43 def self.create(src, pass, meta)
45 clear_meta = { "Coquelicot" => COQUELICOT_VERSION,
46 "Salt" => Base64.encode64(salt).strip }
47 yield YAML.dump(clear_meta) + YAML_START
49 cipher = get_cipher(pass, salt, :encrypt)
50 yield cipher.update(YAML.dump(meta) + YAML_START)
52 while not (buf = src.read(BUFFER_LEN)).nil?
53 yield cipher.update(buf)
61 CIPHER = 'AES-256-CBC'
64 COQUELICOT_VERSION = "1.0"
66 def self.get_cipher(pass, salt, method)
67 hmac = OpenSSL::PKCS5.pbkdf2_hmac_sha1(pass, salt, 2000, 48)
68 cipher = OpenSSL::Cipher.new CIPHER
69 cipher.method(method).call
70 cipher.key = hmac[0..31]
71 cipher.iv = hmac[32..-1]
76 OpenSSL::Random::random_bytes(SALT_LEN)
79 def initialize(path, pass)
80 @file = File.open(path)
81 if YAML_START != (buf = @file.read(YAML_START.length)) then
82 raise "unknown file, read #{buf.inspect}"
85 init_decrypt_cipher pass
91 until YAML_START == (line = @file.readline) do
94 @meta = YAML.load(meta)
95 if @meta["Coquelicot"].nil? or @meta["Coquelicot"] != COQUELICOT_VERSION then
100 def init_decrypt_cipher(pass)
101 salt = Base64.decode64(@meta["Salt"])
102 @cipher = StoredFile::get_cipher(pass, salt, :decrypt)
107 buf = @file.read(BUFFER_LEN)
108 content = @cipher.update(buf)
109 raise "bad key" unless content.start_with? YAML_START
111 block = content.split(YAML_START, 3)
113 if block.length == 3 then
114 @initial_content = block[2]
115 @meta.merge! YAML.load(yaml)
119 until (buf = @file.read(BUFFER_LEN)).nil? do
120 block = @cipher.update(buf).split(YAML_START, 3)
122 break if block.length == 2
124 @initial_content = block[1]
125 @meta.merge! YAML.load(yaml)
129 @cipher.reset unless @cipher.nil?
137 attr_accessor :path, :lockfile_options, :filename_length
139 def add_file(src, pass, options)
142 dst = gen_random_file_name
143 File.open(full_path(dst), 'w').close
146 File.open(full_path(dst), 'w') do |dest|
147 StoredFile.create(src, pass, options) { |data| dest.write data }
150 File.unlink full_path(dst)
153 link = gen_random_file_name
158 def get_file(link, pass)
159 name = read_link(link)
160 return nil if name.nil?
161 StoredFile::open(full_path(name), pass)
164 def file_exists?(link)
165 name = read_link(link)
172 Lockfile.new "#{@path}/.lock", @lockfile_options
179 def add_link(src, dst)
181 File.open(links_path, 'a') do |f|
182 f.write("#{src} #{dst}\n")
190 File.open(links_path, 'r+') do |f|
191 f.readlines.each do |l|
192 links << l unless l.start_with? "#{src} "
204 File.open(links_path) do |f|
207 if line.start_with? "#{src} " then
211 end until line.empty?
217 def gen_random_file_name
219 name = gen_random_base32(@filename_length)
220 end while File.exists?(full_path(name))
229 @depot unless @depot.nil?
231 @depot = Depot.instance
232 @depot.path = options.depot_path if @depot.path.nil?
233 @depot.lockfile_options = options.lockfile_options if @depot.lockfile_options.nil?
234 @depot.filename_length = options.filename_length if @depot.filename_length.nil?
238 # Like RFC 4648 (Base32)
239 FILENAME_CHARS = %w(a b c d e f g h i j k l m n o p q r s t u v w x y z 2 3 4 5 6 7)
240 def gen_random_base32(length)
242 OpenSSL::Random::random_bytes(length).each_byte do |i|
243 name << FILENAME_CHARS[i % FILENAME_CHARS.length]
248 gen_random_base32(options.random_pass_length)
251 def password_match?(password)
252 return TRUE if settings.upload_password.nil?
253 (not password.nil?) && Digest::SHA1.hexdigest(password) == settings.upload_password
257 content_type 'text/css', :charset => 'utf-8'
265 get '/ready/:link' do |link|
266 link, pass = link.split '-' if link.include? '-'
267 unless depot.file_exists? link then
270 base = request.url.gsub(/\/ready\/[^\/]*$/, '')
271 @url = "#{base}/#{link}-#{pass}"
275 get '/:link' do |link|
276 link, pass = link.split '-' if link.include? '-'
277 file = depot.get_file(link, pass)
278 not_found if file.nil?
280 last_modified file.mtime.httpdate
281 attachment file.meta['Filename']
282 response['Content-Length'] = "#{file.meta['Length']}"
283 response['Content-Type'] = file.meta['Content-Type'] || 'application/octet-stream'
284 throw :halt, [200, file]
288 unless password_match? params[:upload_password] then
291 if params[:file] then
292 tmpfile = params[:file][:tempfile]
293 name = params[:file][:filename]
295 if tmpfile.nil? || name.nil? then
296 @error = "No file selected"
299 src = params[:file][:tempfile]
300 pass = gen_random_pass
301 link = depot.add_file(
303 { "Filename" => params[:file][:filename],
304 "Length" => src.stat.size,
305 "Content-Type" => params[:file][:type]
307 redirect "ready/#{link}-#{pass}"
312 url = request.scheme + "://"
314 if request.scheme == "https" && request.port != 443 ||
315 request.scheme == "http" && request.port != 80
316 url << ":#{request.port}"
318 url << request.script_name
329 %base{ :href => base_href }
330 %link{ :rel => 'stylesheet', :href => "style.css", :type => 'text/css',
331 :media => "screen, projection" }
332 %script{ :type => 'text/javascript', :src => 'javascripts/jquery.min.js' }
333 %script{ :type => 'text/javascript', :src => 'javascripts/jquery.lightBoxFu.js' }
334 %script{ :type => 'text/javascript', :src => 'javascripts/jquery.uploadProgress.js' }
335 %script{ :type => 'text/javascript', :src => 'javascripts/coquelicot.js' }
344 %form#upload{ :enctype => 'multipart/form-data',
345 :action => 'upload', :method => 'post' }
347 %input{ :type => 'file', :name => 'file' }
349 %input{ :type => 'submit', :value => 'Send file' }
354 %a{ :href => @url }= @url
360 background-color: $green
365 text-decoration: underline
369 background-color: red
371 border: black solid 1px
379 background: url('images/ajax-loader.gif') no-repeat