`Rack::Lint.new(nil)` is invalid.
[rack.git] / lib / rack / lint.rb
blob0f62038c517e7ecb593b7d12970890fee726919b
1 # frozen_string_literal: true
3 require 'forwardable'
4 require 'uri'
6 require_relative 'constants'
7 require_relative 'utils'
9 module Rack
10   # Rack::Lint validates your application and the requests and
11   # responses according to the Rack spec.
13   class Lint
14     ALLOWED_SCHEMES = %w(https http wss ws).freeze
16     REQUEST_PATH_ORIGIN_FORM = /\A\/[^#]*\z/
17     REQUEST_PATH_ABSOLUTE_FORM = /\A#{Utils::URI_PARSER.make_regexp}\z/
18     REQUEST_PATH_AUTHORITY_FORM = /\A[^\/:]+:\d+\z/
19     REQUEST_PATH_ASTERISK_FORM = '*'
21     # :stopdoc:
23     class LintError < RuntimeError; end
24     # AUTHORS: n.b. The trailing whitespace between paragraphs is important and
25     # should not be removed. The whitespace creates paragraphs in the RDoc
26     # output.
27     #
28     ## This specification aims to formalize the Rack protocol. You
29     ## can (and should) use Rack::Lint to enforce it.
30     ##
31     ## When you develop middleware, be sure to add a Lint before and
32     ## after to catch all mistakes.
33     ##
34     ## = Rack applications
35     ##
36     ## A Rack application is a Ruby object (not a class) that
37     ## responds to +call+.
38     def initialize(app)
39       raise LintError, "app must respond to call" unless app.respond_to?(:call)
41       @app = app
42     end
44     def call(env = nil)
45       Wrapper.new(@app, env).response
46     end
48     class Wrapper
49       def initialize(app, env)
50         @app = app
51         @env = env
52         @response = nil
53         @head_request = false
55         @status = nil
56         @headers = nil
57         @body = nil
58         @invoked = nil
59         @content_length = nil
60         @closed = false
61         @size = 0
62       end
64       def response
65         ## It takes exactly one argument, the *environment*
66         raise LintError, "No env given" unless @env
67         check_environment(@env)
69         ## and returns a non-frozen Array of exactly three values:
70         @response = @app.call(@env)
71         raise LintError, "response is not an Array, but #{@response.class}" unless @response.kind_of? Array
72         raise LintError, "response is frozen" if @response.frozen?
73         raise LintError, "response array has #{@response.size} elements instead of 3" unless @response.size == 3
75         @status, @headers, @body = @response
76         ## The *status*,
77         check_status(@status)
79         ## the *headers*,
80         check_headers(@headers)
82         hijack_proc = check_hijack_response(@headers, @env)
83         if hijack_proc
84           @headers[RACK_HIJACK] = hijack_proc
85         end
87         ## and the *body*.
88         check_content_type_header(@status, @headers)
89         check_content_length_header(@status, @headers)
90         check_rack_protocol_header(@status, @headers)
91         @head_request = @env[REQUEST_METHOD] == HEAD
93         @lint = (@env['rack.lint'] ||= []) << self
95         if (@env['rack.lint.body_iteration'] ||= 0) > 0
96           raise LintError, "Middleware must not call #each directly"
97         end
99         return [@status, @headers, self]
100       end
102       ##
103       ## == The Environment
104       ##
105       def check_environment(env)
106         ## The environment must be an unfrozen instance of Hash that includes
107         ## CGI-like headers. The Rack application is free to modify the
108         ## environment.
109         raise LintError, "env #{env.inspect} is not a Hash, but #{env.class}" unless env.kind_of? Hash
110         raise LintError, "env should not be frozen, but is" if env.frozen?
112         ##
113         ## The environment is required to include these variables
114         ## (adopted from {PEP 333}[https://peps.python.org/pep-0333/]), except when they'd be empty, but see
115         ## below.
117         ## <tt>REQUEST_METHOD</tt>:: The HTTP request method, such as
118         ##                           "GET" or "POST". This cannot ever
119         ##                           be an empty string, and so is
120         ##                           always required.
122         ## <tt>SCRIPT_NAME</tt>:: The initial portion of the request
123         ##                        URL's "path" that corresponds to the
124         ##                        application object, so that the
125         ##                        application knows its virtual
126         ##                        "location". This may be an empty
127         ##                        string, if the application corresponds
128         ##                        to the "root" of the server.
130         ## <tt>PATH_INFO</tt>:: The remainder of the request URL's
131         ##                      "path", designating the virtual
132         ##                      "location" of the request's target
133         ##                      within the application. This may be an
134         ##                      empty string, if the request URL targets
135         ##                      the application root and does not have a
136         ##                      trailing slash. This value may be
137         ##                      percent-encoded when originating from
138         ##                      a URL.
140         ## <tt>QUERY_STRING</tt>:: The portion of the request URL that
141         ##                         follows the <tt>?</tt>, if any. May be
142         ##                         empty, but is always required!
144         ## <tt>SERVER_NAME</tt>:: When combined with <tt>SCRIPT_NAME</tt> and
145         ##                        <tt>PATH_INFO</tt>, these variables can be
146         ##                        used to complete the URL. Note, however,
147         ##                        that <tt>HTTP_HOST</tt>, if present,
148         ##                        should be used in preference to
149         ##                        <tt>SERVER_NAME</tt> for reconstructing
150         ##                        the request URL.
151         ##                        <tt>SERVER_NAME</tt> can never be an empty
152         ##                        string, and so is always required.
154         ## <tt>SERVER_PORT</tt>:: An optional +Integer+ which is the port the
155         ##                        server is running on. Should be specified if
156         ##                        the server is running on a non-standard port.
158         ## <tt>SERVER_PROTOCOL</tt>:: A string representing the HTTP version used
159         ##                            for the request.
161         ## <tt>HTTP_</tt> Variables:: Variables corresponding to the
162         ##                            client-supplied HTTP request
163         ##                            headers (i.e., variables whose
164         ##                            names begin with <tt>HTTP_</tt>). The
165         ##                            presence or absence of these
166         ##                            variables should correspond with
167         ##                            the presence or absence of the
168         ##                            appropriate HTTP header in the
169         ##                            request. See
170         ##                            {RFC3875 section 4.1.18}[https://tools.ietf.org/html/rfc3875#section-4.1.18]
171         ##                            for specific behavior.
173         ## In addition to this, the Rack environment must include these
174         ## Rack-specific variables:
176         ## <tt>rack.url_scheme</tt>:: The scheme of the incoming request, must
177         ##                            be one of +http+, +https+, +ws+ or +wss+.
179         ## <tt>rack.input</tt>:: See below, the input stream.
181         ## <tt>rack.errors</tt>:: See below, the error stream.
183         ## <tt>rack.hijack?</tt>:: See below, if present and true, indicates
184         ##                         that the server supports partial hijacking.
186         ## <tt>rack.hijack</tt>:: See below, if present, an object responding
187         ##                        to +call+ that is used to perform a full
188         ##                        hijack.
190         ## <tt>rack.protocol</tt>:: An optional +Array+ of +String+, containing
191         ##                          the protocols advertised by the client in
192         ##                          the +upgrade+ header (HTTP/1) or the
193         ##                          +:protocol+ pseudo-header (HTTP/2).
194         if protocols = @env['rack.protocol']
195           unless protocols.is_a?(Array) && protocols.all?{|protocol| protocol.is_a?(String)}
196             raise LintError, "rack.protocol must be an Array of Strings"
197           end
198         end
200         ## Additional environment specifications have approved to
201         ## standardized middleware APIs. None of these are required to
202         ## be implemented by the server.
204         ## <tt>rack.session</tt>:: A hash-like interface for storing
205         ##                         request session data.
206         ##                         The store must implement:
207         if session = env[RACK_SESSION]
208           ##                         store(key, value)         (aliased as []=);
209           unless session.respond_to?(:store) && session.respond_to?(:[]=)
210             raise LintError, "session #{session.inspect} must respond to store and []="
211           end
213           ##                         fetch(key, default = nil) (aliased as []);
214           unless session.respond_to?(:fetch) && session.respond_to?(:[])
215             raise LintError, "session #{session.inspect} must respond to fetch and []"
216           end
218           ##                         delete(key);
219           unless session.respond_to?(:delete)
220             raise LintError, "session #{session.inspect} must respond to delete"
221           end
223           ##                         clear;
224           unless session.respond_to?(:clear)
225             raise LintError, "session #{session.inspect} must respond to clear"
226           end
228           ##                         to_hash (returning unfrozen Hash instance);
229           unless session.respond_to?(:to_hash) && session.to_hash.kind_of?(Hash) && !session.to_hash.frozen?
230             raise LintError, "session #{session.inspect} must respond to to_hash and return unfrozen Hash instance"
231           end
232         end
234         ## <tt>rack.logger</tt>:: A common object interface for logging messages.
235         ##                        The object must implement:
236         if logger = env[RACK_LOGGER]
237           ##                         info(message, &block)
238           unless logger.respond_to?(:info)
239             raise LintError, "logger #{logger.inspect} must respond to info"
240           end
242           ##                         debug(message, &block)
243           unless logger.respond_to?(:debug)
244             raise LintError, "logger #{logger.inspect} must respond to debug"
245           end
247           ##                         warn(message, &block)
248           unless logger.respond_to?(:warn)
249             raise LintError, "logger #{logger.inspect} must respond to warn"
250           end
252           ##                         error(message, &block)
253           unless logger.respond_to?(:error)
254             raise LintError, "logger #{logger.inspect} must respond to error"
255           end
257           ##                         fatal(message, &block)
258           unless logger.respond_to?(:fatal)
259             raise LintError, "logger #{logger.inspect} must respond to fatal"
260           end
261         end
263         ## <tt>rack.multipart.buffer_size</tt>:: An Integer hint to the multipart parser as to what chunk size to use for reads and writes.
264         if bufsize = env[RACK_MULTIPART_BUFFER_SIZE]
265           unless bufsize.is_a?(Integer) && bufsize > 0
266             raise LintError, "rack.multipart.buffer_size must be an Integer > 0 if specified"
267           end
268         end
270         ## <tt>rack.multipart.tempfile_factory</tt>:: An object responding to #call with two arguments, the filename and content_type given for the multipart form field, and returning an IO-like object that responds to #<< and optionally #rewind. This factory will be used to instantiate the tempfile for each multipart form file upload field, rather than the default class of Tempfile.
271         if tempfile_factory = env[RACK_MULTIPART_TEMPFILE_FACTORY]
272           raise LintError, "rack.multipart.tempfile_factory must respond to #call" unless tempfile_factory.respond_to?(:call)
273           env[RACK_MULTIPART_TEMPFILE_FACTORY] = lambda do |filename, content_type|
274             io = tempfile_factory.call(filename, content_type)
275             raise LintError, "rack.multipart.tempfile_factory return value must respond to #<<" unless io.respond_to?(:<<)
276             io
277           end
278         end
280         ## The server or the application can store their own data in the
281         ## environment, too.  The keys must contain at least one dot,
282         ## and should be prefixed uniquely.  The prefix <tt>rack.</tt>
283         ## is reserved for use with the Rack core distribution and other
284         ## accepted specifications and must not be used otherwise.
285         ##
286         %w[REQUEST_METHOD SERVER_NAME QUERY_STRING SERVER_PROTOCOL rack.errors].each do |header|
287           raise LintError, "env missing required key #{header}" unless env.include? header
288         end
290         ## The <tt>SERVER_PORT</tt> must be an Integer if set.
291         server_port = env["SERVER_PORT"]
292         unless server_port.nil? || (Integer(server_port) rescue false)
293           raise LintError, "env[SERVER_PORT] is not an Integer"
294         end
296         ## The <tt>SERVER_NAME</tt> must be a valid authority as defined by RFC7540.
297         unless (URI.parse("http://#{env[SERVER_NAME]}/") rescue false)
298           raise LintError, "#{env[SERVER_NAME]} must be a valid authority"
299         end
301         ## The <tt>HTTP_HOST</tt> must be a valid authority as defined by RFC7540.
302         unless (URI.parse("http://#{env[HTTP_HOST]}/") rescue false)
303           raise LintError, "#{env[HTTP_HOST]} must be a valid authority"
304         end
306         ## The <tt>SERVER_PROTOCOL</tt> must match the regexp <tt>HTTP/\d(\.\d)?</tt>.
307         server_protocol = env['SERVER_PROTOCOL']
308         unless %r{HTTP/\d(\.\d)?}.match?(server_protocol)
309           raise LintError, "env[SERVER_PROTOCOL] does not match HTTP/\\d(\\.\\d)?"
310         end
312         ## The environment must not contain the keys
313         ## <tt>HTTP_CONTENT_TYPE</tt> or <tt>HTTP_CONTENT_LENGTH</tt>
314         ## (use the versions without <tt>HTTP_</tt>).
315         %w[HTTP_CONTENT_TYPE HTTP_CONTENT_LENGTH].each { |header|
316           if env.include? header
317             raise LintError, "env contains #{header}, must use #{header[5..-1]}"
318           end
319         }
321         ## The CGI keys (named without a period) must have String values.
322         ## If the string values for CGI keys contain non-ASCII characters,
323         ## they should use ASCII-8BIT encoding.
324         env.each { |key, value|
325           next  if key.include? "."   # Skip extensions
326           unless value.kind_of? String
327             raise LintError, "env variable #{key} has non-string value #{value.inspect}"
328           end
329           next if value.encoding == Encoding::ASCII_8BIT
330           unless value.b !~ /[\x80-\xff]/n
331             raise LintError, "env variable #{key} has value containing non-ASCII characters and has non-ASCII-8BIT encoding #{value.inspect} encoding: #{value.encoding}"
332           end
333         }
335         ## There are the following restrictions:
337         ## * <tt>rack.url_scheme</tt> must either be +http+ or +https+.
338         unless ALLOWED_SCHEMES.include?(env[RACK_URL_SCHEME])
339           raise LintError, "rack.url_scheme unknown: #{env[RACK_URL_SCHEME].inspect}"
340         end
342         ## * There may be a valid input stream in <tt>rack.input</tt>.
343         if rack_input = env[RACK_INPUT]
344           check_input_stream(rack_input)
345           @env[RACK_INPUT] = InputWrapper.new(rack_input)
346         end
348         ## * There must be a valid error stream in <tt>rack.errors</tt>.
349         rack_errors = env[RACK_ERRORS]
350         check_error_stream(rack_errors)
351         @env[RACK_ERRORS] = ErrorWrapper.new(rack_errors)
353         ## * There may be a valid hijack callback in <tt>rack.hijack</tt>
354         check_hijack env
355         ## * There may be a valid early hints callback in <tt>rack.early_hints</tt>
356         check_early_hints env
358         ## * The <tt>REQUEST_METHOD</tt> must be a valid token.
359         unless env[REQUEST_METHOD] =~ /\A[0-9A-Za-z!\#$%&'*+.^_`|~-]+\z/
360           raise LintError, "REQUEST_METHOD unknown: #{env[REQUEST_METHOD].dump}"
361         end
363         ## * The <tt>SCRIPT_NAME</tt>, if non-empty, must start with <tt>/</tt>
364         if env.include?(SCRIPT_NAME) && env[SCRIPT_NAME] != "" && env[SCRIPT_NAME] !~ /\A\//
365           raise LintError, "SCRIPT_NAME must start with /"
366         end
368         ## * The <tt>PATH_INFO</tt>, if provided, must be a valid request target or an empty string.
369         if env.include?(PATH_INFO)
370           case env[PATH_INFO]
371           when REQUEST_PATH_ASTERISK_FORM
372             ##   * Only <tt>OPTIONS</tt> requests may have <tt>PATH_INFO</tt> set to <tt>*</tt> (asterisk-form).
373             unless env[REQUEST_METHOD] == OPTIONS
374               raise LintError, "Only OPTIONS requests may have PATH_INFO set to '*' (asterisk-form)"
375             end
376           when REQUEST_PATH_AUTHORITY_FORM
377             ##   * Only <tt>CONNECT</tt> requests may have <tt>PATH_INFO</tt> set to an authority (authority-form). Note that in HTTP/2+, the authority-form is not a valid request target.
378             unless env[REQUEST_METHOD] == CONNECT
379               raise LintError, "Only CONNECT requests may have PATH_INFO set to an authority (authority-form)"
380             end
381           when REQUEST_PATH_ABSOLUTE_FORM
382             ##   * <tt>CONNECT</tt> and <tt>OPTIONS</tt> requests must not have <tt>PATH_INFO</tt> set to a URI (absolute-form).
383             if env[REQUEST_METHOD] == CONNECT || env[REQUEST_METHOD] == OPTIONS
384               raise LintError, "CONNECT and OPTIONS requests must not have PATH_INFO set to a URI (absolute-form)"
385             end
386           when REQUEST_PATH_ORIGIN_FORM
387             ##   * Otherwise, <tt>PATH_INFO</tt> must start with a <tt>/</tt> and must not include a fragment part starting with '#' (origin-form).
388           when ""
389             # Empty string is okay.
390           else
391             raise LintError, "PATH_INFO must start with a '/' and must not include a fragment part starting with '#' (origin-form)"
392           end
393         end
395         ## * The <tt>CONTENT_LENGTH</tt>, if given, must consist of digits only.
396         if env.include?("CONTENT_LENGTH") && env["CONTENT_LENGTH"] !~ /\A\d+\z/
397           raise LintError, "Invalid CONTENT_LENGTH: #{env["CONTENT_LENGTH"]}"
398         end
400         ## * One of <tt>SCRIPT_NAME</tt> or <tt>PATH_INFO</tt> must be
401         ##   set. <tt>PATH_INFO</tt> should be <tt>/</tt> if
402         ##   <tt>SCRIPT_NAME</tt> is empty.
403         unless env[SCRIPT_NAME] || env[PATH_INFO]
404           raise LintError, "One of SCRIPT_NAME or PATH_INFO must be set (make PATH_INFO '/' if SCRIPT_NAME is empty)"
405         end
406         ##   <tt>SCRIPT_NAME</tt> never should be <tt>/</tt>, but instead be empty.
407         unless env[SCRIPT_NAME] != "/"
408           raise LintError, "SCRIPT_NAME cannot be '/', make it '' and PATH_INFO '/'"
409         end
411         ## <tt>rack.response_finished</tt>:: An array of callables run by the server after the response has been
412         ##                                   processed. This would typically be invoked after sending the response to
413         ##                                   the client, but it could also be invoked if an error occurs while
414         ##                                   generating the response or sending the response; in that case, the error
415         ##                                   argument will be a subclass of +Exception+.
416         ##                                   The callables are invoked with +env, status, headers, error+ arguments and
417         ##                                   should not raise any exceptions. They should be invoked in reverse order
418         ##                                   of registration.
419         if callables = env[RACK_RESPONSE_FINISHED]
420           raise LintError, "rack.response_finished must be an array of callable objects" unless callables.is_a?(Array)
422           callables.each do |callable|
423             raise LintError, "rack.response_finished values must respond to call(env, status, headers, error)" unless callable.respond_to?(:call)
424           end
425         end
426       end
428       ##
429       ## === The Input Stream
430       ##
431       ## The input stream is an IO-like object which contains the raw HTTP
432       ## POST data.
433       def check_input_stream(input)
434         ## When applicable, its external encoding must be "ASCII-8BIT" and it
435         ## must be opened in binary mode.
436         if input.respond_to?(:external_encoding) && input.external_encoding != Encoding::ASCII_8BIT
437           raise LintError, "rack.input #{input} does not have ASCII-8BIT as its external encoding"
438         end
439         if input.respond_to?(:binmode?) && !input.binmode?
440           raise LintError, "rack.input #{input} is not opened in binary mode"
441         end
443         ## The input stream must respond to +gets+, +each+, and +read+.
444         [:gets, :each, :read].each { |method|
445           unless input.respond_to? method
446             raise LintError, "rack.input #{input} does not respond to ##{method}"
447           end
448         }
449       end
451       class InputWrapper
452         def initialize(input)
453           @input = input
454         end
456         ## * +gets+ must be called without arguments and return a string,
457         ##   or +nil+ on EOF.
458         def gets(*args)
459           raise LintError, "rack.input#gets called with arguments" unless args.size == 0
460           v = @input.gets
461           unless v.nil? or v.kind_of? String
462             raise LintError, "rack.input#gets didn't return a String"
463           end
464           v
465         end
467         ## * +read+ behaves like <tt>IO#read</tt>.
468         ##   Its signature is <tt>read([length, [buffer]])</tt>.
469         ##
470         ##   If given, +length+ must be a non-negative Integer (>= 0) or +nil+,
471         ##   and +buffer+ must be a String and may not be nil.
472         ##
473         ##   If +length+ is given and not nil, then this method reads at most
474         ##   +length+ bytes from the input stream.
475         ##
476         ##   If +length+ is not given or nil, then this method reads
477         ##   all data until EOF.
478         ##
479         ##   When EOF is reached, this method returns nil if +length+ is given
480         ##   and not nil, or "" if +length+ is not given or is nil.
481         ##
482         ##   If +buffer+ is given, then the read data will be placed
483         ##   into +buffer+ instead of a newly created String object.
484         def read(*args)
485           unless args.size <= 2
486             raise LintError, "rack.input#read called with too many arguments"
487           end
488           if args.size >= 1
489             unless args.first.kind_of?(Integer) || args.first.nil?
490               raise LintError, "rack.input#read called with non-integer and non-nil length"
491             end
492             unless args.first.nil? || args.first >= 0
493               raise LintError, "rack.input#read called with a negative length"
494             end
495           end
496           if args.size >= 2
497             unless args[1].kind_of?(String)
498               raise LintError, "rack.input#read called with non-String buffer"
499             end
500           end
502           v = @input.read(*args)
504           unless v.nil? or v.kind_of? String
505             raise LintError, "rack.input#read didn't return nil or a String"
506           end
507           if args[0].nil?
508             unless !v.nil?
509               raise LintError, "rack.input#read(nil) returned nil on EOF"
510             end
511           end
513           v
514         end
516         ## * +each+ must be called without arguments and only yield Strings.
517         def each(*args)
518           raise LintError, "rack.input#each called with arguments" unless args.size == 0
519           @input.each { |line|
520             unless line.kind_of? String
521               raise LintError, "rack.input#each didn't yield a String"
522             end
523             yield line
524           }
525         end
527         ## * +close+ can be called on the input stream to indicate that
528         ##   any remaining input is not needed.
529         def close(*args)
530           @input.close(*args)
531         end
532       end
534       ##
535       ## === The Error Stream
536       ##
537       def check_error_stream(error)
538         ## The error stream must respond to +puts+, +write+ and +flush+.
539         [:puts, :write, :flush].each { |method|
540           unless error.respond_to? method
541             raise LintError, "rack.error #{error} does not respond to ##{method}"
542           end
543         }
544       end
546       class ErrorWrapper
547         def initialize(error)
548           @error = error
549         end
551         ## * +puts+ must be called with a single argument that responds to +to_s+.
552         def puts(str)
553           @error.puts str
554         end
556         ## * +write+ must be called with a single argument that is a String.
557         def write(str)
558           raise LintError, "rack.errors#write not called with a String" unless str.kind_of? String
559           @error.write str
560         end
562         ## * +flush+ must be called without arguments and must be called
563         ##   in order to make the error appear for sure.
564         def flush
565           @error.flush
566         end
568         ## * +close+ must never be called on the error stream.
569         def close(*args)
570           raise LintError, "rack.errors#close must not be called"
571         end
572       end
574       ##
575       ## === Hijacking
576       ##
577       ## The hijacking interfaces provides a means for an application to take
578       ## control of the HTTP connection. There are two distinct hijack
579       ## interfaces: full hijacking where the application takes over the raw
580       ## connection, and partial hijacking where the application takes over
581       ## just the response body stream. In both cases, the application is
582       ## responsible for closing the hijacked stream.
583       ##
584       ## Full hijacking only works with HTTP/1. Partial hijacking is functionally
585       ## equivalent to streaming bodies, and is still optionally supported for
586       ## backwards compatibility with older Rack versions.
587       ##
588       ## ==== Full Hijack
589       ##
590       ## Full hijack is used to completely take over an HTTP/1 connection. It
591       ## occurs before any headers are written and causes the request to
592       ## ignores any response generated by the application.
593       ##
594       ## It is intended to be used when applications need access to raw HTTP/1
595       ## connection.
596       ##
597       def check_hijack(env)
598         ## If +rack.hijack+ is present in +env+, it must respond to +call+
599         if original_hijack = env[RACK_HIJACK]
600           raise LintError, "rack.hijack must respond to call" unless original_hijack.respond_to?(:call)
602           env[RACK_HIJACK] = proc do
603             io = original_hijack.call
605             ## and return an +IO+ instance which can be used to read and write
606             ## to the underlying connection using HTTP/1 semantics and
607             ## formatting.
608             raise LintError, "rack.hijack must return an IO instance" unless io.is_a?(IO)
610             io
611           end
612         end
613       end
615       ##
616       ## ==== Partial Hijack
617       ##
618       ## Partial hijack is used for bi-directional streaming of the request and
619       ## response body. It occurs after the status and headers are written by
620       ## the server and causes the server to ignore the Body of the response.
621       ##
622       ## It is intended to be used when applications need bi-directional
623       ## streaming.
624       ##
625       def check_hijack_response(headers, env)
626         ## If +rack.hijack?+ is present in +env+ and truthy,
627         if env[RACK_IS_HIJACK]
628           ## an application may set the special response header +rack.hijack+
629           if original_hijack = headers[RACK_HIJACK]
630             ## to an object that responds to +call+,
631             unless original_hijack.respond_to?(:call)
632               raise LintError, 'rack.hijack header must respond to #call'
633             end
634             ## accepting a +stream+ argument.
635             return proc do |io|
636               original_hijack.call StreamWrapper.new(io)
637             end
638           end
639           ##
640           ## After the response status and headers have been sent, this hijack
641           ## callback will be invoked with a +stream+ argument which follows the
642           ## same interface as outlined in "Streaming Body". Servers must
643           ## ignore the +body+ part of the response tuple when the
644           ## +rack.hijack+ response header is present. Using an empty +Array+
645           ## instance is recommended.
646         else
647           ##
648           ## The special response header +rack.hijack+ must only be set
649           ## if the request +env+ has a truthy +rack.hijack?+.
650           if headers.key?(RACK_HIJACK)
651             raise LintError, 'rack.hijack header must not be present if server does not support hijacking'
652           end
653         end
655         nil
656       end
658       ##
659       ## === Early Hints
660       ##
661       ## The application or any middleware may call the <tt>rack.early_hints</tt>
662       ## with an object which would be valid as the headers of a Rack response.
663       def check_early_hints(env)
664         if env[RACK_EARLY_HINTS]
665           ##
666           ## If <tt>rack.early_hints</tt> is present, it must respond to #call.
667           unless env[RACK_EARLY_HINTS].respond_to?(:call)
668             raise LintError, "rack.early_hints must respond to call"
669           end
671           original_callback = env[RACK_EARLY_HINTS]
672           env[RACK_EARLY_HINTS] = lambda do |headers|
673             ## If <tt>rack.early_hints</tt> is called, it must be called with
674             ## valid Rack response headers.
675             check_headers(headers)
676             original_callback.call(headers)
677           end
678         end
679       end
681       ##
682       ## == The Response
683       ##
684       ## === The Status
685       ##
686       def check_status(status)
687         ## This is an HTTP status. It must be an Integer greater than or equal to
688         ## 100.
689         unless status.is_a?(Integer) && status >= 100
690           raise LintError, "Status must be an Integer >=100"
691         end
692       end
694       ##
695       ## === The Headers
696       ##
697       def check_headers(headers)
698         ## The headers must be a unfrozen Hash.
699         unless headers.kind_of?(Hash)
700           raise LintError, "headers object should be a hash, but isn't (got #{headers.class} as headers)"
701         end
703         if headers.frozen?
704           raise LintError, "headers object should not be frozen, but is"
705         end
707         headers.each do |key, value|
708           ## The header keys must be Strings.
709           unless key.kind_of? String
710             raise LintError, "header key must be a string, was #{key.class}"
711           end
713           ## Special headers starting "rack." are for communicating with the
714           ## server, and must not be sent back to the client.
715           next if key.start_with?("rack.")
717           ## The header must not contain a +Status+ key.
718           raise LintError, "header must not contain status" if key == "status"
719           ## Header keys must conform to RFC7230 token specification, i.e. cannot
720           ## contain non-printable ASCII, DQUOTE or "(),/:;<=>?@[\]{}".
721           raise LintError, "invalid header name: #{key}" if key =~ /[\(\),\/:;<=>\?@\[\\\]{}[:cntrl:]]/
722           ## Header keys must not contain uppercase ASCII characters (A-Z).
723           raise LintError, "uppercase character in header name: #{key}" if key =~ /[A-Z]/
725           ## Header values must be either a String instance,
726           if value.kind_of?(String)
727             check_header_value(key, value)
728           elsif value.kind_of?(Array)
729             ## or an Array of String instances,
730             value.each{|value| check_header_value(key, value)}
731           else
732             raise LintError, "a header value must be a String or Array of Strings, but the value of '#{key}' is a #{value.class}"
733           end
734         end
735       end
737       def check_header_value(key, value)
738         ## such that each String instance must not contain characters below 037.
739         if value =~ /[\000-\037]/
740           raise LintError, "invalid header value #{key}: #{value.inspect}"
741         end
742       end
744       ##
745       ## ==== The +content-type+ Header
746       ##
747       def check_content_type_header(status, headers)
748         headers.each { |key, value|
749           ## There must not be a <tt>content-type</tt> header key when the +Status+ is 1xx,
750           ## 204, or 304.
751           if key == "content-type"
752             if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key? status.to_i
753               raise LintError, "content-type header found in #{status} response, not allowed"
754             end
755             return
756           end
757         }
758       end
760       ##
761       ## ==== The +content-length+ Header
762       ##
763       def check_content_length_header(status, headers)
764         headers.each { |key, value|
765           if key == 'content-length'
766             ## There must not be a <tt>content-length</tt> header key when the
767             ## +Status+ is 1xx, 204, or 304.
768             if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key? status.to_i
769               raise LintError, "content-length header found in #{status} response, not allowed"
770             end
771             @content_length = value
772           end
773         }
774       end
776       def verify_content_length(size)
777         if @head_request
778           unless size == 0
779             raise LintError, "Response body was given for HEAD request, but should be empty"
780           end
781         elsif @content_length
782           unless @content_length == size.to_s
783             raise LintError, "content-length header was #{@content_length}, but should be #{size}"
784           end
785         end
786       end
788       ## 
789       ## ==== The +rack.protocol+ Header
790       ##
791       def check_rack_protocol_header(status, headers)
792         ## If the +rack.protocol+ header is present, it must be a +String+, and
793         ## must be one of the values from the +rack.protocol+ array from the
794         ## environment.
795         protocol = headers['rack.protocol']
797         if protocol
798           request_protocols = @env['rack.protocol']
800           if request_protocols.nil?
801             raise LintError, "rack.protocol header is #{protocol.inspect}, but rack.protocol was not set in request!"
802           elsif !request_protocols.include?(protocol)
803             raise LintError, "rack.protocol header is #{protocol.inspect}, but should be one of #{request_protocols.inspect} from the request!"
804           end
805         end
806       end
807       ##
808       ## Setting this value informs the server that it should perform a
809       ## connection upgrade. In HTTP/1, this is done using the +upgrade+
810       ## header. In HTTP/2, this is done by accepting the request.
811       ##
812       ## === The Body
813       ##
814       ## The Body is typically an +Array+ of +String+ instances, an enumerable
815       ## that yields +String+ instances, a +Proc+ instance, or a File-like
816       ## object.
817       ##
818       ## The Body must respond to +each+ or +call+. It may optionally respond
819       ## to +to_path+ or +to_ary+. A Body that responds to +each+ is considered
820       ## to be an Enumerable Body. A Body that responds to +call+ is considered
821       ## to be a Streaming Body.
822       ##
823       ## A Body that responds to both +each+ and +call+ must be treated as an
824       ## Enumerable Body, not a Streaming Body. If it responds to +each+, you
825       ## must call +each+ and not +call+. If the Body doesn't respond to
826       ## +each+, then you can assume it responds to +call+.
827       ##
828       ## The Body must either be consumed or returned. The Body is consumed by
829       ## optionally calling either +each+ or +call+.
830       ## Then, if the Body responds to +close+, it must be called to release
831       ## any resources associated with the generation of the body.
832       ## In other words, +close+ must always be called at least once; typically
833       ## after the web server has sent the response to the client, but also in
834       ## cases where the Rack application makes internal/virtual requests and
835       ## discards the response.
836       ##
837       def close
838         ##
839         ## After calling +close+, the Body is considered closed and should not
840         ## be consumed again.
841         @closed = true
843         ## If the original Body is replaced by a new Body, the new Body must
844         ## also consume the original Body by calling +close+ if possible.
845         @body.close if @body.respond_to?(:close)
847         index = @lint.index(self)
848         unless @env['rack.lint'][0..index].all? {|lint| lint.instance_variable_get(:@closed)}
849           raise LintError, "Body has not been closed"
850         end
851       end
853       def verify_to_path
854         ##
855         ## If the Body responds to +to_path+, it must return a +String+
856         ## path for the local file system whose contents are identical
857         ## to that produced by calling +each+; this may be used by the
858         ## server as an alternative, possibly more efficient way to
859         ## transport the response. The +to_path+ method does not consume
860         ## the body.
861         if @body.respond_to?(:to_path)
862           unless ::File.exist? @body.to_path
863             raise LintError, "The file identified by body.to_path does not exist"
864           end
865         end
866       end
868       ##
869       ## ==== Enumerable Body
870       ##
871       def each
872         ## The Enumerable Body must respond to +each+.
873         raise LintError, "Enumerable Body must respond to each" unless @body.respond_to?(:each)
875         ## It must only be called once.
876         raise LintError, "Response body must only be invoked once (#{@invoked})" unless @invoked.nil?
878         ## It must not be called after being closed,
879         raise LintError, "Response body is already closed" if @closed
881         @invoked = :each
883         @body.each do |chunk|
884           ## and must only yield String values.
885           unless chunk.kind_of? String
886             raise LintError, "Body yielded non-string value #{chunk.inspect}"
887           end
889           ##
890           ## Middleware must not call +each+ directly on the Body.
891           ## Instead, middleware can return a new Body that calls +each+ on the
892           ## original Body, yielding at least once per iteration.
893           if @lint[0] == self
894             @env['rack.lint.body_iteration'] += 1
895           else
896             if (@env['rack.lint.body_iteration'] -= 1) > 0
897               raise LintError, "New body must yield at least once per iteration of old body"
898             end
899           end
901           @size += chunk.bytesize
902           yield chunk
903         end
905         verify_content_length(@size)
907         verify_to_path
908       end
910       BODY_METHODS = {to_ary: true, each: true, call: true, to_path: true}
912       def to_path
913         @body.to_path
914       end
916       def respond_to?(name, *)
917         if BODY_METHODS.key?(name)
918           @body.respond_to?(name)
919         else
920           super
921         end
922       end
924       ##
925       ## If the Body responds to +to_ary+, it must return an +Array+ whose
926       ## contents are identical to that produced by calling +each+.
927       ## Middleware may call +to_ary+ directly on the Body and return a new
928       ## Body in its place. In other words, middleware can only process the
929       ## Body directly if it responds to +to_ary+. If the Body responds to both
930       ## +to_ary+ and +close+, its implementation of +to_ary+ must call
931       ## +close+.
932       def to_ary
933         @body.to_ary.tap do |content|
934           unless content == @body.enum_for.to_a
935             raise LintError, "#to_ary not identical to contents produced by calling #each"
936           end
937         end
938       ensure
939         close
940       end
942       ##
943       ## ==== Streaming Body
944       ##
945       def call(stream)
946         ## The Streaming Body must respond to +call+.
947         raise LintError, "Streaming Body must respond to call" unless @body.respond_to?(:call)
949         ## It must only be called once.
950         raise LintError, "Response body must only be invoked once (#{@invoked})" unless @invoked.nil?
952         ## It must not be called after being closed.
953         raise LintError, "Response body is already closed" if @closed
955         @invoked = :call
957         ## It takes a +stream+ argument.
958         ##
959         ## The +stream+ argument must implement:
960         ## <tt>read, write, <<, flush, close, close_read, close_write, closed?</tt>
961         ##
962         @body.call(StreamWrapper.new(stream))
963       end
965       class StreamWrapper
966         extend Forwardable
968         ## The semantics of these IO methods must be a best effort match to
969         ## those of a normal Ruby IO or Socket object, using standard arguments
970         ## and raising standard exceptions. Servers are encouraged to simply
971         ## pass on real IO objects, although it is recognized that this approach
972         ## is not directly compatible with HTTP/2.
973         REQUIRED_METHODS = [
974           :read, :write, :<<, :flush, :close,
975           :close_read, :close_write, :closed?
976         ]
978         def_delegators :@stream, *REQUIRED_METHODS
980         def initialize(stream)
981           @stream = stream
983           REQUIRED_METHODS.each do |method_name|
984             raise LintError, "Stream must respond to #{method_name}" unless stream.respond_to?(method_name)
985           end
986         end
987       end
989       # :startdoc:
990     end
991   end
995 ## == Thanks
996 ## Some parts of this specification are adopted from {PEP 333 – Python Web Server Gateway Interface v1.0}[https://peps.python.org/pep-0333/]
997 ## I'd like to thank everyone involved in that effort.