allow to limit file size through the max_file_size setting
authorLunar <lunar@anargeek.net>
Tue, 28 Feb 2012 18:26:47 +0000 (19:26 +0100)
committerLunar <lunar@anargeek.net>
Thu, 14 Mar 2013 09:12:08 +0000 (10:12 +0100)
conf/settings-default.yml
lib/coquelicot.rb
lib/coquelicot/app.rb
lib/coquelicot/num.rb [new file with mode: 0644]
lib/coquelicot/rack/upload.rb
spec/coquelicot/app_spec.rb
spec/coquelicot/rack/upload_spec.rb
views/index.haml
views/style.sass

index 8d9629f..7a77ffb 100644 (file)
@@ -9,6 +9,13 @@
 # These settings are only here for illustration purpose. Site specific
 # configuration only needs to specify the ones that need to be changed.
 
+# Maximum size allowed for uploaded files
+# (in bytes)
+#
+#   Default: 5242880 = 5 * 1024 * 1024
+#
+max_file_size: 5242880
+
 # Default expiration time (if unspecified by users)
 # (in minutes)
 #
index 60c7461..c12aa3d 100644 (file)
@@ -19,5 +19,6 @@ require 'coquelicot/auth'
 require 'coquelicot/stored_file'
 require 'coquelicot/depot'
 require 'coquelicot/rack/multipart_parser'
+require 'coquelicot/num'
 require 'coquelicot/rack/upload'
 require 'coquelicot/app'
index f338c4b..2618f2a 100644 (file)
@@ -43,6 +43,7 @@ module Coquelicot
 
     set :root, Proc.new { app_file && File.expand_path('../../..', app_file) }
     set :depot_path, Proc.new { File.join(root, 'files') }
+    set :max_file_size, 5 * 1024 * 1024 # 5 MiB
     set :default_expire, 60
     set :maximum_expire, 60 * 24 * 30 # 1 month
     set :gone_period, 60 * 24 * 7 # 1 week
diff --git a/lib/coquelicot/num.rb b/lib/coquelicot/num.rb
new file mode 100644 (file)
index 0000000..a5970f2
--- /dev/null
@@ -0,0 +1,33 @@
+# Coquelicot: "one-click" file sharing with a focus on users' privacy.
+# Copyright © 2012 potager.org <jardiniers@potager.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+module Coquelicot::Num
+  # found on: http://codereview.stackexchange.com/questions/9107/
+  def as_size
+    # XXX: i18nize
+    prefix = %W(TiB GiB MiB KiB B)
+    s = self.to_f
+    i = prefix.length - 1
+    while s > 512 && i > 0
+      s /= 1024
+      i -= 1
+    end
+    ((s > 9 || s.modulo(1) < 0.1 ? '%d' : '%.1f') % s) + ' ' + prefix[i]
+  end
+end
+
+Fixnum.send(:include, Coquelicot::Num)
+Bignum.send(:include, Coquelicot::Num)
index 18c4591..ee9f71f 100644 (file)
@@ -89,6 +89,13 @@ module Coquelicot::Rack
     end
 
     def process!
+      # Stop users right now if input has already said the file is too big.
+      length = @env['CONTENT_LENGTH']
+      unless length.nil?
+        length = length.to_i
+        error_for_max_length(length) if length > Coquelicot.settings.max_file_size
+      end
+
       MultipartParser.parse(@env) do |p|
         p.start do
           @expire = Coquelicot.settings.default_expire
@@ -118,6 +125,9 @@ module Coquelicot::Rack
         p.file :file do |filename, type, reader|
           error 403, 'Forbidden' unless @authenticated
 
+          max_length = Coquelicot.settings.max_file_size
+          # We still compute the length of the received data manually, in case
+          # input was lying.
           length = 0
           @link = Coquelicot.depot.add_file(
                     @pass,
@@ -128,6 +138,7 @@ module Coquelicot::Rack
             data = reader.call
             unless data.nil?
               length += data.bytesize
+              error_for_max_length if length > max_length
             else
               error_for_empty if length == 0
             end
@@ -160,6 +171,23 @@ module Coquelicot::Rack
       super
     end
 
+    def error_for_max_length(length = nil)
+      # XXX: i18nize
+      if length
+        message = <<-MESSAGE.gsub(/\n */m, ' ').strip
+          File is bigger than maximum allowed size:
+          #{length.as_size} would exceed the
+          maximum allowed #{Coquelicot.settings.max_file_size.as_size}.
+        MESSAGE
+      else
+        message = <<-MESSAGE.gsub(/\n */m, ' ').strip
+          File is bigger than maximum allowed size
+          (#{Coquelicot.settings.max_file_size.as_size}).
+        MESSAGE
+      end
+      error 413, message
+    end
+
     def error_for_empty
       # XXX: i18nize
       error 403, 'File has no content'
index 7953436..d4d9c35 100644 (file)
@@ -23,6 +23,16 @@ describe Coquelicot::Application do
 
   include_context 'with Coquelicot::Application'
 
+  describe 'get /' do
+    before do
+      visit '/'
+    end
+    it 'should display the maximum file size' do
+      find(:xpath, '//label[@for="file"]/following::*[@class="note"]').
+          should have_content("Max. size: #{Coquelicot.settings.max_file_size.as_size}")
+    end
+  end
+
   describe 'get /README' do
     before do
       visit '/README'
index 7c69360..20ae6b3 100644 (file)
@@ -139,6 +139,52 @@ MULTIPART_DATA
             expect { subject }.to change { Coquelicot.depot.size }.by(1)
           end
         end
+        context 'when file is bigger than limit' do
+          include_context 'correct POST data'
+          before(:each) do
+            Coquelicot.settings.authenticator.stub(:authenticate).and_return(true)
+            Coquelicot.settings.stub(:max_file_size).and_return(100)
+          end
+          context 'when there is a request Content-Length header' do
+            it 'should bail out with 413 (Request Entity Too Large)' do
+              subject.status.should == 413
+            end
+            it 'should display "File is bigger than maximum allowed size"' do
+              subject.body.should include('File is bigger than maximum allowed size')
+            end
+            it 'should display the maximum file size' do
+              subject.body.should include('100 B')
+            end
+          end
+          context 'when there is no request Content-Length header' do
+            before(:each) do
+              env['CONTENT_LENGTH'] = nil
+            end
+            it 'should bail out with 413 (Request Entity Too Large)' do
+              subject.status.should == 413
+            end
+            it 'should display "File is bigger than maximum allowed size"' do
+              subject.body.should include('File is bigger than maximum allowed size')
+            end
+            it 'should display the maximum file size' do
+              subject.body.should include('100 B')
+            end
+          end
+          context 'when the request Content-Length header is lying to us' do
+            before(:each) do
+              env['CONTENT_LENGTH'] = 99
+            end
+            it 'should bail out with 413 (Request Entity Too Large)' do
+              subject.status.should == 413
+            end
+            it 'should display "File is bigger than maximum allowed size"' do
+              subject.body.should include('File is bigger than maximum allowed size')
+            end
+            it 'should display the maximum file size' do
+              subject.body.should include('100 B')
+            end
+          end
+        end
         context 'when receiving a request with other fields after file' do
           before(:each) do
             Coquelicot.settings.authenticator.stub(:authenticate).and_return(true)
index 27eba3b..5775f02 100644 (file)
@@ -50,6 +50,7 @@
   .field
     %label{ :for => 'file' } File:
     %input.input{ :type => 'file', :id => 'file', :name => 'file' }
+    .note Max. size: #{Coquelicot.settings.max_file_size.as_size}
   .field
     .submit
       %input.submit{ :type => 'submit', :value => _('Share!') }
index a155ac3..d6a41a7 100644 (file)
@@ -103,6 +103,12 @@ fieldset
 #gen_pass
   font-size: small
 
+.note
+  clear: left
+  font-style: italic
+  font-size: smaller
+  margin-left: 12em
+
 .field
   clear: left