fix: malformed charset param (#2263)
[rack.git] / lib / rack / files.rb
blob5b8353f5b5257a240f70ecafe2f57b099236dfd1
1 # frozen_string_literal: true
3 require 'time'
5 require_relative 'constants'
6 require_relative 'head'
7 require_relative 'utils'
8 require_relative 'request'
9 require_relative 'mime'
11 module Rack
12   # Rack::Files serves files below the +root+ directory given, according to the
13   # path info of the Rack request.
14   # e.g. when Rack::Files.new("/etc") is used, you can access 'passwd' file
15   # as http://localhost:9292/passwd
16   #
17   # Handlers can detect if bodies are a Rack::Files, and use mechanisms
18   # like sendfile on the +path+.
20   class Files
21     ALLOWED_VERBS = %w[GET HEAD OPTIONS]
22     ALLOW_HEADER = ALLOWED_VERBS.join(', ')
23     MULTIPART_BOUNDARY = 'AaB03x'
25     attr_reader :root
27     def initialize(root, headers = {}, default_mime = 'text/plain')
28       @root = (::File.expand_path(root) if root)
29       @headers = headers
30       @default_mime = default_mime
31       @head = Rack::Head.new(lambda { |env| get env })
32     end
34     def call(env)
35       # HEAD requests drop the response body, including 4xx error messages.
36       @head.call env
37     end
39     def get(env)
40       request = Rack::Request.new env
41       unless ALLOWED_VERBS.include? request.request_method
42         return fail(405, "Method Not Allowed", { 'allow' => ALLOW_HEADER })
43       end
45       path_info = Utils.unescape_path request.path_info
46       return fail(400, "Bad Request") unless Utils.valid_path?(path_info)
48       clean_path_info = Utils.clean_path_info(path_info)
49       path = ::File.join(@root, clean_path_info)
51       available = begin
52         ::File.file?(path) && ::File.readable?(path)
53       rescue SystemCallError
54         # Not sure in what conditions this exception can occur, but this
55         # is a safe way to handle such an error.
56         # :nocov:
57         false
58         # :nocov:
59       end
61       if available
62         serving(request, path)
63       else
64         fail(404, "File not found: #{path_info}")
65       end
66     end
68     def serving(request, path)
69       if request.options?
70         return [200, { 'allow' => ALLOW_HEADER, CONTENT_LENGTH => '0' }, []]
71       end
72       last_modified = ::File.mtime(path).httpdate
73       return [304, {}, []] if request.get_header('HTTP_IF_MODIFIED_SINCE') == last_modified
75       headers = { "last-modified" => last_modified }
76       mime_type = mime_type path, @default_mime
77       headers[CONTENT_TYPE] = mime_type if mime_type
79       # Set custom headers
80       headers.merge!(@headers) if @headers
82       status = 200
83       size = filesize path
85       ranges = Rack::Utils.get_byte_ranges(request.get_header('HTTP_RANGE'), size)
86       if ranges.nil?
87         # No ranges:
88         ranges = [0..size - 1]
89       elsif ranges.empty?
90         # Unsatisfiable. Return error, and file size:
91         response = fail(416, "Byte range unsatisfiable")
92         response[1]["content-range"] = "bytes */#{size}"
93         return response
94       else
95         # Partial content
96         partial_content = true
98         if ranges.size == 1
99           range = ranges[0]
100           headers["content-range"] = "bytes #{range.begin}-#{range.end}/#{size}"
101         else
102           headers[CONTENT_TYPE] = "multipart/byteranges; boundary=#{MULTIPART_BOUNDARY}"
103         end
105         status = 206
106         body = BaseIterator.new(path, ranges, mime_type: mime_type, size: size)
107         size = body.bytesize
108       end
110       headers[CONTENT_LENGTH] = size.to_s
112       if request.head?
113         body = []
114       elsif !partial_content
115         body = Iterator.new(path, ranges, mime_type: mime_type, size: size)
116       end
118       [status, headers, body]
119     end
121     class BaseIterator
122       attr_reader :path, :ranges, :options
124       def initialize(path, ranges, options)
125         @path = path
126         @ranges = ranges
127         @options = options
128       end
130       def each
131         ::File.open(path, "rb") do |file|
132           ranges.each do |range|
133             yield multipart_heading(range) if multipart?
135             each_range_part(file, range) do |part|
136               yield part
137             end
138           end
140           yield "\r\n--#{MULTIPART_BOUNDARY}--\r\n" if multipart?
141         end
142       end
144       def bytesize
145         size = ranges.inject(0) do |sum, range|
146           sum += multipart_heading(range).bytesize if multipart?
147           sum += range.size
148         end
149         size += "\r\n--#{MULTIPART_BOUNDARY}--\r\n".bytesize if multipart?
150         size
151       end
153       def close; end
155       private
157       def multipart?
158         ranges.size > 1
159       end
161       def multipart_heading(range)
162 <<-EOF
164 --#{MULTIPART_BOUNDARY}\r
165 content-type: #{options[:mime_type]}\r
166 content-range: bytes #{range.begin}-#{range.end}/#{options[:size]}\r
169       end
171       def each_range_part(file, range)
172         file.seek(range.begin)
173         remaining_len = range.end - range.begin + 1
174         while remaining_len > 0
175           part = file.read([8192, remaining_len].min)
176           break unless part
177           remaining_len -= part.length
179           yield part
180         end
181       end
182     end
184     class Iterator < BaseIterator
185       alias :to_path :path
186     end
188     private
190     def fail(status, body, headers = {})
191       body += "\n"
193       [
194         status,
195         {
196           CONTENT_TYPE   => "text/plain",
197           CONTENT_LENGTH => body.size.to_s,
198           "x-cascade" => "pass"
199         }.merge!(headers),
200         [body]
201       ]
202     end
204     # The MIME type for the contents of the file located at @path
205     def mime_type(path, default_mime)
206       Mime.mime_type(::File.extname(path), default_mime)
207     end
209     def filesize(path)
210       #   We check via File::size? whether this file provides size info
211       #   via stat (e.g. /proc files often don't), otherwise we have to
212       #   figure it out by reading the whole file into memory.
213       ::File.size?(path) || ::File.read(path).bytesize
214     end
215   end