homura-runtime 0.3.3 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/lib/homura/runtime/version.rb +1 -1
- data/vendor/rack/auth/abstract/handler.rb +41 -0
- data/vendor/rack/auth/abstract/request.rb +51 -0
- data/vendor/rack/auth/basic.rb +58 -0
- data/vendor/rack/bad_request.rb +8 -0
- data/vendor/rack/body_proxy.rb +63 -0
- data/vendor/rack/builder.rb +315 -0
- data/vendor/rack/cascade.rb +67 -0
- data/vendor/rack/common_logger.rb +94 -0
- data/vendor/rack/conditional_get.rb +87 -0
- data/vendor/rack/config.rb +22 -0
- data/vendor/rack/constants.rb +68 -0
- data/vendor/rack/content_length.rb +34 -0
- data/vendor/rack/content_type.rb +33 -0
- data/vendor/rack/deflater.rb +159 -0
- data/vendor/rack/directory.rb +210 -0
- data/vendor/rack/etag.rb +71 -0
- data/vendor/rack/events.rb +172 -0
- data/vendor/rack/files.rb +224 -0
- data/vendor/rack/head.rb +25 -0
- data/vendor/rack/headers.rb +238 -0
- data/vendor/rack/lint.rb +1000 -0
- data/vendor/rack/lock.rb +29 -0
- data/vendor/rack/media_type.rb +42 -0
- data/vendor/rack/method_override.rb +56 -0
- data/vendor/rack/mime.rb +694 -0
- data/vendor/rack/mock.rb +3 -0
- data/vendor/rack/mock_request.rb +161 -0
- data/vendor/rack/mock_response.rb +147 -0
- data/vendor/rack/multipart/generator.rb +99 -0
- data/vendor/rack/multipart/parser.rb +586 -0
- data/vendor/rack/multipart/uploaded_file.rb +82 -0
- data/vendor/rack/multipart.rb +77 -0
- data/vendor/rack/null_logger.rb +48 -0
- data/vendor/rack/protection/authenticity_token.rb +256 -0
- data/vendor/rack/protection/base.rb +140 -0
- data/vendor/rack/protection/content_security_policy.rb +80 -0
- data/vendor/rack/protection/cookie_tossing.rb +77 -0
- data/vendor/rack/protection/escaped_params.rb +93 -0
- data/vendor/rack/protection/form_token.rb +25 -0
- data/vendor/rack/protection/frame_options.rb +39 -0
- data/vendor/rack/protection/http_origin.rb +43 -0
- data/vendor/rack/protection/ip_spoofing.rb +27 -0
- data/vendor/rack/protection/json_csrf.rb +60 -0
- data/vendor/rack/protection/path_traversal.rb +45 -0
- data/vendor/rack/protection/referrer_policy.rb +27 -0
- data/vendor/rack/protection/remote_referrer.rb +22 -0
- data/vendor/rack/protection/remote_token.rb +24 -0
- data/vendor/rack/protection/session_hijacking.rb +37 -0
- data/vendor/rack/protection/strict_transport.rb +41 -0
- data/vendor/rack/protection/version.rb +7 -0
- data/vendor/rack/protection/xss_header.rb +27 -0
- data/vendor/rack/protection.rb +58 -0
- data/vendor/rack/query_parser.rb +261 -0
- data/vendor/rack/recursive.rb +66 -0
- data/vendor/rack/reloader.rb +112 -0
- data/vendor/rack/request.rb +818 -0
- data/vendor/rack/response.rb +403 -0
- data/vendor/rack/rewindable_input.rb +116 -0
- data/vendor/rack/runtime.rb +35 -0
- data/vendor/rack/sendfile.rb +197 -0
- data/vendor/rack/session/abstract/id.rb +533 -0
- data/vendor/rack/session/constants.rb +13 -0
- data/vendor/rack/session/cookie.rb +292 -0
- data/vendor/rack/session/encryptor.rb +415 -0
- data/vendor/rack/session/pool.rb +76 -0
- data/vendor/rack/session/version.rb +10 -0
- data/vendor/rack/session.rb +12 -0
- data/vendor/rack/show_exceptions.rb +433 -0
- data/vendor/rack/show_status.rb +121 -0
- data/vendor/rack/static.rb +188 -0
- data/vendor/rack/tempfile_reaper.rb +44 -0
- data/vendor/rack/urlmap.rb +99 -0
- data/vendor/rack/utils.rb +631 -0
- data/vendor/rack/version.rb +17 -0
- data/vendor/rack.rb +66 -0
- metadata +76 -1
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
# -*- encoding: binary -*-
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# homura patch: Opal's URI module does not provide RFC2396_PARSER /
|
|
5
|
+
# DEFAULT_PARSER, and `fileutils` / `tempfile` are stubbed. We use `cgi`
|
|
6
|
+
# for escape / unescape and provide a tiny CGI-backed URI_PARSER so the
|
|
7
|
+
# rest of rack/utils.rb continues to work unchanged.
|
|
8
|
+
require 'cgi'
|
|
9
|
+
# require 'uri'
|
|
10
|
+
# require 'fileutils'
|
|
11
|
+
require 'set'
|
|
12
|
+
require 'tempfile'
|
|
13
|
+
require 'time'
|
|
14
|
+
require 'erb'
|
|
15
|
+
|
|
16
|
+
require_relative 'query_parser'
|
|
17
|
+
require_relative 'mime'
|
|
18
|
+
require_relative 'headers'
|
|
19
|
+
require_relative 'constants'
|
|
20
|
+
|
|
21
|
+
module Rack
|
|
22
|
+
# Rack::Utils contains a grab-bag of useful methods for writing web
|
|
23
|
+
# applications adopted from all kinds of Ruby libraries.
|
|
24
|
+
|
|
25
|
+
module Utils
|
|
26
|
+
ParameterTypeError = QueryParser::ParameterTypeError
|
|
27
|
+
InvalidParameterError = QueryParser::InvalidParameterError
|
|
28
|
+
ParamsTooDeepError = QueryParser::ParamsTooDeepError
|
|
29
|
+
DEFAULT_SEP = QueryParser::DEFAULT_SEP
|
|
30
|
+
COMMON_SEP = QueryParser::COMMON_SEP
|
|
31
|
+
KeySpaceConstrainedParams = QueryParser::Params
|
|
32
|
+
# homura patch: CGI-backed URI_PARSER stand-in for Opal.
|
|
33
|
+
URI_PARSER = Module.new do
|
|
34
|
+
def self.escape(s); CGI.escape(s.to_s); end
|
|
35
|
+
def self.unescape(s); CGI.unescape(s.to_s); end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class << self
|
|
39
|
+
attr_accessor :default_query_parser
|
|
40
|
+
end
|
|
41
|
+
# The default amount of nesting to allowed by hash parameters.
|
|
42
|
+
# This helps prevent a rogue client from triggering a possible stack overflow
|
|
43
|
+
# when parsing parameters.
|
|
44
|
+
self.default_query_parser = QueryParser.make_default(32)
|
|
45
|
+
|
|
46
|
+
module_function
|
|
47
|
+
|
|
48
|
+
# URI escapes. (CGI style space to +)
|
|
49
|
+
def escape(s)
|
|
50
|
+
URI.encode_www_form_component(s)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Like URI escaping, but with %20 instead of +. Strictly speaking this is
|
|
54
|
+
# true URI escaping.
|
|
55
|
+
def escape_path(s)
|
|
56
|
+
URI_PARSER.escape s
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Unescapes the **path** component of a URI. See Rack::Utils.unescape for
|
|
60
|
+
# unescaping query parameters or form components.
|
|
61
|
+
def unescape_path(s)
|
|
62
|
+
URI_PARSER.unescape s
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Unescapes a URI escaped string with +encoding+. +encoding+ will be the
|
|
66
|
+
# target encoding of the string returned, and it defaults to UTF-8
|
|
67
|
+
def unescape(s, encoding = Encoding::UTF_8)
|
|
68
|
+
URI.decode_www_form_component(s, encoding)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
class << self
|
|
72
|
+
attr_accessor :multipart_total_part_limit
|
|
73
|
+
|
|
74
|
+
attr_accessor :multipart_file_limit
|
|
75
|
+
|
|
76
|
+
# multipart_part_limit is the original name of multipart_file_limit, but
|
|
77
|
+
# the limit only counts parts with filenames.
|
|
78
|
+
alias multipart_part_limit multipart_file_limit
|
|
79
|
+
alias multipart_part_limit= multipart_file_limit=
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The maximum number of file parts a request can contain. Accepting too
|
|
83
|
+
# many parts can lead to the server running out of file handles.
|
|
84
|
+
# Set to `0` for no limit.
|
|
85
|
+
self.multipart_file_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || ENV['RACK_MULTIPART_FILE_LIMIT'] || 128).to_i
|
|
86
|
+
|
|
87
|
+
# The maximum total number of parts a request can contain. Accepting too
|
|
88
|
+
# many can lead to excessive memory use and parsing time.
|
|
89
|
+
self.multipart_total_part_limit = (ENV['RACK_MULTIPART_TOTAL_PART_LIMIT'] || 4096).to_i
|
|
90
|
+
|
|
91
|
+
def self.param_depth_limit
|
|
92
|
+
default_query_parser.param_depth_limit
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.param_depth_limit=(v)
|
|
96
|
+
self.default_query_parser = self.default_query_parser.new_depth_limit(v)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
if defined?(Process::CLOCK_MONOTONIC)
|
|
100
|
+
def clock_time
|
|
101
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
102
|
+
end
|
|
103
|
+
else
|
|
104
|
+
# :nocov:
|
|
105
|
+
def clock_time
|
|
106
|
+
Time.now.to_f
|
|
107
|
+
end
|
|
108
|
+
# :nocov:
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def parse_query(qs, d = nil, &unescaper)
|
|
112
|
+
Rack::Utils.default_query_parser.parse_query(qs, d, &unescaper)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def parse_nested_query(qs, d = nil)
|
|
116
|
+
Rack::Utils.default_query_parser.parse_nested_query(qs, d)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def build_query(params)
|
|
120
|
+
params.map { |k, v|
|
|
121
|
+
if v.class == Array
|
|
122
|
+
build_query(v.map { |x| [k, x] })
|
|
123
|
+
else
|
|
124
|
+
v.nil? ? escape(k) : "#{escape(k)}=#{escape(v)}"
|
|
125
|
+
end
|
|
126
|
+
}.join("&")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def build_nested_query(value, prefix = nil)
|
|
130
|
+
case value
|
|
131
|
+
when Array
|
|
132
|
+
value.map { |v|
|
|
133
|
+
build_nested_query(v, "#{prefix}[]")
|
|
134
|
+
}.join("&")
|
|
135
|
+
when Hash
|
|
136
|
+
value.map { |k, v|
|
|
137
|
+
build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
|
|
138
|
+
}.delete_if(&:empty?).join('&')
|
|
139
|
+
when nil
|
|
140
|
+
escape(prefix)
|
|
141
|
+
else
|
|
142
|
+
raise ArgumentError, "value must be a Hash" if prefix.nil?
|
|
143
|
+
"#{escape(prefix)}=#{escape(value)}"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def q_values(q_value_header)
|
|
148
|
+
q_value_header.to_s.split(',').map do |part|
|
|
149
|
+
value, parameters = part.split(';', 2).map(&:strip)
|
|
150
|
+
quality = 1.0
|
|
151
|
+
if parameters && (md = /\Aq=([\d.]+)/.match(parameters))
|
|
152
|
+
quality = md[1].to_f
|
|
153
|
+
end
|
|
154
|
+
[value, quality]
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def forwarded_values(forwarded_header)
|
|
159
|
+
return nil unless forwarded_header
|
|
160
|
+
forwarded_header = forwarded_header.to_s.gsub("\n", ";")
|
|
161
|
+
|
|
162
|
+
forwarded_header.split(';').each_with_object({}) do |field, values|
|
|
163
|
+
field.split(',').each do |pair|
|
|
164
|
+
pair = pair.split('=').map(&:strip).join('=')
|
|
165
|
+
return nil unless pair =~ /\A(by|for|host|proto)="?([^"]+)"?\Z/i
|
|
166
|
+
(values[$1.downcase.to_sym] ||= []) << $2
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
module_function :forwarded_values
|
|
171
|
+
|
|
172
|
+
# Return best accept value to use, based on the algorithm
|
|
173
|
+
# in RFC 2616 Section 14. If there are multiple best
|
|
174
|
+
# matches (same specificity and quality), the value returned
|
|
175
|
+
# is arbitrary.
|
|
176
|
+
def best_q_match(q_value_header, available_mimes)
|
|
177
|
+
values = q_values(q_value_header)
|
|
178
|
+
|
|
179
|
+
matches = values.map do |req_mime, quality|
|
|
180
|
+
match = available_mimes.find { |am| Rack::Mime.match?(am, req_mime) }
|
|
181
|
+
next unless match
|
|
182
|
+
[match, quality]
|
|
183
|
+
end.compact.sort_by do |match, quality|
|
|
184
|
+
(match.split('/', 2).count('*') * -10) + quality
|
|
185
|
+
end.last
|
|
186
|
+
matches&.first
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Introduced in ERB 4.0. ERB::Escape is an alias for ERB::Utils which
|
|
190
|
+
# doesn't get monkey-patched by rails
|
|
191
|
+
if defined?(ERB::Escape) && ERB::Escape.instance_method(:html_escape)
|
|
192
|
+
define_method(:escape_html, ERB::Escape.instance_method(:html_escape))
|
|
193
|
+
# :nocov:
|
|
194
|
+
# Ruby 3.2/ERB 4.0 added ERB::Escape#html_escape, so the else
|
|
195
|
+
# branch cannot be hit on the current Ruby version.
|
|
196
|
+
else
|
|
197
|
+
require 'cgi/escape'
|
|
198
|
+
# Escape ampersands, brackets and quotes to their HTML/XML entities.
|
|
199
|
+
def escape_html(string)
|
|
200
|
+
CGI.escapeHTML(string.to_s)
|
|
201
|
+
end
|
|
202
|
+
# :nocov:
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def select_best_encoding(available_encodings, accept_encoding)
|
|
206
|
+
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
|
|
207
|
+
|
|
208
|
+
expanded_accept_encoding = []
|
|
209
|
+
|
|
210
|
+
accept_encoding.each do |m, q|
|
|
211
|
+
preference = available_encodings.index(m) || available_encodings.size
|
|
212
|
+
|
|
213
|
+
if m == "*"
|
|
214
|
+
(available_encodings - accept_encoding.map(&:first)).each do |m2|
|
|
215
|
+
expanded_accept_encoding << [m2, q, preference]
|
|
216
|
+
end
|
|
217
|
+
else
|
|
218
|
+
expanded_accept_encoding << [m, q, preference]
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
encoding_candidates = expanded_accept_encoding
|
|
223
|
+
.sort_by { |_, q, p| [-q, p] }
|
|
224
|
+
.map!(&:first)
|
|
225
|
+
|
|
226
|
+
unless encoding_candidates.include?("identity")
|
|
227
|
+
encoding_candidates.push("identity")
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
expanded_accept_encoding.each do |m, q|
|
|
231
|
+
encoding_candidates.delete(m) if q == 0.0
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
(encoding_candidates & available_encodings)[0]
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# :call-seq:
|
|
238
|
+
# parse_cookies_header(value) -> hash
|
|
239
|
+
#
|
|
240
|
+
# Parse cookies from the provided header +value+ according to RFC6265. The
|
|
241
|
+
# syntax for cookie headers only supports semicolons. Returns a map of
|
|
242
|
+
# cookie +key+ to cookie +value+.
|
|
243
|
+
#
|
|
244
|
+
# parse_cookies_header('myname=myvalue; max-age=0')
|
|
245
|
+
# # => {"myname"=>"myvalue", "max-age"=>"0"}
|
|
246
|
+
#
|
|
247
|
+
def parse_cookies_header(value)
|
|
248
|
+
return {} unless value
|
|
249
|
+
|
|
250
|
+
value.split(/; */n).each_with_object({}) do |cookie, cookies|
|
|
251
|
+
next if cookie.empty?
|
|
252
|
+
key, value = cookie.split('=', 2)
|
|
253
|
+
cookies[key] = (unescape(value) rescue value) unless cookies.key?(key)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# :call-seq:
|
|
258
|
+
# parse_cookies(env) -> hash
|
|
259
|
+
#
|
|
260
|
+
# Parse cookies from the provided request environment using
|
|
261
|
+
# parse_cookies_header. Returns a map of cookie +key+ to cookie +value+.
|
|
262
|
+
#
|
|
263
|
+
# parse_cookies({'HTTP_COOKIE' => 'myname=myvalue'})
|
|
264
|
+
# # => {'myname' => 'myvalue'}
|
|
265
|
+
#
|
|
266
|
+
def parse_cookies(env)
|
|
267
|
+
parse_cookies_header env[HTTP_COOKIE]
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# A valid cookie key according to RFC6265 and RFC2616.
|
|
271
|
+
# A <cookie-name> can be any US-ASCII characters, except control characters, spaces, or tabs. It also must not contain a separator character like the following: ( ) < > @ , ; : \ " / [ ] ? = { }.
|
|
272
|
+
VALID_COOKIE_KEY = /\A[!#$%&'*+\-\.\^_`|~0-9a-zA-Z]+\z/.freeze
|
|
273
|
+
private_constant :VALID_COOKIE_KEY
|
|
274
|
+
|
|
275
|
+
# :call-seq:
|
|
276
|
+
# set_cookie_header(key, value) -> encoded string
|
|
277
|
+
#
|
|
278
|
+
# Generate an encoded string using the provided +key+ and +value+ suitable
|
|
279
|
+
# for the +set-cookie+ header according to RFC6265. The +value+ may be an
|
|
280
|
+
# instance of either +String+ or +Hash+. If the cookie key is invalid (as
|
|
281
|
+
# defined by RFC6265), an +ArgumentError+ will be raised.
|
|
282
|
+
#
|
|
283
|
+
# If the cookie +value+ is an instance of +Hash+, it considers the following
|
|
284
|
+
# cookie attribute keys: +domain+, +max_age+, +expires+ (must be instance
|
|
285
|
+
# of +Time+), +secure+, +http_only+, +same_site+ and +value+. For more
|
|
286
|
+
# details about the interpretation of these fields, consult
|
|
287
|
+
# [RFC6265 Section 5.2](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2).
|
|
288
|
+
#
|
|
289
|
+
# set_cookie_header("myname", "myvalue")
|
|
290
|
+
# # => "myname=myvalue"
|
|
291
|
+
#
|
|
292
|
+
# set_cookie_header("myname", {value: "myvalue", max_age: 10})
|
|
293
|
+
# # => "myname=myvalue; max-age=10"
|
|
294
|
+
#
|
|
295
|
+
def set_cookie_header(key, value)
|
|
296
|
+
unless key =~ VALID_COOKIE_KEY
|
|
297
|
+
raise ArgumentError, "invalid cookie key: #{key.inspect}"
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
case value
|
|
301
|
+
when Hash
|
|
302
|
+
domain = "; domain=#{value[:domain]}" if value[:domain]
|
|
303
|
+
path = "; path=#{value[:path]}" if value[:path]
|
|
304
|
+
max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
|
|
305
|
+
expires = "; expires=#{value[:expires].httpdate}" if value[:expires]
|
|
306
|
+
secure = "; secure" if value[:secure]
|
|
307
|
+
httponly = "; httponly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
|
|
308
|
+
same_site =
|
|
309
|
+
case value[:same_site]
|
|
310
|
+
when false, nil
|
|
311
|
+
nil
|
|
312
|
+
when :none, 'None', :None
|
|
313
|
+
'; samesite=none'
|
|
314
|
+
when :lax, 'Lax', :Lax
|
|
315
|
+
'; samesite=lax'
|
|
316
|
+
when true, :strict, 'Strict', :Strict
|
|
317
|
+
'; samesite=strict'
|
|
318
|
+
else
|
|
319
|
+
raise ArgumentError, "Invalid :same_site value: #{value[:same_site].inspect}"
|
|
320
|
+
end
|
|
321
|
+
partitioned = "; partitioned" if value[:partitioned]
|
|
322
|
+
value = value[:value]
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
value = [value] unless Array === value
|
|
326
|
+
|
|
327
|
+
return "#{key}=#{value.map { |v| escape v }.join('&')}#{domain}" \
|
|
328
|
+
"#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}#{partitioned}"
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# :call-seq:
|
|
332
|
+
# set_cookie_header!(headers, key, value) -> header value
|
|
333
|
+
#
|
|
334
|
+
# Append a cookie in the specified headers with the given cookie +key+ and
|
|
335
|
+
# +value+ using set_cookie_header.
|
|
336
|
+
#
|
|
337
|
+
# If the headers already contains a +set-cookie+ key, it will be converted
|
|
338
|
+
# to an +Array+ if not already, and appended to.
|
|
339
|
+
def set_cookie_header!(headers, key, value)
|
|
340
|
+
if header = headers[SET_COOKIE]
|
|
341
|
+
if header.is_a?(Array)
|
|
342
|
+
header << set_cookie_header(key, value)
|
|
343
|
+
else
|
|
344
|
+
headers[SET_COOKIE] = [header, set_cookie_header(key, value)]
|
|
345
|
+
end
|
|
346
|
+
else
|
|
347
|
+
headers[SET_COOKIE] = set_cookie_header(key, value)
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# :call-seq:
|
|
352
|
+
# delete_set_cookie_header(key, value = {}) -> encoded string
|
|
353
|
+
#
|
|
354
|
+
# Generate an encoded string based on the given +key+ and +value+ using
|
|
355
|
+
# set_cookie_header for the purpose of causing the specified cookie to be
|
|
356
|
+
# deleted. The +value+ may be an instance of +Hash+ and can include
|
|
357
|
+
# attributes as outlined by set_cookie_header. The encoded cookie will have
|
|
358
|
+
# a +max_age+ of 0 seconds, an +expires+ date in the past and an empty
|
|
359
|
+
# +value+. When used with the +set-cookie+ header, it will cause the client
|
|
360
|
+
# to *remove* any matching cookie.
|
|
361
|
+
#
|
|
362
|
+
# delete_set_cookie_header("myname")
|
|
363
|
+
# # => "myname=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
|
|
364
|
+
#
|
|
365
|
+
def delete_set_cookie_header(key, value = {})
|
|
366
|
+
set_cookie_header(key, value.merge(max_age: '0', expires: Time.at(0), value: ''))
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def delete_cookie_header!(headers, key, value = {})
|
|
370
|
+
headers[SET_COOKIE] = delete_set_cookie_header!(headers[SET_COOKIE], key, value)
|
|
371
|
+
|
|
372
|
+
return nil
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# :call-seq:
|
|
376
|
+
# delete_set_cookie_header!(header, key, value = {}) -> header value
|
|
377
|
+
#
|
|
378
|
+
# Set an expired cookie in the specified headers with the given cookie
|
|
379
|
+
# +key+ and +value+ using delete_set_cookie_header. This causes
|
|
380
|
+
# the client to immediately delete the specified cookie.
|
|
381
|
+
#
|
|
382
|
+
# delete_set_cookie_header!(nil, "mycookie")
|
|
383
|
+
# # => "mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
|
|
384
|
+
#
|
|
385
|
+
# If the header is non-nil, it will be modified in place.
|
|
386
|
+
#
|
|
387
|
+
# header = []
|
|
388
|
+
# delete_set_cookie_header!(header, "mycookie")
|
|
389
|
+
# # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
|
|
390
|
+
# header
|
|
391
|
+
# # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
|
|
392
|
+
#
|
|
393
|
+
def delete_set_cookie_header!(header, key, value = {})
|
|
394
|
+
if header
|
|
395
|
+
header = Array(header)
|
|
396
|
+
header << delete_set_cookie_header(key, value)
|
|
397
|
+
else
|
|
398
|
+
header = delete_set_cookie_header(key, value)
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
return header
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def rfc2822(time)
|
|
405
|
+
time.rfc2822
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Parses the "Range:" header, if present, into an array of Range objects.
|
|
409
|
+
# Returns nil if the header is missing or syntactically invalid.
|
|
410
|
+
# Returns an empty array if none of the ranges are satisfiable.
|
|
411
|
+
def byte_ranges(env, size)
|
|
412
|
+
get_byte_ranges env['HTTP_RANGE'], size
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def get_byte_ranges(http_range, size)
|
|
416
|
+
# See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
|
|
417
|
+
# Ignore Range when file size is 0 to avoid a 416 error.
|
|
418
|
+
return nil if size.zero?
|
|
419
|
+
return nil unless http_range && http_range =~ /bytes=([^;]+)/
|
|
420
|
+
ranges = []
|
|
421
|
+
$1.split(/,[ \t]*/).each do |range_spec|
|
|
422
|
+
return nil unless range_spec.include?('-')
|
|
423
|
+
range = range_spec.split('-')
|
|
424
|
+
r0, r1 = range[0], range[1]
|
|
425
|
+
if r0.nil? || r0.empty?
|
|
426
|
+
return nil if r1.nil?
|
|
427
|
+
# suffix-byte-range-spec, represents trailing suffix of file
|
|
428
|
+
r0 = size - r1.to_i
|
|
429
|
+
r0 = 0 if r0 < 0
|
|
430
|
+
r1 = size - 1
|
|
431
|
+
else
|
|
432
|
+
r0 = r0.to_i
|
|
433
|
+
if r1.nil?
|
|
434
|
+
r1 = size - 1
|
|
435
|
+
else
|
|
436
|
+
r1 = r1.to_i
|
|
437
|
+
return nil if r1 < r0 # backwards range is syntactically invalid
|
|
438
|
+
r1 = size - 1 if r1 >= size
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
ranges << (r0..r1) if r0 <= r1
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
return [] if ranges.map(&:size).sum > size
|
|
445
|
+
|
|
446
|
+
ranges
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
# :nocov:
|
|
450
|
+
if defined?(OpenSSL.fixed_length_secure_compare)
|
|
451
|
+
# Constant time string comparison.
|
|
452
|
+
#
|
|
453
|
+
# NOTE: the values compared should be of fixed length, such as strings
|
|
454
|
+
# that have already been processed by HMAC. This should not be used
|
|
455
|
+
# on variable length plaintext strings because it could leak length info
|
|
456
|
+
# via timing attacks.
|
|
457
|
+
def secure_compare(a, b)
|
|
458
|
+
return false unless a.bytesize == b.bytesize
|
|
459
|
+
|
|
460
|
+
OpenSSL.fixed_length_secure_compare(a, b)
|
|
461
|
+
end
|
|
462
|
+
# :nocov:
|
|
463
|
+
else
|
|
464
|
+
def secure_compare(a, b)
|
|
465
|
+
return false unless a.bytesize == b.bytesize
|
|
466
|
+
|
|
467
|
+
l = a.unpack("C*")
|
|
468
|
+
|
|
469
|
+
r, i = 0, -1
|
|
470
|
+
b.each_byte { |v| r |= v ^ l[i += 1] }
|
|
471
|
+
r == 0
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# Context allows the use of a compatible middleware at different points
|
|
476
|
+
# in a request handling stack. A compatible middleware must define
|
|
477
|
+
# #context which should take the arguments env and app. The first of which
|
|
478
|
+
# would be the request environment. The second of which would be the rack
|
|
479
|
+
# application that the request would be forwarded to.
|
|
480
|
+
class Context
|
|
481
|
+
attr_reader :for, :app
|
|
482
|
+
|
|
483
|
+
def initialize(app_f, app_r)
|
|
484
|
+
raise 'running context does not respond to #context' unless app_f.respond_to? :context
|
|
485
|
+
@for, @app = app_f, app_r
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def call(env)
|
|
489
|
+
@for.context(env, @app)
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def recontext(app)
|
|
493
|
+
self.class.new(@for, app)
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def context(env, app = @app)
|
|
497
|
+
recontext(app).call(env)
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
# Every standard HTTP code mapped to the appropriate message.
|
|
502
|
+
# Generated with:
|
|
503
|
+
# curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv \
|
|
504
|
+
# | ruby -rcsv -e "puts CSV.parse(STDIN, headers: true) \
|
|
505
|
+
# .reject {|v| v['Description'] == 'Unassigned' or v['Description'].include? '(' } \
|
|
506
|
+
# .map {|v| %Q/#{v['Value']} => '#{v['Description']}'/ }.join(','+?\n)"
|
|
507
|
+
HTTP_STATUS_CODES = {
|
|
508
|
+
100 => 'Continue',
|
|
509
|
+
101 => 'Switching Protocols',
|
|
510
|
+
102 => 'Processing',
|
|
511
|
+
103 => 'Early Hints',
|
|
512
|
+
200 => 'OK',
|
|
513
|
+
201 => 'Created',
|
|
514
|
+
202 => 'Accepted',
|
|
515
|
+
203 => 'Non-Authoritative Information',
|
|
516
|
+
204 => 'No Content',
|
|
517
|
+
205 => 'Reset Content',
|
|
518
|
+
206 => 'Partial Content',
|
|
519
|
+
207 => 'Multi-Status',
|
|
520
|
+
208 => 'Already Reported',
|
|
521
|
+
226 => 'IM Used',
|
|
522
|
+
300 => 'Multiple Choices',
|
|
523
|
+
301 => 'Moved Permanently',
|
|
524
|
+
302 => 'Found',
|
|
525
|
+
303 => 'See Other',
|
|
526
|
+
304 => 'Not Modified',
|
|
527
|
+
305 => 'Use Proxy',
|
|
528
|
+
307 => 'Temporary Redirect',
|
|
529
|
+
308 => 'Permanent Redirect',
|
|
530
|
+
400 => 'Bad Request',
|
|
531
|
+
401 => 'Unauthorized',
|
|
532
|
+
402 => 'Payment Required',
|
|
533
|
+
403 => 'Forbidden',
|
|
534
|
+
404 => 'Not Found',
|
|
535
|
+
405 => 'Method Not Allowed',
|
|
536
|
+
406 => 'Not Acceptable',
|
|
537
|
+
407 => 'Proxy Authentication Required',
|
|
538
|
+
408 => 'Request Timeout',
|
|
539
|
+
409 => 'Conflict',
|
|
540
|
+
410 => 'Gone',
|
|
541
|
+
411 => 'Length Required',
|
|
542
|
+
412 => 'Precondition Failed',
|
|
543
|
+
413 => 'Content Too Large',
|
|
544
|
+
414 => 'URI Too Long',
|
|
545
|
+
415 => 'Unsupported Media Type',
|
|
546
|
+
416 => 'Range Not Satisfiable',
|
|
547
|
+
417 => 'Expectation Failed',
|
|
548
|
+
421 => 'Misdirected Request',
|
|
549
|
+
422 => 'Unprocessable Content',
|
|
550
|
+
423 => 'Locked',
|
|
551
|
+
424 => 'Failed Dependency',
|
|
552
|
+
425 => 'Too Early',
|
|
553
|
+
426 => 'Upgrade Required',
|
|
554
|
+
428 => 'Precondition Required',
|
|
555
|
+
429 => 'Too Many Requests',
|
|
556
|
+
431 => 'Request Header Fields Too Large',
|
|
557
|
+
451 => 'Unavailable For Legal Reasons',
|
|
558
|
+
500 => 'Internal Server Error',
|
|
559
|
+
501 => 'Not Implemented',
|
|
560
|
+
502 => 'Bad Gateway',
|
|
561
|
+
503 => 'Service Unavailable',
|
|
562
|
+
504 => 'Gateway Timeout',
|
|
563
|
+
505 => 'HTTP Version Not Supported',
|
|
564
|
+
506 => 'Variant Also Negotiates',
|
|
565
|
+
507 => 'Insufficient Storage',
|
|
566
|
+
508 => 'Loop Detected',
|
|
567
|
+
511 => 'Network Authentication Required'
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
# Responses with HTTP status codes that should not have an entity body
|
|
571
|
+
STATUS_WITH_NO_ENTITY_BODY = Hash[((100..199).to_a << 204 << 304).product([true])]
|
|
572
|
+
|
|
573
|
+
SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
|
|
574
|
+
[message.downcase.gsub(/\s|-/, '_').to_sym, code]
|
|
575
|
+
}.flatten]
|
|
576
|
+
|
|
577
|
+
OBSOLETE_SYMBOLS_TO_STATUS_CODES = {
|
|
578
|
+
payload_too_large: 413,
|
|
579
|
+
unprocessable_entity: 422,
|
|
580
|
+
bandwidth_limit_exceeded: 509,
|
|
581
|
+
not_extended: 510
|
|
582
|
+
}.freeze
|
|
583
|
+
private_constant :OBSOLETE_SYMBOLS_TO_STATUS_CODES
|
|
584
|
+
|
|
585
|
+
OBSOLETE_SYMBOL_MAPPINGS = {
|
|
586
|
+
payload_too_large: :content_too_large,
|
|
587
|
+
unprocessable_entity: :unprocessable_content
|
|
588
|
+
}.freeze
|
|
589
|
+
private_constant :OBSOLETE_SYMBOL_MAPPINGS
|
|
590
|
+
|
|
591
|
+
def status_code(status)
|
|
592
|
+
if status.is_a?(Symbol)
|
|
593
|
+
SYMBOL_TO_STATUS_CODE.fetch(status) do
|
|
594
|
+
fallback_code = OBSOLETE_SYMBOLS_TO_STATUS_CODES.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" }
|
|
595
|
+
message = "Status code #{status.inspect} is deprecated and will be removed in a future version of Rack."
|
|
596
|
+
if canonical_symbol = OBSOLETE_SYMBOL_MAPPINGS[status]
|
|
597
|
+
message = "#{message} Please use #{canonical_symbol.inspect} instead."
|
|
598
|
+
end
|
|
599
|
+
warn message, uplevel: 3
|
|
600
|
+
fallback_code
|
|
601
|
+
end
|
|
602
|
+
else
|
|
603
|
+
status.to_i
|
|
604
|
+
end
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
PATH_SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact).freeze
|
|
608
|
+
|
|
609
|
+
def clean_path_info(path_info)
|
|
610
|
+
parts = path_info.split PATH_SEPS
|
|
611
|
+
|
|
612
|
+
clean = []
|
|
613
|
+
|
|
614
|
+
parts.each do |part|
|
|
615
|
+
next if part.empty? || part == '.'
|
|
616
|
+
part == '..' ? clean.pop : clean << part
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
clean_path = clean.join(::File::SEPARATOR)
|
|
620
|
+
clean_path.prepend("/") if parts.empty? || parts.first.empty?
|
|
621
|
+
clean_path
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
NULL_BYTE = "\0"
|
|
625
|
+
|
|
626
|
+
def valid_path?(path)
|
|
627
|
+
path.valid_encoding? && !path.include?(NULL_BYTE)
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
end
|
|
631
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright (C) 2007-2019 Leah Neukirchen <http://leahneukirchen.org/infopage.html>
|
|
4
|
+
#
|
|
5
|
+
# Rack is freely distributable under the terms of an MIT-style license.
|
|
6
|
+
# See MIT-LICENSE or https://opensource.org/licenses/MIT.
|
|
7
|
+
|
|
8
|
+
module Rack
|
|
9
|
+
VERSION = "3.2.0"
|
|
10
|
+
|
|
11
|
+
RELEASE = VERSION
|
|
12
|
+
|
|
13
|
+
# Return the Rack release as a dotted string.
|
|
14
|
+
def self.release
|
|
15
|
+
VERSION
|
|
16
|
+
end
|
|
17
|
+
end
|