technomancy-rack 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. data/KNOWN-ISSUES +18 -0
  2. data/README +242 -0
  3. data/bin/rackup +183 -0
  4. data/lib/rack.rb +92 -0
  5. data/lib/rack/adapter/camping.rb +22 -0
  6. data/lib/rack/auth/abstract/handler.rb +28 -0
  7. data/lib/rack/auth/abstract/request.rb +37 -0
  8. data/lib/rack/auth/basic.rb +58 -0
  9. data/lib/rack/auth/digest/md5.rb +124 -0
  10. data/lib/rack/auth/digest/nonce.rb +51 -0
  11. data/lib/rack/auth/digest/params.rb +55 -0
  12. data/lib/rack/auth/digest/request.rb +40 -0
  13. data/lib/rack/auth/openid.rb +116 -0
  14. data/lib/rack/builder.rb +56 -0
  15. data/lib/rack/cascade.rb +36 -0
  16. data/lib/rack/commonlogger.rb +56 -0
  17. data/lib/rack/file.rb +112 -0
  18. data/lib/rack/handler/cgi.rb +57 -0
  19. data/lib/rack/handler/fastcgi.rb +83 -0
  20. data/lib/rack/handler/lsws.rb +52 -0
  21. data/lib/rack/handler/mongrel.rb +97 -0
  22. data/lib/rack/handler/scgi.rb +57 -0
  23. data/lib/rack/handler/webrick.rb +57 -0
  24. data/lib/rack/lint.rb +394 -0
  25. data/lib/rack/lobster.rb +65 -0
  26. data/lib/rack/mock.rb +172 -0
  27. data/lib/rack/recursive.rb +57 -0
  28. data/lib/rack/reloader.rb +64 -0
  29. data/lib/rack/request.rb +197 -0
  30. data/lib/rack/response.rb +166 -0
  31. data/lib/rack/session/abstract/id.rb +126 -0
  32. data/lib/rack/session/cookie.rb +71 -0
  33. data/lib/rack/session/memcache.rb +83 -0
  34. data/lib/rack/session/pool.rb +67 -0
  35. data/lib/rack/showexceptions.rb +344 -0
  36. data/lib/rack/showstatus.rb +103 -0
  37. data/lib/rack/static.rb +38 -0
  38. data/lib/rack/urlmap.rb +48 -0
  39. data/lib/rack/utils.rb +256 -0
  40. data/test/spec_rack_auth_basic.rb +69 -0
  41. data/test/spec_rack_auth_digest.rb +169 -0
  42. data/test/spec_rack_builder.rb +50 -0
  43. data/test/spec_rack_camping.rb +47 -0
  44. data/test/spec_rack_cascade.rb +50 -0
  45. data/test/spec_rack_cgi.rb +91 -0
  46. data/test/spec_rack_commonlogger.rb +32 -0
  47. data/test/spec_rack_fastcgi.rb +91 -0
  48. data/test/spec_rack_file.rb +40 -0
  49. data/test/spec_rack_lint.rb +317 -0
  50. data/test/spec_rack_lobster.rb +45 -0
  51. data/test/spec_rack_mock.rb +152 -0
  52. data/test/spec_rack_mongrel.rb +165 -0
  53. data/test/spec_rack_recursive.rb +77 -0
  54. data/test/spec_rack_request.rb +384 -0
  55. data/test/spec_rack_response.rb +167 -0
  56. data/test/spec_rack_session_cookie.rb +49 -0
  57. data/test/spec_rack_session_memcache.rb +100 -0
  58. data/test/spec_rack_session_pool.rb +84 -0
  59. data/test/spec_rack_showexceptions.rb +21 -0
  60. data/test/spec_rack_showstatus.rb +71 -0
  61. data/test/spec_rack_static.rb +37 -0
  62. data/test/spec_rack_urlmap.rb +175 -0
  63. data/test/spec_rack_utils.rb +69 -0
  64. data/test/spec_rack_webrick.rb +106 -0
  65. data/test/testrequest.rb +43 -0
  66. metadata +167 -0
@@ -0,0 +1,97 @@
1
+ require 'mongrel'
2
+ require 'stringio'
3
+
4
+ class Mongrel::HttpResponse
5
+ NO_CLOSE_STATUS_FORMAT = "HTTP/1.1 %d %s\r\n".freeze
6
+
7
+ # Sends the status to the client without closing the connection.
8
+ #
9
+ # ==== Parameters
10
+ # content_length<Fixnum>:: The length of the content. Defaults to body length.
11
+ def send_status_no_connection_close(content_length=@body.length)
12
+ unless @status_sent
13
+ write(NO_CLOSE_STATUS_FORMAT % [@status, Mongrel::HTTP_STATUS_CODES[@status]])
14
+ @status_sent = true
15
+ end
16
+ end
17
+ end
18
+
19
+ module Rack
20
+ module Handler
21
+ class Mongrel < ::Mongrel::HttpHandler
22
+ def self.run(app, options={})
23
+ server = ::Mongrel::HttpServer.new(options[:Host] || '0.0.0.0',
24
+ options[:Port] || 8080)
25
+ # Acts like Rack::URLMap, utilizing Mongrel's own path finding methods.
26
+ # Use is similar to #run, replacing the app argument with a hash of
27
+ # { path=>app, ... } or an instance of Rack::URLMap.
28
+ if options[:map]
29
+ if app.is_a? Hash
30
+ app.each do |path, appl|
31
+ path = '/'+path unless path[0] == ?/
32
+ server.register(path, Rack::Handler::Mongrel.new(appl))
33
+ end
34
+ elsif app.is_a? URLMap
35
+ app.instance_variable_get(:@mapping).each do |(host, path, appl)|
36
+ next if !host.nil? && !options[:Host].nil? && options[:Host] != host
37
+ path = '/'+path unless path[0] == ?/
38
+ server.register(path, Rack::Handler::Mongrel.new(appl))
39
+ end
40
+ else
41
+ raise ArgumentError, "first argument should be a Hash or URLMap"
42
+ end
43
+ else
44
+ server.register('/', Rack::Handler::Mongrel.new(app))
45
+ end
46
+ yield server if block_given?
47
+ server.run.join
48
+ end
49
+
50
+ def initialize(app)
51
+ @app = app
52
+ end
53
+
54
+ def process(request, response)
55
+ env = {}.replace(request.params)
56
+ env.delete "HTTP_CONTENT_TYPE"
57
+ env.delete "HTTP_CONTENT_LENGTH"
58
+
59
+ env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
60
+
61
+ env.update({"rack.version" => [0,1],
62
+ "rack.input" => request.body || StringIO.new(""),
63
+ "rack.errors" => STDERR,
64
+
65
+ "rack.multithread" => true,
66
+ "rack.multiprocess" => false, # ???
67
+ "rack.run_once" => false,
68
+
69
+ "rack.url_scheme" => "http",
70
+ })
71
+ env["QUERY_STRING"] ||= ""
72
+ env.delete "PATH_INFO" if env["PATH_INFO"] == ""
73
+
74
+ status, headers, body = @app.call(env)
75
+
76
+ begin
77
+ response.status = status.to_i
78
+ headers.each { |k, vs|
79
+ vs.each { |v|
80
+ response.header[k] = v
81
+ }
82
+ }
83
+ if body.respond_to? :each
84
+ body.each { |part|
85
+ response.body << part
86
+ }
87
+ else
88
+ body.call(response)
89
+ end
90
+ response.finished
91
+ ensure
92
+ body.close if body.respond_to? :close
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,57 @@
1
+ require 'scgi'
2
+ require 'stringio'
3
+
4
+ module Rack
5
+ module Handler
6
+ class SCGI < ::SCGI::Processor
7
+ attr_accessor :app
8
+
9
+ def self.run(app, options=nil)
10
+ new(options.merge(:app=>app,
11
+ :host=>options[:Host],
12
+ :port=>options[:Port],
13
+ :socket=>options[:Socket])).listen
14
+ end
15
+
16
+ def initialize(settings = {})
17
+ @app = settings[:app]
18
+ @log = Object.new
19
+ def @log.info(*args); end
20
+ def @log.error(*args); end
21
+ super(settings)
22
+ end
23
+
24
+ def process_request(request, input_body, socket)
25
+ env = {}.replace(request)
26
+ env.delete "HTTP_CONTENT_TYPE"
27
+ env.delete "HTTP_CONTENT_LENGTH"
28
+ env["REQUEST_PATH"], env["QUERY_STRING"] = env["REQUEST_URI"].split('?', 2)
29
+ env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
30
+ env["PATH_INFO"] = env["REQUEST_PATH"]
31
+ env["QUERY_STRING"] ||= ""
32
+ env["SCRIPT_NAME"] = ""
33
+ env.update({"rack.version" => [0,1],
34
+ "rack.input" => StringIO.new(input_body),
35
+ "rack.errors" => STDERR,
36
+
37
+ "rack.multithread" => true,
38
+ "rack.multiprocess" => true,
39
+ "rack.run_once" => false,
40
+
41
+ "rack.url_scheme" => ["yes", "on", "1"].include?(env["HTTPS"]) ? "https" : "http"
42
+ })
43
+ status, headers, body = app.call(env)
44
+ begin
45
+ socket.write("Status: #{status}\r\n")
46
+ headers.each do |k, vs|
47
+ vs.each {|v| socket.write("#{k}: #{v}\r\n")}
48
+ end
49
+ socket.write("\r\n")
50
+ body.each {|s| socket.write(s)}
51
+ ensure
52
+ body.close if body.respond_to? :close
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,57 @@
1
+ require 'webrick'
2
+ require 'stringio'
3
+
4
+ module Rack
5
+ module Handler
6
+ class WEBrick < WEBrick::HTTPServlet::AbstractServlet
7
+ def self.run(app, options={})
8
+ server = ::WEBrick::HTTPServer.new(options)
9
+ server.mount "/", Rack::Handler::WEBrick, app
10
+ trap(:INT) { server.shutdown }
11
+ yield server if block_given?
12
+ server.start
13
+ end
14
+
15
+ def initialize(server, app)
16
+ super server
17
+ @app = app
18
+ end
19
+
20
+ def service(req, res)
21
+ env = req.meta_vars
22
+ env.delete_if { |k, v| v.nil? }
23
+
24
+ env.update({"rack.version" => [0,1],
25
+ "rack.input" => StringIO.new(req.body.to_s),
26
+ "rack.errors" => STDERR,
27
+
28
+ "rack.multithread" => true,
29
+ "rack.multiprocess" => false,
30
+ "rack.run_once" => false,
31
+
32
+ "rack.url_scheme" => ["yes", "on", "1"].include?(ENV["HTTPS"]) ? "https" : "http"
33
+ })
34
+
35
+ env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
36
+ env["QUERY_STRING"] ||= ""
37
+ env["REQUEST_PATH"] ||= "/"
38
+ env.delete "PATH_INFO" if env["PATH_INFO"] == ""
39
+
40
+ status, headers, body = @app.call(env)
41
+ begin
42
+ res.status = status.to_i
43
+ headers.each { |k, vs|
44
+ vs.each { |v|
45
+ res[k] = v
46
+ }
47
+ }
48
+ body.each { |part|
49
+ res.body << part
50
+ }
51
+ ensure
52
+ body.close if body.respond_to? :close
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,394 @@
1
+ module Rack
2
+ # Rack::Lint validates your application and the requests and
3
+ # responses according to the Rack spec.
4
+
5
+ class Lint
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ # :stopdoc:
11
+
12
+ class LintError < RuntimeError; end
13
+ module Assertion
14
+ def assert(message, &block)
15
+ unless block.call
16
+ raise LintError, message
17
+ end
18
+ end
19
+ end
20
+ include Assertion
21
+
22
+ ## This specification aims to formalize the Rack protocol. You
23
+ ## can (and should) use Rack::Lint to enforce it.
24
+ ##
25
+ ## When you develop middleware, be sure to add a Lint before and
26
+ ## after to catch all mistakes.
27
+
28
+ ## = Rack applications
29
+
30
+ ## A Rack application is an Ruby object (not a class) that
31
+ ## responds to +call+.
32
+ def call(env=nil)
33
+ ## It takes exactly one argument, the *environment*
34
+ assert("No env given") { env }
35
+ check_env env
36
+
37
+ env['rack.input'] = InputWrapper.new(env['rack.input'])
38
+ env['rack.errors'] = ErrorWrapper.new(env['rack.errors'])
39
+
40
+ ## and returns an Array of exactly three values:
41
+ status, headers, @body = @app.call(env)
42
+ ## The *status*,
43
+ check_status status
44
+ ## the *headers*,
45
+ check_headers headers
46
+ ## and the *body*.
47
+ check_content_type status, headers
48
+ [status, headers, self]
49
+ end
50
+
51
+ ## == The Environment
52
+ def check_env(env)
53
+ ## The environment must be an true instance of Hash (no
54
+ ## subclassing allowed) that includes CGI-like headers.
55
+ ## The application is free to modify the environment.
56
+ assert("env #{env.inspect} is not a Hash, but #{env.class}") {
57
+ env.instance_of? Hash
58
+ }
59
+
60
+ ##
61
+ ## The environment is required to include these variables
62
+ ## (adopted from PEP333), except when they'd be empty, but see
63
+ ## below.
64
+
65
+ ## <tt>REQUEST_METHOD</tt>:: The HTTP request method, such as
66
+ ## "GET" or "POST". This cannot ever
67
+ ## be an empty string, and so is
68
+ ## always required.
69
+
70
+ ## <tt>SCRIPT_NAME</tt>:: The initial portion of the request
71
+ ## URL's "path" that corresponds to the
72
+ ## application object, so that the
73
+ ## application knows its virtual
74
+ ## "location". This may be an empty
75
+ ## string, if the application corresponds
76
+ ## to the "root" of the server.
77
+
78
+ ## <tt>PATH_INFO</tt>:: The remainder of the request URL's
79
+ ## "path", designating the virtual
80
+ ## "location" of the request's target
81
+ ## within the application. This may be an
82
+ ## empty string, if the request URL targets
83
+ ## the application root and does not have a
84
+ ## trailing slash.
85
+
86
+ ## <tt>QUERY_STRING</tt>:: The portion of the request URL that
87
+ ## follows the <tt>?</tt>, if any. May be
88
+ ## empty, but is always required!
89
+
90
+ ## <tt>SERVER_NAME</tt>, <tt>SERVER_PORT</tt>:: When combined with <tt>SCRIPT_NAME</tt> and <tt>PATH_INFO</tt>, these variables can be used to complete the URL. Note, however, that <tt>HTTP_HOST</tt>, if present, should be used in preference to <tt>SERVER_NAME</tt> for reconstructing the request URL. <tt>SERVER_NAME</tt> and <tt>SERVER_PORT</tt> can never be empty strings, and so are always required.
91
+
92
+ ## <tt>HTTP_</tt> Variables:: Variables corresponding to the
93
+ ## client-supplied HTTP request
94
+ ## headers (i.e., variables whose
95
+ ## names begin with <tt>HTTP_</tt>). The
96
+ ## presence or absence of these
97
+ ## variables should correspond with
98
+ ## the presence or absence of the
99
+ ## appropriate HTTP header in the
100
+ ## request.
101
+
102
+ ## In addition to this, the Rack environment must include these
103
+ ## Rack-specific variables:
104
+
105
+ ## <tt>rack.version</tt>:: The Array [0,1], representing this version of Rack.
106
+ ## <tt>rack.url_scheme</tt>:: +http+ or +https+, depending on the request URL.
107
+ ## <tt>rack.input</tt>:: See below, the input stream.
108
+ ## <tt>rack.errors</tt>:: See below, the error stream.
109
+ ## <tt>rack.multithread</tt>:: true if the application object may be simultaneously invoked by another thread in the same process, false otherwise.
110
+ ## <tt>rack.multiprocess</tt>:: true if an equivalent application object may be simultaneously invoked by another process, false otherwise.
111
+ ## <tt>rack.run_once</tt>:: true if the server expects (but does not guarantee!) that the application will only be invoked this one time during the life of its containing process. Normally, this will only be true for a server based on CGI (or something similar).
112
+
113
+ ## The server or the application can store their own data in the
114
+ ## environment, too. The keys must contain at least one dot,
115
+ ## and should be prefixed uniquely. The prefix <tt>rack.</tt>
116
+ ## is reserved for use with the Rack core distribution and must
117
+ ## not be used otherwise.
118
+ ##
119
+
120
+ %w[REQUEST_METHOD SERVER_NAME SERVER_PORT
121
+ QUERY_STRING
122
+ rack.version rack.input rack.errors
123
+ rack.multithread rack.multiprocess rack.run_once].each { |header|
124
+ assert("env missing required key #{header}") { env.include? header }
125
+ }
126
+
127
+ ## The environment must not contain the keys
128
+ ## <tt>HTTP_CONTENT_TYPE</tt> or <tt>HTTP_CONTENT_LENGTH</tt>
129
+ ## (use the versions without <tt>HTTP_</tt>).
130
+ %w[HTTP_CONTENT_TYPE HTTP_CONTENT_LENGTH].each { |header|
131
+ assert("env contains #{header}, must use #{header[5,-1]}") {
132
+ not env.include? header
133
+ }
134
+ }
135
+
136
+ ## The CGI keys (named without a period) must have String values.
137
+ env.each { |key, value|
138
+ next if key.include? "." # Skip extensions
139
+ assert("env variable #{key} has non-string value #{value.inspect}") {
140
+ value.instance_of? String
141
+ }
142
+ }
143
+
144
+ ##
145
+ ## There are the following restrictions:
146
+
147
+ ## * <tt>rack.version</tt> must be an array of Integers.
148
+ assert("rack.version must be an Array, was #{env["rack.version"].class}") {
149
+ env["rack.version"].instance_of? Array
150
+ }
151
+ ## * <tt>rack.url_scheme</tt> must either be +http+ or +https+.
152
+ assert("rack.url_scheme unknown: #{env["rack.url_scheme"].inspect}") {
153
+ %w[http https].include? env["rack.url_scheme"]
154
+ }
155
+
156
+ ## * There must be a valid input stream in <tt>rack.input</tt>.
157
+ check_input env["rack.input"]
158
+ ## * There must be a valid error stream in <tt>rack.errors</tt>.
159
+ check_error env["rack.errors"]
160
+
161
+ ## * The <tt>REQUEST_METHOD</tt> must be one of +GET+, +POST+, +PUT+,
162
+ ## +DELETE+, +HEAD+, +OPTIONS+, +TRACE+.
163
+ assert("REQUEST_METHOD unknown: #{env["REQUEST_METHOD"]}") {
164
+ %w[GET POST PUT DELETE
165
+ HEAD OPTIONS TRACE].include?(env["REQUEST_METHOD"])
166
+ }
167
+
168
+ ## * The <tt>SCRIPT_NAME</tt>, if non-empty, must start with <tt>/</tt>
169
+ assert("SCRIPT_NAME must start with /") {
170
+ !env.include?("SCRIPT_NAME") ||
171
+ env["SCRIPT_NAME"] == "" ||
172
+ env["SCRIPT_NAME"] =~ /\A\//
173
+ }
174
+ ## * The <tt>PATH_INFO</tt>, if non-empty, must start with <tt>/</tt>
175
+ assert("PATH_INFO must start with /") {
176
+ !env.include?("PATH_INFO") ||
177
+ env["PATH_INFO"] == "" ||
178
+ env["PATH_INFO"] =~ /\A\//
179
+ }
180
+ ## * The <tt>CONTENT_LENGTH</tt>, if given, must consist of digits only.
181
+ assert("Invalid CONTENT_LENGTH: #{env["CONTENT_LENGTH"]}") {
182
+ !env.include?("CONTENT_LENGTH") || env["CONTENT_LENGTH"] =~ /\A\d+\z/
183
+ }
184
+
185
+ ## * One of <tt>SCRIPT_NAME</tt> or <tt>PATH_INFO</tt> must be
186
+ ## set. <tt>PATH_INFO</tt> should be <tt>/</tt> if
187
+ ## <tt>SCRIPT_NAME</tt> is empty.
188
+ assert("One of SCRIPT_NAME or PATH_INFO must be set (make PATH_INFO '/' if SCRIPT_NAME is empty)") {
189
+ env["SCRIPT_NAME"] || env["PATH_INFO"]
190
+ }
191
+ ## <tt>SCRIPT_NAME</tt> never should be <tt>/</tt>, but instead be empty.
192
+ assert("SCRIPT_NAME cannot be '/', make it '' and PATH_INFO '/'") {
193
+ env["SCRIPT_NAME"] != "/"
194
+ }
195
+ end
196
+
197
+ ## === The Input Stream
198
+ def check_input(input)
199
+ ## The input stream must respond to +gets+, +each+ and +read+.
200
+ [:gets, :each, :read].each { |method|
201
+ assert("rack.input #{input} does not respond to ##{method}") {
202
+ input.respond_to? method
203
+ }
204
+ }
205
+ end
206
+
207
+ class InputWrapper
208
+ include Assertion
209
+
210
+ def initialize(input)
211
+ @input = input
212
+ end
213
+
214
+ ## * +gets+ must be called without arguments and return a string,
215
+ ## or +nil+ on EOF.
216
+ def gets(*args)
217
+ assert("rack.input#gets called with arguments") { args.size == 0 }
218
+ v = @input.gets
219
+ assert("rack.input#gets didn't return a String") {
220
+ v.nil? or v.instance_of? String
221
+ }
222
+ v
223
+ end
224
+
225
+ ## * +read+ must be called without or with one integer argument
226
+ ## and return a string, or +nil+ on EOF.
227
+ def read(*args)
228
+ assert("rack.input#read called with too many arguments") {
229
+ args.size <= 1
230
+ }
231
+ if args.size == 1
232
+ assert("rack.input#read called with non-integer argument") {
233
+ args.first.kind_of? Integer
234
+ }
235
+ end
236
+ v = @input.read(*args)
237
+ assert("rack.input#read didn't return a String") {
238
+ v.nil? or v.instance_of? String
239
+ }
240
+ v
241
+ end
242
+
243
+ ## * +each+ must be called without arguments and only yield Strings.
244
+ def each(*args)
245
+ assert("rack.input#each called with arguments") { args.size == 0 }
246
+ @input.each { |line|
247
+ assert("rack.input#each didn't yield a String") {
248
+ line.instance_of? String
249
+ }
250
+ yield line
251
+ }
252
+ end
253
+
254
+ ## * +close+ must never be called on the input stream.
255
+ def close(*args)
256
+ assert("rack.input#close must not be called") { false }
257
+ end
258
+ end
259
+
260
+ ## === The Error Stream
261
+ def check_error(error)
262
+ ## The error stream must respond to +puts+, +write+ and +flush+.
263
+ [:puts, :write, :flush].each { |method|
264
+ assert("rack.error #{error} does not respond to ##{method}") {
265
+ error.respond_to? method
266
+ }
267
+ }
268
+ end
269
+
270
+ class ErrorWrapper
271
+ include Assertion
272
+
273
+ def initialize(error)
274
+ @error = error
275
+ end
276
+
277
+ ## * +puts+ must be called with a single argument that responds to +to_s+.
278
+ def puts(str)
279
+ @error.puts str
280
+ end
281
+
282
+ ## * +write+ must be called with a single argument that is a String.
283
+ def write(str)
284
+ assert("rack.errors#write not called with a String") { str.instance_of? String }
285
+ @error.write str
286
+ end
287
+
288
+ ## * +flush+ must be called without arguments and must be called
289
+ ## in order to make the error appear for sure.
290
+ def flush
291
+ @error.flush
292
+ end
293
+
294
+ ## * +close+ must never be called on the error stream.
295
+ def close(*args)
296
+ assert("rack.errors#close must not be called") { false }
297
+ end
298
+ end
299
+
300
+ ## == The Response
301
+
302
+ ## === The Status
303
+ def check_status(status)
304
+ ## The status, if parsed as integer (+to_i+), must be bigger than 100.
305
+ assert("Status must be >100 seen as integer") { status.to_i > 100 }
306
+ end
307
+
308
+ ## === The Headers
309
+ def check_headers(header)
310
+ ## The header must respond to each, and yield values of key and value.
311
+ assert("header should respond to #each") { header.respond_to? :each }
312
+ header.each { |key, value|
313
+ ## The header keys must be Strings.
314
+ assert("header key must be a string, was #{key.class}") {
315
+ key.instance_of? String
316
+ }
317
+ ## The header must not contain a +Status+ key,
318
+ assert("header must not contain Status") { key.downcase != "status" }
319
+ ## contain keys with <tt>:</tt> or newlines in their name,
320
+ assert("header names must not contain : or \\n") { key !~ /[:\n]/ }
321
+ ## contain keys names that end in <tt>-</tt> or <tt>_</tt>,
322
+ assert("header names must not end in - or _") { key !~ /[-_]\z/ }
323
+ ## but only contain keys that consist of
324
+ ## letters, digits, <tt>_</tt> or <tt>-</tt> and start with a letter.
325
+ assert("invalid header name: #{key}") { key =~ /\A[a-zA-Z][a-zA-Z0-9_-]*\z/ }
326
+ ##
327
+ ## The values of the header must respond to #each.
328
+ assert("header values must respond to #each") { value.respond_to? :each }
329
+ value.each { |item|
330
+ ## The values passed on #each must be Strings
331
+ assert("header values must consist of Strings") {
332
+ item.instance_of?(String)
333
+ }
334
+ ## and not contain characters below 037.
335
+ assert("invalid header value #{key}: #{item.inspect}") {
336
+ item !~ /[\000-\037]/
337
+ }
338
+ }
339
+ }
340
+ end
341
+
342
+ ## === The Content-Type
343
+ def check_content_type(status, headers)
344
+ headers.each { |key, value|
345
+ ## There must be a <tt>Content-Type</tt>, except when the
346
+ ## +Status+ is 204 or 304, in which case there must be none
347
+ ## given.
348
+ if key.downcase == "content-type"
349
+ assert("Content-Type header found in #{status} response, not allowed"){
350
+ not [204, 304].include? status.to_i
351
+ }
352
+ return
353
+ end
354
+ }
355
+ assert("No Content-Type header found") {
356
+ [201, 204, 304].include? status.to_i
357
+ }
358
+ end
359
+
360
+ ## === The Body
361
+ def each
362
+ @closed = false
363
+ ## The Body must respond to #each
364
+ @body.each { |part|
365
+ ## and must only yield String values.
366
+ assert("Body yielded non-string value #{part.inspect}") {
367
+ part.instance_of? String
368
+ }
369
+ yield part
370
+ }
371
+ ##
372
+ ## If the Body responds to #close, it will be called after iteration.
373
+ # XXX howto: assert("Body has not been closed") { @closed }
374
+
375
+ ##
376
+ ## The Body commonly is an Array of Strings, the application
377
+ ## instance itself, or a File-like object.
378
+ end
379
+
380
+ def close
381
+ @closed = true
382
+ @body.close if @body.respond_to?(:close)
383
+ end
384
+
385
+ # :startdoc:
386
+
387
+ end
388
+ end
389
+
390
+ ## == Thanks
391
+ ## Some parts of this specification are adopted from PEP333: Python
392
+ ## Web Server Gateway Interface
393
+ ## v1.0 (http://www.python.org/dev/peps/pep-0333/). I'd like to thank
394
+ ## everyone involved in that effort.