1 | #
|
---|
2 | # httpresponse.rb -- HTTPResponse Class
|
---|
3 | #
|
---|
4 | # Author: IPR -- Internet Programming with Ruby -- writers
|
---|
5 | # Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
|
---|
6 | # Copyright (c) 2002 Internet Programming with Ruby writers. All rights
|
---|
7 | # reserved.
|
---|
8 | #
|
---|
9 | # $IPR: httpresponse.rb,v 1.45 2003/07/11 11:02:25 gotoyuzo Exp $
|
---|
10 |
|
---|
11 | require 'time'
|
---|
12 | require 'webrick/httpversion'
|
---|
13 | require 'webrick/htmlutils'
|
---|
14 | require 'webrick/httputils'
|
---|
15 | require 'webrick/httpstatus'
|
---|
16 |
|
---|
17 | module WEBrick
|
---|
18 | class HTTPResponse
|
---|
19 | BUFSIZE = 1024*4
|
---|
20 |
|
---|
21 | attr_reader :http_version, :status, :header
|
---|
22 | attr_reader :cookies
|
---|
23 | attr_accessor :reason_phrase
|
---|
24 | attr_accessor :body
|
---|
25 |
|
---|
26 | attr_accessor :request_method, :request_uri, :request_http_version
|
---|
27 | attr_accessor :filename
|
---|
28 | attr_accessor :keep_alive
|
---|
29 | attr_reader :config, :sent_size
|
---|
30 |
|
---|
31 | def initialize(config)
|
---|
32 | @config = config
|
---|
33 | @logger = config[:Logger]
|
---|
34 | @header = Hash.new
|
---|
35 | @status = HTTPStatus::RC_OK
|
---|
36 | @reason_phrase = nil
|
---|
37 | @http_version = HTTPVersion::convert(@config[:HTTPVersion])
|
---|
38 | @body = ''
|
---|
39 | @keep_alive = true
|
---|
40 | @cookies = []
|
---|
41 | @request_method = nil
|
---|
42 | @request_uri = nil
|
---|
43 | @request_http_version = @http_version # temporary
|
---|
44 | @chunked = false
|
---|
45 | @filename = nil
|
---|
46 | @sent_size = 0
|
---|
47 | end
|
---|
48 |
|
---|
49 | def status_line
|
---|
50 | "HTTP/#@http_version #@status #@reason_phrase #{CRLF}"
|
---|
51 | end
|
---|
52 |
|
---|
53 | def status=(status)
|
---|
54 | @status = status
|
---|
55 | @reason_phrase = HTTPStatus::reason_phrase(status)
|
---|
56 | end
|
---|
57 |
|
---|
58 | def [](field)
|
---|
59 | @header[field.downcase]
|
---|
60 | end
|
---|
61 |
|
---|
62 | def []=(field, value)
|
---|
63 | @header[field.downcase] = value.to_s
|
---|
64 | end
|
---|
65 |
|
---|
66 | def content_length
|
---|
67 | if len = self['content-length']
|
---|
68 | return Integer(len)
|
---|
69 | end
|
---|
70 | end
|
---|
71 |
|
---|
72 | def content_length=(len)
|
---|
73 | self['content-length'] = len.to_s
|
---|
74 | end
|
---|
75 |
|
---|
76 | def content_type
|
---|
77 | self['content-type']
|
---|
78 | end
|
---|
79 |
|
---|
80 | def content_type=(type)
|
---|
81 | self['content-type'] = type
|
---|
82 | end
|
---|
83 |
|
---|
84 | def each
|
---|
85 | @header.each{|k, v| yield(k, v) }
|
---|
86 | end
|
---|
87 |
|
---|
88 | def chunked?
|
---|
89 | @chunked
|
---|
90 | end
|
---|
91 |
|
---|
92 | def chunked=(val)
|
---|
93 | @chunked = val ? true : false
|
---|
94 | end
|
---|
95 |
|
---|
96 | def keep_alive?
|
---|
97 | @keep_alive
|
---|
98 | end
|
---|
99 |
|
---|
100 | def send_response(socket)
|
---|
101 | begin
|
---|
102 | setup_header()
|
---|
103 | send_header(socket)
|
---|
104 | send_body(socket)
|
---|
105 | rescue Errno::EPIPE, Errno::ECONNRESET, Errno::ENOTCONN => ex
|
---|
106 | @logger.debug(ex)
|
---|
107 | @keep_alive = false
|
---|
108 | rescue Exception => ex
|
---|
109 | @logger.error(ex)
|
---|
110 | @keep_alive = false
|
---|
111 | end
|
---|
112 | end
|
---|
113 |
|
---|
114 | def setup_header()
|
---|
115 | @reason_phrase ||= HTTPStatus::reason_phrase(@status)
|
---|
116 | @header['server'] ||= @config[:ServerSoftware]
|
---|
117 | @header['date'] ||= Time.now.httpdate
|
---|
118 |
|
---|
119 | # HTTP/0.9 features
|
---|
120 | if @request_http_version < "1.0"
|
---|
121 | @http_version = HTTPVersion.new("0.9")
|
---|
122 | @keep_alive = false
|
---|
123 | end
|
---|
124 |
|
---|
125 | # HTTP/1.0 features
|
---|
126 | if @request_http_version < "1.1"
|
---|
127 | if chunked?
|
---|
128 | @chunked = false
|
---|
129 | ver = @request_http_version.to_s
|
---|
130 | msg = "chunked is set for an HTTP/#{ver} request. (ignored)"
|
---|
131 | @logger.warn(msg)
|
---|
132 | end
|
---|
133 | end
|
---|
134 |
|
---|
135 | # Determin the message length (RFC2616 -- 4.4 Message Length)
|
---|
136 | if @status == 304 || @status == 204 || HTTPStatus::info?(@status)
|
---|
137 | @header.delete('content-length')
|
---|
138 | @body = ""
|
---|
139 | elsif chunked?
|
---|
140 | @header["transfer-encoding"] = "chunked"
|
---|
141 | @header.delete('content-length')
|
---|
142 | elsif %r{^multipart/byteranges} =~ @header['content-type']
|
---|
143 | @header.delete('content-length')
|
---|
144 | elsif @header['content-length'].nil?
|
---|
145 | unless @body.is_a?(IO)
|
---|
146 | @header['content-length'] = @body ? @body.size : 0
|
---|
147 | end
|
---|
148 | end
|
---|
149 |
|
---|
150 | # Keep-Alive connection.
|
---|
151 | if @header['connection'] == "close"
|
---|
152 | @keep_alive = false
|
---|
153 | elsif keep_alive?
|
---|
154 | if chunked? || @header['content-length']
|
---|
155 | @header['connection'] = "Keep-Alive"
|
---|
156 | end
|
---|
157 | else
|
---|
158 | @header['connection'] = "close"
|
---|
159 | end
|
---|
160 |
|
---|
161 | # Location is a single absoluteURI.
|
---|
162 | if location = @header['location']
|
---|
163 | if @request_uri
|
---|
164 | @header['location'] = @request_uri.merge(location)
|
---|
165 | end
|
---|
166 | end
|
---|
167 | end
|
---|
168 |
|
---|
169 | def send_header(socket)
|
---|
170 | if @http_version.major > 0
|
---|
171 | data = status_line()
|
---|
172 | @header.each{|key, value|
|
---|
173 | tmp = key.gsub(/\bwww|^te$|\b\w/){|s| s.upcase }
|
---|
174 | data << "#{tmp}: #{value}" << CRLF
|
---|
175 | }
|
---|
176 | @cookies.each{|cookie|
|
---|
177 | data << "Set-Cookie: " << cookie.to_s << CRLF
|
---|
178 | }
|
---|
179 | data << CRLF
|
---|
180 | _write_data(socket, data)
|
---|
181 | end
|
---|
182 | end
|
---|
183 |
|
---|
184 | def send_body(socket)
|
---|
185 | case @body
|
---|
186 | when IO then send_body_io(socket)
|
---|
187 | else send_body_string(socket)
|
---|
188 | end
|
---|
189 | end
|
---|
190 |
|
---|
191 | def to_s
|
---|
192 | ret = ""
|
---|
193 | send_response(ret)
|
---|
194 | ret
|
---|
195 | end
|
---|
196 |
|
---|
197 | def set_redirect(status, url)
|
---|
198 | @body = "<HTML><A HREF=\"#{url.to_s}\">#{url.to_s}</A>.</HTML>\n"
|
---|
199 | @header['location'] = url.to_s
|
---|
200 | raise status
|
---|
201 | end
|
---|
202 |
|
---|
203 | def set_error(ex, backtrace=false)
|
---|
204 | case ex
|
---|
205 | when HTTPStatus::Status
|
---|
206 | @keep_alive = false if HTTPStatus::error?(ex.code)
|
---|
207 | self.status = ex.code
|
---|
208 | else
|
---|
209 | @keep_alive = false
|
---|
210 | self.status = HTTPStatus::RC_INTERNAL_SERVER_ERROR
|
---|
211 | end
|
---|
212 | @header['content-type'] = "text/html"
|
---|
213 |
|
---|
214 | if respond_to?(:create_error_page)
|
---|
215 | create_error_page()
|
---|
216 | return
|
---|
217 | end
|
---|
218 |
|
---|
219 | if @request_uri
|
---|
220 | host, port = @request_uri.host, @request_uri.port
|
---|
221 | else
|
---|
222 | host, port = @config[:ServerName], @config[:Port]
|
---|
223 | end
|
---|
224 |
|
---|
225 | @body = ''
|
---|
226 | @body << <<-_end_of_html_
|
---|
227 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN">
|
---|
228 | <HTML>
|
---|
229 | <HEAD><TITLE>#{HTMLUtils::escape(@reason_phrase)}</TITLE></HEAD>
|
---|
230 | <BODY>
|
---|
231 | <H1>#{HTMLUtils::escape(@reason_phrase)}</H1>
|
---|
232 | #{HTMLUtils::escape(ex.message)}
|
---|
233 | <HR>
|
---|
234 | _end_of_html_
|
---|
235 |
|
---|
236 | if backtrace && $DEBUG
|
---|
237 | @body << "backtrace of `#{HTMLUtils::escape(ex.class.to_s)}' "
|
---|
238 | @body << "#{HTMLUtils::escape(ex.message)}"
|
---|
239 | @body << "<PRE>"
|
---|
240 | ex.backtrace.each{|line| @body << "\t#{line}\n"}
|
---|
241 | @body << "</PRE><HR>"
|
---|
242 | end
|
---|
243 |
|
---|
244 | @body << <<-_end_of_html_
|
---|
245 | <ADDRESS>
|
---|
246 | #{HTMLUtils::escape(@config[:ServerSoftware])} at
|
---|
247 | #{host}:#{port}
|
---|
248 | </ADDRESS>
|
---|
249 | </BODY>
|
---|
250 | </HTML>
|
---|
251 | _end_of_html_
|
---|
252 | end
|
---|
253 |
|
---|
254 | private
|
---|
255 |
|
---|
256 | def send_body_io(socket)
|
---|
257 | begin
|
---|
258 | if @request_method == "HEAD"
|
---|
259 | # do nothing
|
---|
260 | elsif chunked?
|
---|
261 | while buf = @body.read(BUFSIZE)
|
---|
262 | next if buf.empty?
|
---|
263 | data = ""
|
---|
264 | data << format("%x", buf.size) << CRLF
|
---|
265 | data << buf << CRLF
|
---|
266 | _write_data(socket, data)
|
---|
267 | @sent_size += buf.size
|
---|
268 | end
|
---|
269 | _write_data(socket, "0#{CRLF}#{CRLF}")
|
---|
270 | else
|
---|
271 | size = @header['content-length'].to_i
|
---|
272 | _send_file(socket, @body, 0, size)
|
---|
273 | @sent_size = size
|
---|
274 | end
|
---|
275 | ensure
|
---|
276 | @body.close
|
---|
277 | end
|
---|
278 | end
|
---|
279 |
|
---|
280 | def send_body_string(socket)
|
---|
281 | if @request_method == "HEAD"
|
---|
282 | # do nothing
|
---|
283 | elsif chunked?
|
---|
284 | remain = body ? @body.size : 0
|
---|
285 | while buf = @body[@sent_size, BUFSIZE]
|
---|
286 | break if buf.empty?
|
---|
287 | data = ""
|
---|
288 | data << format("%x", buf.size) << CRLF
|
---|
289 | data << buf << CRLF
|
---|
290 | _write_data(socket, data)
|
---|
291 | @sent_size += buf.size
|
---|
292 | end
|
---|
293 | _write_data(socket, "0#{CRLF}#{CRLF}")
|
---|
294 | else
|
---|
295 | if @body && @body.size > 0
|
---|
296 | _write_data(socket, @body)
|
---|
297 | @sent_size = @body.size
|
---|
298 | end
|
---|
299 | end
|
---|
300 | end
|
---|
301 |
|
---|
302 | def _send_file(output, input, offset, size)
|
---|
303 | while offset > 0
|
---|
304 | sz = BUFSIZE < offset ? BUFSIZE : offset
|
---|
305 | buf = input.read(sz)
|
---|
306 | offset -= buf.size
|
---|
307 | end
|
---|
308 |
|
---|
309 | if size == 0
|
---|
310 | while buf = input.read(BUFSIZE)
|
---|
311 | _write_data(output, buf)
|
---|
312 | end
|
---|
313 | else
|
---|
314 | while size > 0
|
---|
315 | sz = BUFSIZE < size ? BUFSIZE : size
|
---|
316 | buf = input.read(sz)
|
---|
317 | _write_data(output, buf)
|
---|
318 | size -= buf.size
|
---|
319 | end
|
---|
320 | end
|
---|
321 | end
|
---|
322 |
|
---|
323 | def _write_data(socket, data)
|
---|
324 | socket << data
|
---|
325 | end
|
---|
326 | end
|
---|
327 | end
|
---|