rack 2.1.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of rack might be problematic. Click here for more details.

Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +377 -16
  3. data/CONTRIBUTING.md +144 -0
  4. data/MIT-LICENSE +1 -1
  5. data/README.md +328 -0
  6. data/SPEC.rdoc +365 -0
  7. data/lib/rack/auth/abstract/handler.rb +3 -1
  8. data/lib/rack/auth/abstract/request.rb +2 -2
  9. data/lib/rack/auth/basic.rb +4 -7
  10. data/lib/rack/bad_request.rb +8 -0
  11. data/lib/rack/body_proxy.rb +34 -12
  12. data/lib/rack/builder.rb +162 -59
  13. data/lib/rack/cascade.rb +24 -10
  14. data/lib/rack/common_logger.rb +43 -28
  15. data/lib/rack/conditional_get.rb +30 -25
  16. data/lib/rack/constants.rb +66 -0
  17. data/lib/rack/content_length.rb +10 -16
  18. data/lib/rack/content_type.rb +9 -7
  19. data/lib/rack/deflater.rb +78 -50
  20. data/lib/rack/directory.rb +86 -63
  21. data/lib/rack/etag.rb +14 -22
  22. data/lib/rack/events.rb +18 -17
  23. data/lib/rack/files.rb +99 -61
  24. data/lib/rack/head.rb +8 -9
  25. data/lib/rack/headers.rb +238 -0
  26. data/lib/rack/lint.rb +868 -642
  27. data/lib/rack/lock.rb +2 -6
  28. data/lib/rack/logger.rb +3 -0
  29. data/lib/rack/media_type.rb +9 -4
  30. data/lib/rack/method_override.rb +6 -2
  31. data/lib/rack/mime.rb +14 -5
  32. data/lib/rack/mock.rb +1 -253
  33. data/lib/rack/mock_request.rb +171 -0
  34. data/lib/rack/mock_response.rb +124 -0
  35. data/lib/rack/multipart/generator.rb +15 -8
  36. data/lib/rack/multipart/parser.rb +238 -107
  37. data/lib/rack/multipart/uploaded_file.rb +17 -7
  38. data/lib/rack/multipart.rb +54 -42
  39. data/lib/rack/null_logger.rb +9 -0
  40. data/lib/rack/query_parser.rb +87 -105
  41. data/lib/rack/recursive.rb +3 -1
  42. data/lib/rack/reloader.rb +0 -4
  43. data/lib/rack/request.rb +366 -135
  44. data/lib/rack/response.rb +186 -68
  45. data/lib/rack/rewindable_input.rb +24 -6
  46. data/lib/rack/runtime.rb +8 -7
  47. data/lib/rack/sendfile.rb +29 -27
  48. data/lib/rack/show_exceptions.rb +27 -12
  49. data/lib/rack/show_status.rb +21 -13
  50. data/lib/rack/static.rb +19 -12
  51. data/lib/rack/tempfile_reaper.rb +14 -5
  52. data/lib/rack/urlmap.rb +5 -6
  53. data/lib/rack/utils.rb +274 -260
  54. data/lib/rack/version.rb +21 -0
  55. data/lib/rack.rb +18 -103
  56. metadata +25 -52
  57. data/README.rdoc +0 -262
  58. data/Rakefile +0 -123
  59. data/SPEC +0 -263
  60. data/bin/rackup +0 -5
  61. data/contrib/rack.png +0 -0
  62. data/contrib/rack.svg +0 -150
  63. data/contrib/rack_logo.svg +0 -164
  64. data/contrib/rdoc.css +0 -412
  65. data/example/lobster.ru +0 -6
  66. data/example/protectedlobster.rb +0 -16
  67. data/example/protectedlobster.ru +0 -10
  68. data/lib/rack/auth/digest/md5.rb +0 -131
  69. data/lib/rack/auth/digest/nonce.rb +0 -54
  70. data/lib/rack/auth/digest/params.rb +0 -54
  71. data/lib/rack/auth/digest/request.rb +0 -43
  72. data/lib/rack/chunked.rb +0 -92
  73. data/lib/rack/core_ext/regexp.rb +0 -14
  74. data/lib/rack/file.rb +0 -8
  75. data/lib/rack/handler/cgi.rb +0 -62
  76. data/lib/rack/handler/fastcgi.rb +0 -102
  77. data/lib/rack/handler/lsws.rb +0 -63
  78. data/lib/rack/handler/scgi.rb +0 -73
  79. data/lib/rack/handler/thin.rb +0 -38
  80. data/lib/rack/handler/webrick.rb +0 -122
  81. data/lib/rack/handler.rb +0 -104
  82. data/lib/rack/lobster.rb +0 -72
  83. data/lib/rack/server.rb +0 -467
  84. data/lib/rack/session/abstract/id.rb +0 -528
  85. data/lib/rack/session/cookie.rb +0 -205
  86. data/lib/rack/session/memcache.rb +0 -10
  87. data/lib/rack/session/pool.rb +0 -85
  88. data/rack.gemspec +0 -44
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'time'
4
- require 'rack/utils'
5
- require 'rack/mime'
6
- require 'rack/files'
4
+
5
+ require_relative 'constants'
6
+ require_relative 'utils'
7
+ require_relative 'head'
8
+ require_relative 'mime'
9
+ require_relative 'files'
7
10
 
8
11
  module Rack
9
12
  # Rack::Directory serves entries below the +root+ given, according to the
@@ -14,8 +17,8 @@ module Rack
14
17
  # If +app+ is not specified, a Rack::Files of the same +root+ will be used.
15
18
 
16
19
  class Directory
17
- DIR_FILE = "<tr><td class='name'><a href='%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>"
18
- DIR_PAGE = <<-PAGE
20
+ DIR_FILE = "<tr><td class='name'><a href='%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>\n"
21
+ DIR_PAGE_HEADER = <<-PAGE
19
22
  <html><head>
20
23
  <title>%s</title>
21
24
  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
@@ -36,33 +39,51 @@ table { width:100%%; }
36
39
  <th class='type'>Type</th>
37
40
  <th class='mtime'>Last Modified</th>
38
41
  </tr>
39
- %s
42
+ PAGE
43
+ DIR_PAGE_FOOTER = <<-PAGE
40
44
  </table>
41
45
  <hr />
42
46
  </body></html>
43
47
  PAGE
44
48
 
49
+ # Body class for directory entries, showing an index page with links
50
+ # to each file.
45
51
  class DirectoryBody < Struct.new(:root, :path, :files)
52
+ # Yield strings for each part of the directory entry
46
53
  def each
47
- show_path = Rack::Utils.escape_html(path.sub(/^#{root}/, ''))
48
- listings = files.map{|f| DIR_FILE % DIR_FILE_escape(*f) } * "\n"
49
- page = DIR_PAGE % [ show_path, show_path, listings ]
50
- page.each_line{|l| yield l }
54
+ show_path = Utils.escape_html(path.sub(/^#{root}/, ''))
55
+ yield(DIR_PAGE_HEADER % [ show_path, show_path ])
56
+
57
+ unless path.chomp('/') == root
58
+ yield(DIR_FILE % DIR_FILE_escape(files.call('..')))
59
+ end
60
+
61
+ Dir.foreach(path) do |basename|
62
+ next if basename.start_with?('.')
63
+ next unless f = files.call(basename)
64
+ yield(DIR_FILE % DIR_FILE_escape(f))
65
+ end
66
+
67
+ yield(DIR_PAGE_FOOTER)
51
68
  end
52
69
 
53
70
  private
54
- # Assumes url is already escaped.
55
- def DIR_FILE_escape url, *html
56
- [url, *html.map { |e| Utils.escape_html(e) }]
71
+
72
+ # Escape each element in the array of html strings.
73
+ def DIR_FILE_escape(htmls)
74
+ htmls.map { |e| Utils.escape_html(e) }
57
75
  end
58
76
  end
59
77
 
60
- attr_reader :root, :path
78
+ # The root of the directory hierarchy. Only requests for files and
79
+ # directories inside of the root directory are supported.
80
+ attr_reader :root
61
81
 
82
+ # Set the root directory and application for serving files.
62
83
  def initialize(root, app = nil)
63
84
  @root = ::File.expand_path(root)
64
- @app = app || Rack::Files.new(@root)
65
- @head = Rack::Head.new(lambda { |env| get env })
85
+ @app = app || Files.new(@root)
86
+ @head = Head.new(method(:get))
66
87
  end
67
88
 
68
89
  def call(env)
@@ -70,100 +91,101 @@ table { width:100%%; }
70
91
  @head.call env
71
92
  end
72
93
 
94
+ # Internals of request handling. Similar to call but does
95
+ # not remove body for HEAD requests.
73
96
  def get(env)
74
97
  script_name = env[SCRIPT_NAME]
75
98
  path_info = Utils.unescape_path(env[PATH_INFO])
76
99
 
77
- if bad_request = check_bad_request(path_info)
78
- bad_request
79
- elsif forbidden = check_forbidden(path_info)
80
- forbidden
100
+ if client_error_response = check_bad_request(path_info) || check_forbidden(path_info)
101
+ client_error_response
81
102
  else
82
103
  path = ::File.join(@root, path_info)
83
104
  list_path(env, path, path_info, script_name)
84
105
  end
85
106
  end
86
107
 
108
+ # Rack response to use for requests with invalid paths, or nil if path is valid.
87
109
  def check_bad_request(path_info)
88
110
  return if Utils.valid_path?(path_info)
89
111
 
90
112
  body = "Bad Request\n"
91
- size = body.bytesize
92
- return [400, { CONTENT_TYPE => "text/plain",
93
- CONTENT_LENGTH => size.to_s,
94
- "X-Cascade" => "pass" }, [body]]
113
+ [400, { CONTENT_TYPE => "text/plain",
114
+ CONTENT_LENGTH => body.bytesize.to_s,
115
+ "x-cascade" => "pass" }, [body]]
95
116
  end
96
117
 
118
+ # Rack response to use for requests with paths outside the root, or nil if path is inside the root.
97
119
  def check_forbidden(path_info)
98
120
  return unless path_info.include? ".."
121
+ return if ::File.expand_path(::File.join(@root, path_info)).start_with?(@root)
99
122
 
100
123
  body = "Forbidden\n"
101
- size = body.bytesize
102
- return [403, { CONTENT_TYPE => "text/plain",
103
- CONTENT_LENGTH => size.to_s,
104
- "X-Cascade" => "pass" }, [body]]
124
+ [403, { CONTENT_TYPE => "text/plain",
125
+ CONTENT_LENGTH => body.bytesize.to_s,
126
+ "x-cascade" => "pass" }, [body]]
105
127
  end
106
128
 
129
+ # Rack response to use for directories under the root.
107
130
  def list_directory(path_info, path, script_name)
108
- files = [['../', 'Parent Directory', '', '', '']]
109
- glob = ::File.join(path, '*')
110
-
111
131
  url_head = (script_name.split('/') + path_info.split('/')).map do |part|
112
- Rack::Utils.escape_path part
132
+ Utils.escape_path part
113
133
  end
114
134
 
115
- Dir[glob].sort.each do |node|
116
- stat = stat(node)
135
+ # Globbing not safe as path could contain glob metacharacters
136
+ body = DirectoryBody.new(@root, path, ->(basename) do
137
+ stat = stat(::File.join(path, basename))
117
138
  next unless stat
118
- basename = ::File.basename(node)
119
- ext = ::File.extname(node)
120
139
 
121
- url = ::File.join(*url_head + [Rack::Utils.escape_path(basename)])
122
- size = stat.size
123
- type = stat.directory? ? 'directory' : Mime.mime_type(ext)
124
- size = stat.directory? ? '-' : filesize_format(size)
140
+ url = ::File.join(*url_head + [Utils.escape_path(basename)])
125
141
  mtime = stat.mtime.httpdate
126
- url << '/' if stat.directory?
127
- basename << '/' if stat.directory?
128
-
129
- files << [ url, basename, size, type, mtime ]
130
- end
131
-
132
- return [ 200, { CONTENT_TYPE => 'text/html; charset=utf-8' }, DirectoryBody.new(@root, path, files) ]
142
+ if stat.directory?
143
+ type = 'directory'
144
+ size = '-'
145
+ url << '/'
146
+ if basename == '..'
147
+ basename = 'Parent Directory'
148
+ else
149
+ basename << '/'
150
+ end
151
+ else
152
+ type = Mime.mime_type(::File.extname(basename))
153
+ size = filesize_format(stat.size)
154
+ end
155
+
156
+ [ url, basename, size, type, mtime ]
157
+ end)
158
+
159
+ [ 200, { CONTENT_TYPE => 'text/html; charset=utf-8' }, body ]
133
160
  end
134
161
 
135
- def stat(node)
136
- ::File.stat(node)
162
+ # File::Stat for the given path, but return nil for missing/bad entries.
163
+ def stat(path)
164
+ ::File.stat(path)
137
165
  rescue Errno::ENOENT, Errno::ELOOP
138
166
  return nil
139
167
  end
140
168
 
141
- # TODO: add correct response if not readable, not sure if 404 is the best
142
- # option
169
+ # Rack response to use for files and directories under the root.
170
+ # Unreadable and non-file, non-directory entries will get a 404 response.
143
171
  def list_path(env, path, path_info, script_name)
144
- stat = ::File.stat(path)
145
-
146
- if stat.readable?
172
+ if (stat = stat(path)) && stat.readable?
147
173
  return @app.call(env) if stat.file?
148
174
  return list_directory(path_info, path, script_name) if stat.directory?
149
- else
150
- raise Errno::ENOENT, 'No such file or directory'
151
175
  end
152
176
 
153
- rescue Errno::ENOENT, Errno::ELOOP
154
- return entity_not_found(path_info)
177
+ entity_not_found(path_info)
155
178
  end
156
179
 
180
+ # Rack response to use for unreadable and non-file, non-directory entries.
157
181
  def entity_not_found(path_info)
158
182
  body = "Entity not found: #{path_info}\n"
159
- size = body.bytesize
160
- return [404, { CONTENT_TYPE => "text/plain",
161
- CONTENT_LENGTH => size.to_s,
162
- "X-Cascade" => "pass" }, [body]]
183
+ [404, { CONTENT_TYPE => "text/plain",
184
+ CONTENT_LENGTH => body.bytesize.to_s,
185
+ "x-cascade" => "pass" }, [body]]
163
186
  end
164
187
 
165
188
  # Stolen from Ramaze
166
-
167
189
  FILESIZE_FORMAT = [
168
190
  ['%.1fT', 1 << 40],
169
191
  ['%.1fG', 1 << 30],
@@ -171,6 +193,7 @@ table { width:100%%; }
171
193
  ['%.1fK', 1 << 10],
172
194
  ]
173
195
 
196
+ # Provide human readable file sizes
174
197
  def filesize_format(int)
175
198
  FILESIZE_FORMAT.each do |format, size|
176
199
  return format % (int.to_f / size) if int >= size
data/lib/rack/etag.rb CHANGED
@@ -1,17 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rack'
4
3
  require 'digest/sha2'
5
4
 
5
+ require_relative 'constants'
6
+ require_relative 'utils'
7
+
6
8
  module Rack
7
- # Automatically sets the ETag header on all String bodies.
9
+ # Automatically sets the etag header on all String bodies.
8
10
  #
9
- # The ETag header is skipped if ETag or Last-Modified headers are sent or if
11
+ # The etag header is skipped if etag or last-modified headers are sent or if
10
12
  # a sendfile body (body.responds_to :to_path) is given (since such cases
11
13
  # should be handled by apache/nginx).
12
14
  #
13
- # On initialization, you can pass two parameters: a Cache-Control directive
14
- # used when Etag is absent and a directive when it is present. The first
15
+ # On initialization, you can pass two parameters: a cache-control directive
16
+ # used when etag is absent and a directive when it is present. The first
15
17
  # defaults to nil, while the second defaults to "max-age=0, private, must-revalidate"
16
18
  class ETag
17
19
  ETAG_STRING = Rack::ETAG
@@ -24,14 +26,11 @@ module Rack
24
26
  end
25
27
 
26
28
  def call(env)
27
- status, headers, body = @app.call(env)
29
+ status, headers, body = response = @app.call(env)
28
30
 
29
- if etag_status?(status) && etag_body?(body) && !skip_caching?(headers)
30
- original_body = body
31
- digest, new_body = digest_body(body)
32
- body = Rack::BodyProxy.new(new_body) do
33
- original_body.close if original_body.respond_to?(:close)
34
- end
31
+ if etag_status?(status) && body.respond_to?(:to_ary) && !skip_caching?(headers)
32
+ body = body.to_ary
33
+ digest = digest_body(body)
35
34
  headers[ETAG_STRING] = %(W/"#{digest}") if digest
36
35
  end
37
36
 
@@ -43,7 +42,7 @@ module Rack
43
42
  end
44
43
  end
45
44
 
46
- [status, headers, body]
45
+ response
47
46
  end
48
47
 
49
48
  private
@@ -52,25 +51,18 @@ module Rack
52
51
  status == 200 || status == 201
53
52
  end
54
53
 
55
- def etag_body?(body)
56
- !body.respond_to?(:to_path)
57
- end
58
-
59
54
  def skip_caching?(headers)
60
- (headers[CACHE_CONTROL] && headers[CACHE_CONTROL].include?('no-cache')) ||
61
- headers.key?(ETAG_STRING) || headers.key?('Last-Modified')
55
+ headers.key?(ETAG_STRING) || headers.key?('last-modified')
62
56
  end
63
57
 
64
58
  def digest_body(body)
65
- parts = []
66
59
  digest = nil
67
60
 
68
61
  body.each do |part|
69
- parts << part
70
62
  (digest ||= Digest::SHA256.new) << part unless part.empty?
71
63
  end
72
64
 
73
- [digest && digest.hexdigest.byteslice(0, 32), parts]
65
+ digest && digest.hexdigest.byteslice(0,32)
74
66
  end
75
67
  end
76
68
  end
data/lib/rack/events.rb CHANGED
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rack/response'
4
- require 'rack/body_proxy'
3
+ require_relative 'body_proxy'
4
+ require_relative 'request'
5
+ require_relative 'response'
5
6
 
6
7
  module Rack
7
8
  ### This middleware provides hooks to certain places in the request /
@@ -59,26 +60,26 @@ module Rack
59
60
 
60
61
  class Events
61
62
  module Abstract
62
- def on_start req, res
63
+ def on_start(req, res)
63
64
  end
64
65
 
65
- def on_commit req, res
66
+ def on_commit(req, res)
66
67
  end
67
68
 
68
- def on_send req, res
69
+ def on_send(req, res)
69
70
  end
70
71
 
71
- def on_finish req, res
72
+ def on_finish(req, res)
72
73
  end
73
74
 
74
- def on_error req, res, e
75
+ def on_error(req, res, e)
75
76
  end
76
77
  end
77
78
 
78
79
  class EventedBodyProxy < Rack::BodyProxy # :nodoc:
79
80
  attr_reader :request, :response
80
81
 
81
- def initialize body, request, response, handlers, &block
82
+ def initialize(body, request, response, handlers, &block)
82
83
  super(body, &block)
83
84
  @request = request
84
85
  @response = response
@@ -94,7 +95,7 @@ module Rack
94
95
  class BufferedResponse < Rack::Response::Raw # :nodoc:
95
96
  attr_reader :body
96
97
 
97
- def initialize status, headers, body
98
+ def initialize(status, headers, body)
98
99
  super(status, headers)
99
100
  @body = body
100
101
  end
@@ -102,12 +103,12 @@ module Rack
102
103
  def to_a; [status, headers, body]; end
103
104
  end
104
105
 
105
- def initialize app, handlers
106
+ def initialize(app, handlers)
106
107
  @app = app
107
108
  @handlers = handlers
108
109
  end
109
110
 
110
- def call env
111
+ def call(env)
111
112
  request = make_request env
112
113
  on_start request, nil
113
114
 
@@ -129,27 +130,27 @@ module Rack
129
130
 
130
131
  private
131
132
 
132
- def on_error request, response, e
133
+ def on_error(request, response, e)
133
134
  @handlers.reverse_each { |handler| handler.on_error request, response, e }
134
135
  end
135
136
 
136
- def on_commit request, response
137
+ def on_commit(request, response)
137
138
  @handlers.reverse_each { |handler| handler.on_commit request, response }
138
139
  end
139
140
 
140
- def on_start request, response
141
+ def on_start(request, response)
141
142
  @handlers.each { |handler| handler.on_start request, nil }
142
143
  end
143
144
 
144
- def on_finish request, response
145
+ def on_finish(request, response)
145
146
  @handlers.reverse_each { |handler| handler.on_finish request, response }
146
147
  end
147
148
 
148
- def make_request env
149
+ def make_request(env)
149
150
  Rack::Request.new env
150
151
  end
151
152
 
152
- def make_response status, headers, body
153
+ def make_response(status, headers, body)
153
154
  BufferedResponse.new status, headers, body
154
155
  end
155
156
  end
data/lib/rack/files.rb CHANGED
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'time'
4
- require 'rack/utils'
5
- require 'rack/mime'
6
- require 'rack/request'
7
- require 'rack/head'
4
+
5
+ require_relative 'constants'
6
+ require_relative 'head'
7
+ require_relative 'utils'
8
+ require_relative 'request'
9
+ require_relative 'mime'
8
10
 
9
11
  module Rack
10
12
  # Rack::Files serves files below the +root+ directory given, according to the
@@ -18,11 +20,12 @@ module Rack
18
20
  class Files
19
21
  ALLOWED_VERBS = %w[GET HEAD OPTIONS]
20
22
  ALLOW_HEADER = ALLOWED_VERBS.join(', ')
23
+ MULTIPART_BOUNDARY = 'AaB03x'
21
24
 
22
25
  attr_reader :root
23
26
 
24
27
  def initialize(root, headers = {}, default_mime = 'text/plain')
25
- @root = ::File.expand_path root
28
+ @root = (::File.expand_path(root) if root)
26
29
  @headers = headers
27
30
  @default_mime = default_mime
28
31
  @head = Rack::Head.new(lambda { |env| get env })
@@ -36,7 +39,7 @@ module Rack
36
39
  def get(env)
37
40
  request = Rack::Request.new env
38
41
  unless ALLOWED_VERBS.include? request.request_method
39
- return fail(405, "Method Not Allowed", { 'Allow' => ALLOW_HEADER })
42
+ return fail(405, "Method Not Allowed", { 'allow' => ALLOW_HEADER })
40
43
  end
41
44
 
42
45
  path_info = Utils.unescape_path request.path_info
@@ -48,7 +51,11 @@ module Rack
48
51
  available = begin
49
52
  ::File.file?(path) && ::File.readable?(path)
50
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:
51
57
  false
58
+ # :nocov:
52
59
  end
53
60
 
54
61
  if available
@@ -60,85 +67,126 @@ module Rack
60
67
 
61
68
  def serving(request, path)
62
69
  if request.options?
63
- return [200, { 'Allow' => ALLOW_HEADER, CONTENT_LENGTH => '0' }, []]
70
+ return [200, { 'allow' => ALLOW_HEADER, CONTENT_LENGTH => '0' }, []]
64
71
  end
65
72
  last_modified = ::File.mtime(path).httpdate
66
73
  return [304, {}, []] if request.get_header('HTTP_IF_MODIFIED_SINCE') == last_modified
67
74
 
68
- headers = { "Last-Modified" => last_modified }
75
+ headers = { "last-modified" => last_modified }
69
76
  mime_type = mime_type path, @default_mime
70
77
  headers[CONTENT_TYPE] = mime_type if mime_type
71
78
 
72
79
  # Set custom headers
73
- @headers.each { |field, content| headers[field] = content } if @headers
74
-
75
- response = [ 200, headers ]
80
+ headers.merge!(@headers) if @headers
76
81
 
82
+ status = 200
77
83
  size = filesize path
78
84
 
79
- range = nil
80
85
  ranges = Rack::Utils.get_byte_ranges(request.get_header('HTTP_RANGE'), size)
81
- if ranges.nil? || ranges.length > 1
82
- # No ranges, or multiple ranges (which we don't support):
83
- # TODO: Support multiple byte-ranges
84
- response[0] = 200
85
- range = 0..size - 1
86
+ if ranges.nil?
87
+ # No ranges:
88
+ ranges = [0..size - 1]
86
89
  elsif ranges.empty?
87
90
  # Unsatisfiable. Return error, and file size:
88
91
  response = fail(416, "Byte range unsatisfiable")
89
- response[1]["Content-Range"] = "bytes */#{size}"
92
+ response[1]["content-range"] = "bytes */#{size}"
90
93
  return response
91
94
  else
92
- # Partial content:
93
- range = ranges[0]
94
- response[0] = 206
95
- response[1]["Content-Range"] = "bytes #{range.begin}-#{range.end}/#{size}"
96
- size = range.end - range.begin + 1
95
+ # Partial content
96
+ partial_content = true
97
+
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
104
+
105
+ status = 206
106
+ body = BaseIterator.new(path, ranges, mime_type: mime_type, size: size)
107
+ size = body.bytesize
97
108
  end
98
109
 
99
- response[2] = [response_body] unless response_body.nil?
110
+ headers[CONTENT_LENGTH] = size.to_s
100
111
 
101
- response[1][CONTENT_LENGTH] = size.to_s
102
- response[2] = make_body request, path, range
103
- response
112
+ if request.head?
113
+ body = []
114
+ elsif !partial_content
115
+ body = Iterator.new(path, ranges, mime_type: mime_type, size: size)
116
+ end
117
+
118
+ [status, headers, body]
104
119
  end
105
120
 
106
- class Iterator
107
- attr_reader :path, :range
108
- alias :to_path :path
121
+ class BaseIterator
122
+ attr_reader :path, :ranges, :options
109
123
 
110
- def initialize path, range
111
- @path = path
112
- @range = range
124
+ def initialize(path, ranges, options)
125
+ @path = path
126
+ @ranges = ranges
127
+ @options = options
113
128
  end
114
129
 
115
130
  def each
116
131
  ::File.open(path, "rb") do |file|
117
- file.seek(range.begin)
118
- remaining_len = range.end - range.begin + 1
119
- while remaining_len > 0
120
- part = file.read([8192, remaining_len].min)
121
- break unless part
122
- remaining_len -= part.length
123
-
124
- yield part
132
+ ranges.each do |range|
133
+ yield multipart_heading(range) if multipart?
134
+
135
+ each_range_part(file, range) do |part|
136
+ yield part
137
+ end
125
138
  end
139
+
140
+ yield "\r\n--#{MULTIPART_BOUNDARY}--\r\n" if multipart?
141
+ end
142
+ end
143
+
144
+ def bytesize
145
+ size = ranges.inject(0) do |sum, range|
146
+ sum += multipart_heading(range).bytesize if multipart?
147
+ sum += range.size
126
148
  end
149
+ size += "\r\n--#{MULTIPART_BOUNDARY}--\r\n".bytesize if multipart?
150
+ size
127
151
  end
128
152
 
129
153
  def close; end
130
- end
131
154
 
132
- private
155
+ private
133
156
 
134
- def make_body request, path, range
135
- if request.head?
136
- []
137
- else
138
- Iterator.new path, range
157
+ def multipart?
158
+ ranges.size > 1
159
+ end
160
+
161
+ def multipart_heading(range)
162
+ <<-EOF
163
+ \r
164
+ --#{MULTIPART_BOUNDARY}\r
165
+ content-type: #{options[:mime_type]}\r
166
+ content-range: bytes #{range.begin}-#{range.end}/#{options[:size]}\r
167
+ \r
168
+ EOF
169
+ end
170
+
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
178
+
179
+ yield part
180
+ end
139
181
  end
140
182
  end
141
183
 
184
+ class Iterator < BaseIterator
185
+ alias :to_path :path
186
+ end
187
+
188
+ private
189
+
142
190
  def fail(status, body, headers = {})
143
191
  body += "\n"
144
192
 
@@ -147,32 +195,22 @@ module Rack
147
195
  {
148
196
  CONTENT_TYPE => "text/plain",
149
197
  CONTENT_LENGTH => body.size.to_s,
150
- "X-Cascade" => "pass"
198
+ "x-cascade" => "pass"
151
199
  }.merge!(headers),
152
200
  [body]
153
201
  ]
154
202
  end
155
203
 
156
204
  # The MIME type for the contents of the file located at @path
157
- def mime_type path, default_mime
205
+ def mime_type(path, default_mime)
158
206
  Mime.mime_type(::File.extname(path), default_mime)
159
207
  end
160
208
 
161
- def filesize path
162
- # If response_body is present, use its size.
163
- return response_body.bytesize if response_body
164
-
209
+ def filesize(path)
165
210
  # We check via File::size? whether this file provides size info
166
211
  # via stat (e.g. /proc files often don't), otherwise we have to
167
212
  # figure it out by reading the whole file into memory.
168
213
  ::File.size?(path) || ::File.read(path).bytesize
169
214
  end
170
-
171
- # By default, the response body for file requests is nil.
172
- # In this case, the response body will be generated later
173
- # from the file at @path
174
- def response_body
175
- nil
176
- end
177
215
  end
178
216
  end