webricknio 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +26 -0
- data/README.md +58 -0
- data/lib/generator/USAGE +8 -0
- data/lib/generator/webricknio.rb +57 -0
- data/lib/generator/webricknio_generator.rb +15 -0
- data/lib/rack/handler/webricknio.rb +88 -0
- data/lib/webricknio.rb +237 -0
- data/lib/webricknio/accesslog.rb +157 -0
- data/lib/webricknio/block.rb +271 -0
- data/lib/webricknio/block.yaml +7 -0
- data/lib/webricknio/config.rb +125 -0
- data/lib/webricknio/httprequest.rb +559 -0
- data/lib/webricknio/httpresponse.rb +469 -0
- data/lib/webricknio/httpserver.rb +499 -0
- data/lib/webricknio/log.rb +30 -0
- data/lib/webricknio/version.rb +14 -0
- data/webricknio.gemspec +15 -0
- metadata +79 -0
@@ -0,0 +1,469 @@
|
|
1
|
+
#
|
2
|
+
# httpresponse.rb -- HTTPResponse Class
|
3
|
+
#
|
4
|
+
# Author: IPR -- Internet Programming with Ruby -- writers, Pradeep Singh
|
5
|
+
# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
|
6
|
+
# Copyright (c) 2002 Internet Programming with Ruby writers
|
7
|
+
# Copyright (c) 2012 Pradeep Singh
|
8
|
+
# All rights reserved.
|
9
|
+
#
|
10
|
+
# $IPR: httpresponse.rb,v 1.45 2003/07/11 11:02:25 gotoyuzo Exp $
|
11
|
+
|
12
|
+
require 'time'
|
13
|
+
require 'webrick/httpversion'
|
14
|
+
require 'webrick/htmlutils'
|
15
|
+
require 'webrick/httputils'
|
16
|
+
require 'webrick/httpstatus'
|
17
|
+
require 'webrick/utils'
|
18
|
+
|
19
|
+
require 'java'
|
20
|
+
|
21
|
+
java_import 'java.nio.ByteBuffer'
|
22
|
+
|
23
|
+
module WEBrickNIO
|
24
|
+
##
|
25
|
+
# An HTTP response.
|
26
|
+
|
27
|
+
class HTTPResponse
|
28
|
+
attr_reader :http_version, :status, :header
|
29
|
+
attr_reader :cookies
|
30
|
+
attr_accessor :reason_phrase
|
31
|
+
|
32
|
+
##
|
33
|
+
# Body may be a String or IO subclass.
|
34
|
+
|
35
|
+
attr_accessor :body
|
36
|
+
|
37
|
+
attr_accessor :request_method, :request_uri, :request_http_version
|
38
|
+
attr_accessor :filename
|
39
|
+
attr_accessor :keep_alive
|
40
|
+
attr_reader :config, :sent_size
|
41
|
+
|
42
|
+
##
|
43
|
+
# Creates a new HTTP response object
|
44
|
+
|
45
|
+
def initialize(config)
|
46
|
+
@config = config
|
47
|
+
@buffer_size = config[:OutputBufferSize]
|
48
|
+
@logger = config[:Logger]
|
49
|
+
@header = Hash.new
|
50
|
+
@status = ::WEBrick::HTTPStatus::RC_OK
|
51
|
+
@reason_phrase = nil
|
52
|
+
@http_version = ::WEBrick::HTTPVersion::convert(@config[:HTTPVersion])
|
53
|
+
@body = ''
|
54
|
+
@keep_alive = true
|
55
|
+
@cookies = []
|
56
|
+
@request_method = nil
|
57
|
+
@request_uri = nil
|
58
|
+
@request_http_version = @http_version # temporary
|
59
|
+
@chunked = false
|
60
|
+
@filename = nil
|
61
|
+
@sent_size = 0
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# The response's HTTP status line
|
66
|
+
|
67
|
+
def status_line
|
68
|
+
"HTTP/#@http_version #@status #@reason_phrase #{::WEBrick::CRLF}"
|
69
|
+
end
|
70
|
+
|
71
|
+
##
|
72
|
+
# Sets the response's status to the +status+ code
|
73
|
+
|
74
|
+
def status=(status)
|
75
|
+
@status = status
|
76
|
+
@reason_phrase = ::WEBrick::HTTPStatus::reason_phrase(status)
|
77
|
+
end
|
78
|
+
|
79
|
+
##
|
80
|
+
# Retrieves the response header +field+
|
81
|
+
|
82
|
+
def [](field)
|
83
|
+
@header[field.downcase]
|
84
|
+
end
|
85
|
+
|
86
|
+
##
|
87
|
+
# Sets the response header +field+ to +value+
|
88
|
+
|
89
|
+
def []=(field, value)
|
90
|
+
@header[field.downcase] = value.to_s
|
91
|
+
end
|
92
|
+
|
93
|
+
##
|
94
|
+
# The content-length header
|
95
|
+
|
96
|
+
def content_length
|
97
|
+
if len = self['content-length']
|
98
|
+
return Integer(len)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
##
|
103
|
+
# Sets the content-length header to +len+
|
104
|
+
|
105
|
+
def content_length=(len)
|
106
|
+
self['content-length'] = len.to_s
|
107
|
+
end
|
108
|
+
|
109
|
+
##
|
110
|
+
# The content-type header
|
111
|
+
|
112
|
+
def content_type
|
113
|
+
self['content-type']
|
114
|
+
end
|
115
|
+
|
116
|
+
##
|
117
|
+
# Sets the content-type header to +type+
|
118
|
+
|
119
|
+
def content_type=(type)
|
120
|
+
self['content-type'] = type
|
121
|
+
end
|
122
|
+
|
123
|
+
##
|
124
|
+
# Iterates over each header in the resopnse
|
125
|
+
|
126
|
+
def each
|
127
|
+
@header.each{|field, value| yield(field, value) }
|
128
|
+
end
|
129
|
+
|
130
|
+
##
|
131
|
+
# Will this response body be returned using chunked transfer-encoding?
|
132
|
+
|
133
|
+
def chunked?
|
134
|
+
@chunked
|
135
|
+
end
|
136
|
+
|
137
|
+
##
|
138
|
+
# Enables chunked transfer encoding.
|
139
|
+
|
140
|
+
def chunked=(val)
|
141
|
+
@chunked = val ? true : false
|
142
|
+
end
|
143
|
+
|
144
|
+
##
|
145
|
+
# Will this response's connection be kept alive?
|
146
|
+
|
147
|
+
def keep_alive?
|
148
|
+
@keep_alive
|
149
|
+
end
|
150
|
+
|
151
|
+
##
|
152
|
+
# Sends the response on +socket+
|
153
|
+
|
154
|
+
def send_response(sock_channel)
|
155
|
+
if !sock_channel.is_open
|
156
|
+
@logger.debug "client closed connection"
|
157
|
+
@keep_alive = false
|
158
|
+
return
|
159
|
+
end
|
160
|
+
|
161
|
+
begin
|
162
|
+
buffer = ByteBuffer.wrap(get_header.to_java_bytes)
|
163
|
+
while(buffer.has_remaining)
|
164
|
+
num_sent = sock_channel.java_send :write, [Java::JavaNio::ByteBuffer], buffer
|
165
|
+
raise ::WEBrick::HTTPStatus::EOFError if num_sent == -1 # stream is dead
|
166
|
+
end
|
167
|
+
@logger.debug "header byte size sent: #{get_header.bytesize}"
|
168
|
+
buffer.clear
|
169
|
+
if @body && @body.length > 0
|
170
|
+
buffer = ByteBuffer.wrap(@body.to_java_bytes)
|
171
|
+
while(buffer.has_remaining)
|
172
|
+
num_sent = sock_channel.java_send :write, [Java::JavaNio::ByteBuffer], buffer
|
173
|
+
raise ::WEBrick::HTTPStatus::EOFError if num_sent == -1 # stream is dead
|
174
|
+
end
|
175
|
+
@logger.debug "body byte size sent: #{@body.bytesize}"
|
176
|
+
else
|
177
|
+
@logger.debug "no body to be sent"
|
178
|
+
end
|
179
|
+
rescue Errno::EPIPE, Errno::ECONNRESET, Errno::ENOTCONN => ex
|
180
|
+
@logger.error(ex)
|
181
|
+
@keep_alive = false
|
182
|
+
rescue Exception => ex
|
183
|
+
if ex.java_class.name.nil?
|
184
|
+
@logger.error(ex)
|
185
|
+
else
|
186
|
+
@logger.debug "error sending response: #{ex.java_class.name}"
|
187
|
+
end
|
188
|
+
@keep_alive = false
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
|
193
|
+
##
|
194
|
+
# Sets up the headers for sending
|
195
|
+
|
196
|
+
def setup_header()
|
197
|
+
@reason_phrase ||= ::WEBrick::HTTPStatus::reason_phrase(@status)
|
198
|
+
@header['server'] ||= @config[:ServerSoftware]
|
199
|
+
@header['date'] ||= Time.now.httpdate
|
200
|
+
|
201
|
+
# HTTP/0.9 features
|
202
|
+
if @request_http_version < "1.0"
|
203
|
+
@http_version = ::WEBrick::HTTPVersion.new("0.9")
|
204
|
+
@keep_alive = false
|
205
|
+
end
|
206
|
+
|
207
|
+
# HTTP/1.0 features
|
208
|
+
if @request_http_version < "1.1"
|
209
|
+
if chunked?
|
210
|
+
@chunked = false
|
211
|
+
ver = @request_http_version.to_s
|
212
|
+
msg = "chunked is set for an HTTP/#{ver} request. (ignored)"
|
213
|
+
@logger.warn(msg)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# Determine the message length (RFC2616 -- 4.4 Message Length)
|
218
|
+
if @status == 304 || @status == 204 || ::WEBrick::HTTPStatus::info?(@status)
|
219
|
+
@header.delete('content-length')
|
220
|
+
@body = ""
|
221
|
+
elsif chunked?
|
222
|
+
@header["transfer-encoding"] = "chunked"
|
223
|
+
@header.delete('content-length')
|
224
|
+
elsif %r{^multipart/byteranges} =~ @header['content-type']
|
225
|
+
@header.delete('content-length')
|
226
|
+
elsif @header['content-length'].nil?
|
227
|
+
unless @body.is_a?(IO)
|
228
|
+
@header['content-length'] = @body ? @body.bytesize : 0
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# Keep-Alive connection.
|
233
|
+
if @header['connection'] == "close"
|
234
|
+
@keep_alive = false
|
235
|
+
elsif keep_alive?
|
236
|
+
if chunked? || @header['content-length'] || @status == 304 || @status == 204
|
237
|
+
@header['connection'] = "Keep-Alive"
|
238
|
+
else
|
239
|
+
msg = "Could not determine content-length of response body. Set content-length of the response or set Response#chunked = true"
|
240
|
+
@logger.warn(msg)
|
241
|
+
@header['connection'] = "close"
|
242
|
+
@keep_alive = false
|
243
|
+
end
|
244
|
+
else
|
245
|
+
@header['connection'] = "close"
|
246
|
+
end
|
247
|
+
|
248
|
+
# Location is a single absoluteURI.
|
249
|
+
if location = @header['location']
|
250
|
+
if @request_uri
|
251
|
+
@header['location'] = @request_uri.merge(location)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
|
257
|
+
##
|
258
|
+
# Sends the headers on +socket+
|
259
|
+
|
260
|
+
def get_header
|
261
|
+
@final_header ||=
|
262
|
+
if @http_version.major > 0 then
|
263
|
+
setup_header()
|
264
|
+
data = status_line()
|
265
|
+
@header.each{|key, value|
|
266
|
+
tmp = key.gsub(/\bwww|^te$|\b\w/){ $&.upcase }
|
267
|
+
data << "#{tmp}: #{value}" << ::WEBrick::CRLF
|
268
|
+
}
|
269
|
+
@cookies.each{|cookie|
|
270
|
+
data << "Set-Cookie: " << cookie.to_s << ::WEBrick::CRLF
|
271
|
+
}
|
272
|
+
data << ::WEBrick::CRLF
|
273
|
+
data
|
274
|
+
else
|
275
|
+
""
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
##
|
280
|
+
# Sends the body on +socket+
|
281
|
+
|
282
|
+
def send_body(socket)
|
283
|
+
case @body
|
284
|
+
when IO then send_body_io(socket)
|
285
|
+
else send_body_string(socket)
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
def to_s # :nodoc:
|
290
|
+
ret = ""
|
291
|
+
send_response(ret)
|
292
|
+
ret
|
293
|
+
end
|
294
|
+
|
295
|
+
##
|
296
|
+
# Redirects to +url+ with a WEBrick::HTTPStatus::Redirect +status+.
|
297
|
+
#
|
298
|
+
# Example:
|
299
|
+
#
|
300
|
+
# res.set_redirect WEBrick::HTTPStatus::TemporaryRedirect
|
301
|
+
|
302
|
+
def set_redirect(status, url)
|
303
|
+
@body = "<HTML><A HREF=\"#{url.to_s}\">#{url.to_s}</A>.</HTML>\n"
|
304
|
+
@header['location'] = url.to_s
|
305
|
+
raise status
|
306
|
+
end
|
307
|
+
|
308
|
+
##
|
309
|
+
# Creates an error page for exception +ex+ with an optional +backtrace+
|
310
|
+
|
311
|
+
def set_error(ex, backtrace=false)
|
312
|
+
case ex
|
313
|
+
when ::WEBrick::HTTPStatus::Status
|
314
|
+
@keep_alive = false if ::WEBrick::HTTPStatus::error?(ex.code)
|
315
|
+
self.status = ex.code
|
316
|
+
else
|
317
|
+
@keep_alive = false
|
318
|
+
self.status = ::WEBrick::HTTPStatus::RC_INTERNAL_SERVER_ERROR
|
319
|
+
end
|
320
|
+
@header['content-type'] = "text/html; charset=ISO-8859-1"
|
321
|
+
|
322
|
+
if respond_to?(:create_error_page)
|
323
|
+
create_error_page()
|
324
|
+
return
|
325
|
+
end
|
326
|
+
|
327
|
+
if @request_uri
|
328
|
+
host, port = @request_uri.host, @request_uri.port
|
329
|
+
else
|
330
|
+
host, port = @config[:ServerName], @config[:Port]
|
331
|
+
end
|
332
|
+
|
333
|
+
@body = ''
|
334
|
+
@body << <<-_end_of_html_
|
335
|
+
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN">
|
336
|
+
<HTML>
|
337
|
+
<HEAD><TITLE>#{::WEBrick::HTMLUtils::escape(@reason_phrase)}</TITLE></HEAD>
|
338
|
+
<BODY>
|
339
|
+
<H1>#{::WEBrick::HTMLUtils::escape(@reason_phrase)}</H1>
|
340
|
+
#{::WEBrick::HTMLUtils::escape(ex.message)}
|
341
|
+
<HR>
|
342
|
+
_end_of_html_
|
343
|
+
|
344
|
+
if backtrace && $DEBUG
|
345
|
+
@body << "backtrace of `#{::WEBrick::HTMLUtils::escape(ex.class.to_s)}' "
|
346
|
+
@body << "#{::WEBrick::HTMLUtils::escape(ex.message)}"
|
347
|
+
@body << "<PRE>"
|
348
|
+
ex.backtrace.each{|line| @body << "\t#{line}\n"}
|
349
|
+
@body << "</PRE><HR>"
|
350
|
+
end
|
351
|
+
|
352
|
+
@body << <<-_end_of_html_
|
353
|
+
<ADDRESS>
|
354
|
+
#{::WEBrick::HTMLUtils::escape(@config[:ServerSoftware])} at
|
355
|
+
#{host}:#{port}
|
356
|
+
</ADDRESS>
|
357
|
+
</BODY>
|
358
|
+
</HTML>
|
359
|
+
_end_of_html_
|
360
|
+
end
|
361
|
+
|
362
|
+
def create_error_page()
|
363
|
+
if @request_uri
|
364
|
+
host, port = @request_uri.host, @request_uri.port
|
365
|
+
else
|
366
|
+
#host, port = @config[:ServerName], @config[:Port]
|
367
|
+
end
|
368
|
+
|
369
|
+
@body << <<-_end_of_html_
|
370
|
+
<!DOCTYPE html>
|
371
|
+
<HTML>
|
372
|
+
<HEAD><TITLE>#{::WEBrick::HTMLUtils::escape(@reason_phrase)}</TITLE></HEAD>
|
373
|
+
<BODY>
|
374
|
+
<H1>#{::WEBrick::HTMLUtils::escape(@reason_phrase)}</H1>
|
375
|
+
<HR>
|
376
|
+
<ADDRESS>
|
377
|
+
#{::WEBrick::HTMLUtils::escape(@config[:ServerSoftware])}
|
378
|
+
_end_of_html_
|
379
|
+
|
380
|
+
if defined?(host) && defined?(port) && !host.nil? && !port.nil?
|
381
|
+
@body << <<-_end_of_html_
|
382
|
+
at
|
383
|
+
#{host}:#{port}
|
384
|
+
_end_of_html_
|
385
|
+
end
|
386
|
+
|
387
|
+
@body << <<-_end_of_html_
|
388
|
+
</ADDRESS>
|
389
|
+
</BODY>
|
390
|
+
</HTML>
|
391
|
+
_end_of_html_
|
392
|
+
end
|
393
|
+
|
394
|
+
|
395
|
+
private
|
396
|
+
|
397
|
+
|
398
|
+
def send_body_io(socket)
|
399
|
+
begin
|
400
|
+
if @request_method == "HEAD"
|
401
|
+
# do nothing
|
402
|
+
elsif chunked?
|
403
|
+
while buf = @body.read(@buffer_size)
|
404
|
+
next if buf.empty?
|
405
|
+
data = ""
|
406
|
+
data << format("%x", buf.bytesize) << CRLF
|
407
|
+
data << buf << CRLF
|
408
|
+
_write_data(socket, data)
|
409
|
+
@sent_size += buf.bytesize
|
410
|
+
end
|
411
|
+
_write_data(socket, "0#{CRLF}#{CRLF}")
|
412
|
+
else
|
413
|
+
size = @header['content-length'].to_i
|
414
|
+
_send_file(socket, @body, 0, size)
|
415
|
+
@sent_size = size
|
416
|
+
end
|
417
|
+
ensure
|
418
|
+
@body.close
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
def send_body_string(socket)
|
423
|
+
if @request_method == "HEAD"
|
424
|
+
# do nothing
|
425
|
+
elsif chunked?
|
426
|
+
body ? @body.bytesize : 0
|
427
|
+
while buf = @body[@sent_size, @buffer_size]
|
428
|
+
break if buf.empty?
|
429
|
+
data = ""
|
430
|
+
data << format("%x", buf.bytesize) << CRLF
|
431
|
+
data << buf << CRLF
|
432
|
+
_write_data(socket, data)
|
433
|
+
@sent_size += buf.bytesize
|
434
|
+
end
|
435
|
+
_write_data(socket, "0#{CRLF}#{CRLF}")
|
436
|
+
else
|
437
|
+
if @body && @body.bytesize > 0
|
438
|
+
_write_data(socket, @body)
|
439
|
+
@sent_size = @body.bytesize
|
440
|
+
end
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
def _send_file(output, input, offset, size)
|
445
|
+
while offset > 0
|
446
|
+
sz = @buffer_size < size ? @buffer_size : size
|
447
|
+
buf = input.read(sz)
|
448
|
+
offset -= buf.bytesize
|
449
|
+
end
|
450
|
+
|
451
|
+
if size == 0
|
452
|
+
while buf = input.read(@buffer_size)
|
453
|
+
_write_data(output, buf)
|
454
|
+
end
|
455
|
+
else
|
456
|
+
while size > 0
|
457
|
+
sz = @buffer_size < size ? @buffer_size : size
|
458
|
+
buf = input.read(sz)
|
459
|
+
_write_data(output, buf)
|
460
|
+
size -= buf.bytesize
|
461
|
+
end
|
462
|
+
end
|
463
|
+
end
|
464
|
+
|
465
|
+
def _write_data(socket, data)
|
466
|
+
socket << data
|
467
|
+
end
|
468
|
+
end
|
469
|
+
end
|