homura-runtime 0.3.2 → 0.3.4

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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/exe/compile-assets +2 -2
  4. data/exe/compile-erb +5 -7
  5. data/lib/homura/runtime/build_support.rb +19 -2
  6. data/lib/homura/runtime/version.rb +1 -1
  7. data/vendor/rack/auth/abstract/handler.rb +41 -0
  8. data/vendor/rack/auth/abstract/request.rb +51 -0
  9. data/vendor/rack/auth/basic.rb +58 -0
  10. data/vendor/rack/bad_request.rb +8 -0
  11. data/vendor/rack/body_proxy.rb +63 -0
  12. data/vendor/rack/builder.rb +315 -0
  13. data/vendor/rack/cascade.rb +67 -0
  14. data/vendor/rack/common_logger.rb +94 -0
  15. data/vendor/rack/conditional_get.rb +87 -0
  16. data/vendor/rack/config.rb +22 -0
  17. data/vendor/rack/constants.rb +68 -0
  18. data/vendor/rack/content_length.rb +34 -0
  19. data/vendor/rack/content_type.rb +33 -0
  20. data/vendor/rack/deflater.rb +159 -0
  21. data/vendor/rack/directory.rb +210 -0
  22. data/vendor/rack/etag.rb +71 -0
  23. data/vendor/rack/events.rb +172 -0
  24. data/vendor/rack/files.rb +224 -0
  25. data/vendor/rack/head.rb +25 -0
  26. data/vendor/rack/headers.rb +238 -0
  27. data/vendor/rack/lint.rb +1000 -0
  28. data/vendor/rack/lock.rb +29 -0
  29. data/vendor/rack/media_type.rb +42 -0
  30. data/vendor/rack/method_override.rb +56 -0
  31. data/vendor/rack/mime.rb +694 -0
  32. data/vendor/rack/mock.rb +3 -0
  33. data/vendor/rack/mock_request.rb +161 -0
  34. data/vendor/rack/mock_response.rb +147 -0
  35. data/vendor/rack/multipart/generator.rb +99 -0
  36. data/vendor/rack/multipart/parser.rb +586 -0
  37. data/vendor/rack/multipart/uploaded_file.rb +82 -0
  38. data/vendor/rack/multipart.rb +77 -0
  39. data/vendor/rack/null_logger.rb +48 -0
  40. data/vendor/rack/protection/authenticity_token.rb +256 -0
  41. data/vendor/rack/protection/base.rb +140 -0
  42. data/vendor/rack/protection/content_security_policy.rb +80 -0
  43. data/vendor/rack/protection/cookie_tossing.rb +77 -0
  44. data/vendor/rack/protection/escaped_params.rb +93 -0
  45. data/vendor/rack/protection/form_token.rb +25 -0
  46. data/vendor/rack/protection/frame_options.rb +39 -0
  47. data/vendor/rack/protection/http_origin.rb +43 -0
  48. data/vendor/rack/protection/ip_spoofing.rb +27 -0
  49. data/vendor/rack/protection/json_csrf.rb +60 -0
  50. data/vendor/rack/protection/path_traversal.rb +45 -0
  51. data/vendor/rack/protection/referrer_policy.rb +27 -0
  52. data/vendor/rack/protection/remote_referrer.rb +22 -0
  53. data/vendor/rack/protection/remote_token.rb +24 -0
  54. data/vendor/rack/protection/session_hijacking.rb +37 -0
  55. data/vendor/rack/protection/strict_transport.rb +41 -0
  56. data/vendor/rack/protection/version.rb +7 -0
  57. data/vendor/rack/protection/xss_header.rb +27 -0
  58. data/vendor/rack/protection.rb +58 -0
  59. data/vendor/rack/query_parser.rb +261 -0
  60. data/vendor/rack/recursive.rb +66 -0
  61. data/vendor/rack/reloader.rb +112 -0
  62. data/vendor/rack/request.rb +818 -0
  63. data/vendor/rack/response.rb +403 -0
  64. data/vendor/rack/rewindable_input.rb +116 -0
  65. data/vendor/rack/runtime.rb +35 -0
  66. data/vendor/rack/sendfile.rb +197 -0
  67. data/vendor/rack/session/abstract/id.rb +533 -0
  68. data/vendor/rack/session/constants.rb +13 -0
  69. data/vendor/rack/session/cookie.rb +292 -0
  70. data/vendor/rack/session/encryptor.rb +415 -0
  71. data/vendor/rack/session/pool.rb +76 -0
  72. data/vendor/rack/session/version.rb +10 -0
  73. data/vendor/rack/session.rb +12 -0
  74. data/vendor/rack/show_exceptions.rb +433 -0
  75. data/vendor/rack/show_status.rb +121 -0
  76. data/vendor/rack/static.rb +188 -0
  77. data/vendor/rack/tempfile_reaper.rb +44 -0
  78. data/vendor/rack/urlmap.rb +99 -0
  79. data/vendor/rack/utils.rb +631 -0
  80. data/vendor/rack/version.rb +17 -0
  81. data/vendor/rack.rb +66 -0
  82. metadata +76 -1
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ require_relative 'constants'
6
+ require_relative 'utils'
7
+ require_relative 'head'
8
+ require_relative 'mime'
9
+ require_relative 'files'
10
+
11
+ module Rack
12
+ # Rack::Directory serves entries below the +root+ given, according to the
13
+ # path info of the Rack request. If a directory is found, the file's contents
14
+ # will be presented in an html based index. If a file is found, the env will
15
+ # be passed to the specified +app+.
16
+ #
17
+ # If +app+ is not specified, a Rack::Files of the same +root+ will be used.
18
+ #
19
+ # Be aware that just like the default behavior of most webservers, Rack::Directory
20
+ # will follow symbolic links encountered under the root. If a symlink points to
21
+ # a location outside of the root, that target will still be served as part of
22
+ # the response.
23
+
24
+ class Directory
25
+ 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"
26
+ DIR_PAGE_HEADER = <<-PAGE
27
+ <html><head>
28
+ <title>%s</title>
29
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
30
+ <style type='text/css'>
31
+ table { width:100%%; }
32
+ .name { text-align:left; }
33
+ .size, .mtime { text-align:right; }
34
+ .type { width:11em; }
35
+ .mtime { width:15em; }
36
+ </style>
37
+ </head><body>
38
+ <h1>%s</h1>
39
+ <hr />
40
+ <table>
41
+ <tr>
42
+ <th class='name'>Name</th>
43
+ <th class='size'>Size</th>
44
+ <th class='type'>Type</th>
45
+ <th class='mtime'>Last Modified</th>
46
+ </tr>
47
+ PAGE
48
+ DIR_PAGE_FOOTER = <<-PAGE
49
+ </table>
50
+ <hr />
51
+ </body></html>
52
+ PAGE
53
+
54
+ # Body class for directory entries, showing an index page with links
55
+ # to each file.
56
+ class DirectoryBody < Struct.new(:root, :path, :files)
57
+ # Yield strings for each part of the directory entry
58
+ def each
59
+ show_path = Utils.escape_html(path.sub(/^#{root}/, ''))
60
+ yield(DIR_PAGE_HEADER % [ show_path, show_path ])
61
+
62
+ unless path.chomp('/') == root
63
+ yield(DIR_FILE % DIR_FILE_escape(files.call('..')))
64
+ end
65
+
66
+ Dir.foreach(path) do |basename|
67
+ next if basename.start_with?('.')
68
+ next unless f = files.call(basename)
69
+ yield(DIR_FILE % DIR_FILE_escape(f))
70
+ end
71
+
72
+ yield(DIR_PAGE_FOOTER)
73
+ end
74
+
75
+ private
76
+
77
+ # Escape each element in the array of html strings.
78
+ def DIR_FILE_escape(htmls)
79
+ htmls.map { |e| Utils.escape_html(e) }
80
+ end
81
+ end
82
+
83
+ # The root of the directory hierarchy. Only requests for files and
84
+ # directories inside of the root directory are supported.
85
+ attr_reader :root
86
+
87
+ # Set the root directory and application for serving files.
88
+ def initialize(root, app = nil)
89
+ @root = ::File.expand_path(root)
90
+ @app = app || Files.new(@root)
91
+ @head = Head.new(method(:get))
92
+ end
93
+
94
+ def call(env)
95
+ # strip body if this is a HEAD call
96
+ @head.call env
97
+ end
98
+
99
+ # Internals of request handling. Similar to call but does
100
+ # not remove body for HEAD requests.
101
+ def get(env)
102
+ script_name = env[SCRIPT_NAME]
103
+ path_info = Utils.unescape_path(env[PATH_INFO])
104
+
105
+ if client_error_response = check_bad_request(path_info) || check_forbidden(path_info)
106
+ client_error_response
107
+ else
108
+ path = ::File.join(@root, path_info)
109
+ list_path(env, path, path_info, script_name)
110
+ end
111
+ end
112
+
113
+ # Rack response to use for requests with invalid paths, or nil if path is valid.
114
+ def check_bad_request(path_info)
115
+ return if Utils.valid_path?(path_info)
116
+
117
+ body = "Bad Request\n"
118
+ [400, { CONTENT_TYPE => "text/plain",
119
+ CONTENT_LENGTH => body.bytesize.to_s,
120
+ "x-cascade" => "pass" }, [body]]
121
+ end
122
+
123
+ # Rack response to use for requests with paths outside the root, or nil if path is inside the root.
124
+ def check_forbidden(path_info)
125
+ return unless path_info.include? ".."
126
+ return if ::File.expand_path(::File.join(@root, path_info)).start_with?(@root)
127
+
128
+ body = "Forbidden\n"
129
+ [403, { CONTENT_TYPE => "text/plain",
130
+ CONTENT_LENGTH => body.bytesize.to_s,
131
+ "x-cascade" => "pass" }, [body]]
132
+ end
133
+
134
+ # Rack response to use for directories under the root.
135
+ def list_directory(path_info, path, script_name)
136
+ url_head = (script_name.split('/') + path_info.split('/')).map do |part|
137
+ Utils.escape_path part
138
+ end
139
+
140
+ # Globbing not safe as path could contain glob metacharacters
141
+ body = DirectoryBody.new(@root, path, ->(basename) do
142
+ stat = stat(::File.join(path, basename))
143
+ next unless stat
144
+
145
+ url = ::File.join(*url_head + [Utils.escape_path(basename)])
146
+ mtime = stat.mtime.httpdate
147
+ if stat.directory?
148
+ type = 'directory'
149
+ size = '-'
150
+ url << '/'
151
+ if basename == '..'
152
+ basename = 'Parent Directory'
153
+ else
154
+ basename << '/'
155
+ end
156
+ else
157
+ type = Mime.mime_type(::File.extname(basename))
158
+ size = filesize_format(stat.size)
159
+ end
160
+
161
+ [ url, basename, size, type, mtime ]
162
+ end)
163
+
164
+ [ 200, { CONTENT_TYPE => 'text/html; charset=utf-8' }, body ]
165
+ end
166
+
167
+ # File::Stat for the given path, but return nil for missing/bad entries.
168
+ def stat(path)
169
+ ::File.stat(path)
170
+ rescue Errno::ENOENT, Errno::ELOOP
171
+ return nil
172
+ end
173
+
174
+ # Rack response to use for files and directories under the root.
175
+ # Unreadable and non-file, non-directory entries will get a 404 response.
176
+ def list_path(env, path, path_info, script_name)
177
+ if (stat = stat(path)) && stat.readable?
178
+ return @app.call(env) if stat.file?
179
+ return list_directory(path_info, path, script_name) if stat.directory?
180
+ end
181
+
182
+ entity_not_found(path_info)
183
+ end
184
+
185
+ # Rack response to use for unreadable and non-file, non-directory entries.
186
+ def entity_not_found(path_info)
187
+ body = "Entity not found: #{path_info}\n"
188
+ [404, { CONTENT_TYPE => "text/plain",
189
+ CONTENT_LENGTH => body.bytesize.to_s,
190
+ "x-cascade" => "pass" }, [body]]
191
+ end
192
+
193
+ # Stolen from Ramaze
194
+ FILESIZE_FORMAT = [
195
+ ['%.1fT', 1 << 40],
196
+ ['%.1fG', 1 << 30],
197
+ ['%.1fM', 1 << 20],
198
+ ['%.1fK', 1 << 10],
199
+ ]
200
+
201
+ # Provide human readable file sizes
202
+ def filesize_format(int)
203
+ FILESIZE_FORMAT.each do |format, size|
204
+ return format % (int.to_f / size) if int >= size
205
+ end
206
+
207
+ "#{int}B"
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest/sha2'
4
+
5
+ require_relative 'constants'
6
+ require_relative 'utils'
7
+
8
+ module Rack
9
+ # Automatically sets the etag header on all String bodies.
10
+ #
11
+ # The etag header is skipped if etag or last-modified headers are sent or if
12
+ # a sendfile body (body.responds_to :to_path) is given (since such cases
13
+ # should be handled by apache/nginx).
14
+ #
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
17
+ # defaults to nil, while the second defaults to "max-age=0, private, must-revalidate"
18
+ class ETag
19
+ ETAG_STRING = Rack::ETAG
20
+ DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate"
21
+
22
+ def initialize(app, no_cache_control = nil, cache_control = DEFAULT_CACHE_CONTROL)
23
+ @app = app
24
+ @cache_control = cache_control
25
+ @no_cache_control = no_cache_control
26
+ end
27
+
28
+ def call(env)
29
+ status, headers, body = response = @app.call(env)
30
+
31
+ if etag_status?(status) && body.respond_to?(:to_ary) && !skip_caching?(headers)
32
+ body = body.to_ary
33
+ digest = digest_body(body)
34
+ headers[ETAG_STRING] = %(W/"#{digest}") if digest
35
+
36
+ # Body was modified, so we need to re-assign it:
37
+ response[2] = body
38
+ end
39
+
40
+ unless headers[CACHE_CONTROL]
41
+ if digest
42
+ headers[CACHE_CONTROL] = @cache_control if @cache_control
43
+ else
44
+ headers[CACHE_CONTROL] = @no_cache_control if @no_cache_control
45
+ end
46
+ end
47
+
48
+ response
49
+ end
50
+
51
+ private
52
+
53
+ def etag_status?(status)
54
+ status == 200 || status == 201
55
+ end
56
+
57
+ def skip_caching?(headers)
58
+ headers.key?(ETAG_STRING) || headers.key?('last-modified')
59
+ end
60
+
61
+ def digest_body(body)
62
+ digest = nil
63
+
64
+ body.each do |part|
65
+ (digest ||= Digest::SHA256.new) << part unless part.empty?
66
+ end
67
+
68
+ digest && digest.hexdigest.byteslice(0,32)
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'body_proxy'
4
+ require_relative 'request'
5
+ require_relative 'response'
6
+
7
+ module Rack
8
+ ### This middleware provides hooks to certain places in the request /
9
+ # response lifecycle. This is so that middleware that don't need to filter
10
+ # the response data can safely leave it alone and not have to send messages
11
+ # down the traditional "rack stack".
12
+ #
13
+ # The events are:
14
+ #
15
+ # * on_start(request, response)
16
+ #
17
+ # This event is sent at the start of the request, before the next
18
+ # middleware in the chain is called. This method is called with a request
19
+ # object, and a response object. Right now, the response object is always
20
+ # nil, but in the future it may actually be a real response object.
21
+ #
22
+ # * on_commit(request, response)
23
+ #
24
+ # The response has been committed. The application has returned, but the
25
+ # response has not been sent to the webserver yet. This method is always
26
+ # called with a request object and the response object. The response
27
+ # object is constructed from the rack triple that the application returned.
28
+ # Changes may still be made to the response object at this point.
29
+ #
30
+ # * on_send(request, response)
31
+ #
32
+ # The webserver has started iterating over the response body, or has called
33
+ # the streaming body, and presumably has started sending data over the
34
+ # wire. This method is always called with a request object and the response
35
+ # object. The response object is constructed from the rack triple that the
36
+ # application returned. Changes SHOULD NOT be made to the response object
37
+ # as the webserver has already started sending data. Any mutations will
38
+ # likely result in an exception.
39
+ #
40
+ # * on_finish(request, response)
41
+ #
42
+ # The webserver has closed the response, and all data has been written to
43
+ # the response socket. The request and response object should both be
44
+ # read-only at this point. The body MAY NOT be available on the response
45
+ # object as it may have been flushed to the socket.
46
+ #
47
+ # * on_error(request, response, error)
48
+ #
49
+ # An exception has occurred in the application or an `on_commit` event.
50
+ # This method will get the request, the response (if available) and the
51
+ # exception that was raised.
52
+ #
53
+ # ## Order
54
+ #
55
+ # `on_start` is called on the handlers in the order that they were passed to
56
+ # the constructor. `on_commit`, on_send`, `on_finish`, and `on_error` are
57
+ # called in the reverse order. `on_finish` handlers are called inside an
58
+ # `ensure` block, so they are guaranteed to be called even if something
59
+ # raises an exception. If something raises an exception in a `on_finish`
60
+ # method, then nothing is guaranteed.
61
+
62
+ class Events
63
+ module Abstract
64
+ def on_start(req, res)
65
+ end
66
+
67
+ def on_commit(req, res)
68
+ end
69
+
70
+ def on_send(req, res)
71
+ end
72
+
73
+ def on_finish(req, res)
74
+ end
75
+
76
+ def on_error(req, res, e)
77
+ end
78
+ end
79
+
80
+ class EventedBodyProxy < Rack::BodyProxy # :nodoc:
81
+ attr_reader :request, :response
82
+
83
+ def initialize(body, request, response, handlers, &block)
84
+ super(body, &block)
85
+ @request = request
86
+ @response = response
87
+ @handlers = handlers
88
+ end
89
+
90
+ def each
91
+ @handlers.reverse_each { |handler| handler.on_send request, response }
92
+ super
93
+ end
94
+
95
+ def call(stream)
96
+ @handlers.reverse_each { |handler| handler.on_send request, response }
97
+ super
98
+ end
99
+
100
+ def respond_to?(method_name, include_all = false)
101
+ case method_name
102
+ when :each, :call
103
+ @body.respond_to?(method_name, include_all)
104
+ else
105
+ super
106
+ end
107
+ end
108
+ end
109
+
110
+ class BufferedResponse < Rack::Response::Raw # :nodoc:
111
+ attr_reader :body
112
+
113
+ def initialize(status, headers, body)
114
+ super(status, headers)
115
+ @body = body
116
+ end
117
+
118
+ def to_a; [status, headers, body]; end
119
+ end
120
+
121
+ def initialize(app, handlers)
122
+ @app = app
123
+ @handlers = handlers
124
+ end
125
+
126
+ def call(env)
127
+ request = make_request env
128
+ on_start request, nil
129
+
130
+ begin
131
+ status, headers, body = @app.call request.env
132
+ response = make_response status, headers, body
133
+ on_commit request, response
134
+ rescue StandardError => e
135
+ on_error request, response, e
136
+ on_finish request, response
137
+ raise
138
+ end
139
+
140
+ body = EventedBodyProxy.new(body, request, response, @handlers) do
141
+ on_finish request, response
142
+ end
143
+ [response.status, response.headers, body]
144
+ end
145
+
146
+ private
147
+
148
+ def on_error(request, response, e)
149
+ @handlers.reverse_each { |handler| handler.on_error request, response, e }
150
+ end
151
+
152
+ def on_commit(request, response)
153
+ @handlers.reverse_each { |handler| handler.on_commit request, response }
154
+ end
155
+
156
+ def on_start(request, response)
157
+ @handlers.each { |handler| handler.on_start request, nil }
158
+ end
159
+
160
+ def on_finish(request, response)
161
+ @handlers.reverse_each { |handler| handler.on_finish request, response }
162
+ end
163
+
164
+ def make_request(env)
165
+ Rack::Request.new env
166
+ end
167
+
168
+ def make_response(status, headers, body)
169
+ BufferedResponse.new status, headers, body
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ require_relative 'constants'
6
+ require_relative 'head'
7
+ require_relative 'utils'
8
+ require_relative 'request'
9
+ require_relative 'mime'
10
+
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+.
19
+ #
20
+ # Be aware that just like the default behavior of most webservers, Rack::Files
21
+ # will follow symbolic links encountered under the root. If a symlink points to
22
+ # a location outside of the root, that target will still be served as part of
23
+ # the response.
24
+
25
+ class Files
26
+ ALLOWED_VERBS = %w[GET HEAD OPTIONS]
27
+ ALLOW_HEADER = ALLOWED_VERBS.join(', ')
28
+ MULTIPART_BOUNDARY = 'AaB03x'
29
+
30
+ attr_reader :root
31
+
32
+ def initialize(root, headers = {}, default_mime = 'text/plain')
33
+ @root = (::File.expand_path(root) if root)
34
+ @headers = headers
35
+ @default_mime = default_mime
36
+ @head = Rack::Head.new(lambda { |env| get env })
37
+ end
38
+
39
+ def call(env)
40
+ # HEAD requests drop the response body, including 4xx error messages.
41
+ @head.call env
42
+ end
43
+
44
+ def get(env)
45
+ request = Rack::Request.new env
46
+ unless ALLOWED_VERBS.include? request.request_method
47
+ return fail(405, "Method Not Allowed", { 'allow' => ALLOW_HEADER })
48
+ end
49
+
50
+ path_info = Utils.unescape_path request.path_info
51
+ return fail(400, "Bad Request") unless Utils.valid_path?(path_info)
52
+
53
+ clean_path_info = Utils.clean_path_info(path_info)
54
+ path = ::File.join(@root, clean_path_info)
55
+
56
+ available = begin
57
+ ::File.file?(path) && ::File.readable?(path)
58
+ rescue SystemCallError
59
+ # Not sure in what conditions this exception can occur, but this
60
+ # is a safe way to handle such an error.
61
+ # :nocov:
62
+ false
63
+ # :nocov:
64
+ end
65
+
66
+ if available
67
+ serving(request, path)
68
+ else
69
+ fail(404, "File not found: #{path_info}")
70
+ end
71
+ end
72
+
73
+ def serving(request, path)
74
+ if request.options?
75
+ return [200, { 'allow' => ALLOW_HEADER, CONTENT_LENGTH => '0' }, []]
76
+ end
77
+ last_modified = ::File.mtime(path).httpdate
78
+ return [304, {}, []] if request.get_header('HTTP_IF_MODIFIED_SINCE') == last_modified
79
+
80
+ headers = { "last-modified" => last_modified }
81
+ mime_type = mime_type path, @default_mime
82
+ headers[CONTENT_TYPE] = mime_type if mime_type
83
+
84
+ assign_headers(headers, request)
85
+
86
+ status = 200
87
+ size = filesize path
88
+
89
+ ranges = Rack::Utils.get_byte_ranges(request.get_header('HTTP_RANGE'), size)
90
+ if ranges.nil?
91
+ # No ranges:
92
+ ranges = [0..size - 1]
93
+ elsif ranges.empty?
94
+ # Unsatisfiable. Return error, and file size:
95
+ response = fail(416, "Byte range unsatisfiable")
96
+ response[1]["content-range"] = "bytes */#{size}"
97
+ return response
98
+ else
99
+ # Partial content
100
+ partial_content = true
101
+
102
+ if ranges.size == 1
103
+ range = ranges[0]
104
+ headers["content-range"] = "bytes #{range.begin}-#{range.end}/#{size}"
105
+ else
106
+ headers[CONTENT_TYPE] = "multipart/byteranges; boundary=#{MULTIPART_BOUNDARY}"
107
+ end
108
+
109
+ status = 206
110
+ body = BaseIterator.new(path, ranges, mime_type: mime_type, size: size)
111
+ size = body.bytesize
112
+ end
113
+
114
+ headers[CONTENT_LENGTH] = size.to_s
115
+
116
+ if request.head?
117
+ body = []
118
+ elsif !partial_content
119
+ body = Iterator.new(path, ranges, mime_type: mime_type, size: size)
120
+ end
121
+
122
+ [status, headers, body]
123
+ end
124
+
125
+ def assign_headers(headers, request)
126
+ headers.merge!(@headers) if @headers
127
+ end
128
+
129
+ class BaseIterator
130
+ attr_reader :path, :ranges, :options
131
+
132
+ def initialize(path, ranges, options)
133
+ @path = path
134
+ @ranges = ranges
135
+ @options = options
136
+ end
137
+
138
+ def each
139
+ ::File.open(path, "rb") do |file|
140
+ ranges.each do |range|
141
+ yield multipart_heading(range) if multipart?
142
+
143
+ each_range_part(file, range) do |part|
144
+ yield part
145
+ end
146
+ end
147
+
148
+ yield "\r\n--#{MULTIPART_BOUNDARY}--\r\n" if multipart?
149
+ end
150
+ end
151
+
152
+ def bytesize
153
+ size = ranges.inject(0) do |sum, range|
154
+ sum += multipart_heading(range).bytesize if multipart?
155
+ sum += range.size
156
+ end
157
+ size += "\r\n--#{MULTIPART_BOUNDARY}--\r\n".bytesize if multipart?
158
+ size
159
+ end
160
+
161
+ def close; end
162
+
163
+ private
164
+
165
+ def multipart?
166
+ ranges.size > 1
167
+ end
168
+
169
+ def multipart_heading(range)
170
+ <<-EOF
171
+ \r
172
+ --#{MULTIPART_BOUNDARY}\r
173
+ content-type: #{options[:mime_type]}\r
174
+ content-range: bytes #{range.begin}-#{range.end}/#{options[:size]}\r
175
+ \r
176
+ EOF
177
+ end
178
+
179
+ def each_range_part(file, range)
180
+ file.seek(range.begin)
181
+ remaining_len = range.end - range.begin + 1
182
+ while remaining_len > 0
183
+ part = file.read([8192, remaining_len].min)
184
+ break unless part
185
+ remaining_len -= part.length
186
+
187
+ yield part
188
+ end
189
+ end
190
+ end
191
+
192
+ class Iterator < BaseIterator
193
+ alias :to_path :path
194
+ end
195
+
196
+ private
197
+
198
+ def fail(status, body, headers = {})
199
+ body += "\n"
200
+
201
+ [
202
+ status,
203
+ {
204
+ CONTENT_TYPE => "text/plain",
205
+ CONTENT_LENGTH => body.size.to_s,
206
+ "x-cascade" => "pass"
207
+ }.merge!(headers),
208
+ [body]
209
+ ]
210
+ end
211
+
212
+ # The MIME type for the contents of the file located at @path
213
+ def mime_type(path, default_mime)
214
+ Mime.mime_type(::File.extname(path), default_mime)
215
+ end
216
+
217
+ def filesize(path)
218
+ # We check via File::size? whether this file provides size info
219
+ # via stat (e.g. /proc files often don't), otherwise we have to
220
+ # figure it out by reading the whole file into memory.
221
+ ::File.size?(path) || ::File.read(path).bytesize
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'constants'
4
+ require_relative 'body_proxy'
5
+
6
+ module Rack
7
+ # Rack::Head returns an empty body for all HEAD requests. It leaves
8
+ # all other requests unchanged.
9
+ class Head
10
+ def initialize(app)
11
+ @app = app
12
+ end
13
+
14
+ def call(env)
15
+ _, _, body = response = @app.call(env)
16
+
17
+ if env[REQUEST_METHOD] == HEAD
18
+ body.close if body.respond_to?(:close)
19
+ response[2] = []
20
+ end
21
+
22
+ response
23
+ end
24
+ end
25
+ end