1 #
2 # httprequest.rb -- HTTPRequest 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: httprequest.rb,v 1.64 2003/07/13 17:18:22 gotoyuzo Exp $
10
11 require 'timeout'
12 require 'uri'
13
14 require 'webrick/httpversion'
15 require 'webrick/httpstatus'
16 require 'webrick/httputils'
17 require 'webrick/cookie'
18
19 module WEBrick
20
21 class HTTPRequest
22 BODY_CONTAINABLE_METHODS = [ "POST", "PUT" ]
23 BUFSIZE = 1024*4
24
25 # Request line
26 attr_reader :request_line
27 attr_reader :request_method, :unparsed_uri, :http_version
28
29 # Request-URI
30 attr_reader :request_uri, :host, :port, :path
31 attr_accessor :script_name, :path_info, :query_string
32
33 # Header and entity body
34 attr_reader :raw_header, :header, :cookies
35 attr_reader :accept, :accept_charset
36 attr_reader :accept_encoding, :accept_language
37
38 # Misc
39 attr_accessor :user
40 attr_reader :addr, :peeraddr
41 attr_reader :attributes
42 attr_reader :keep_alive
43 attr_reader :request_time
44
45 def initialize(config)
46 @config = config
47 @logger = config[:Logger]
48
49 @request_line = @request_method =
50 @unparsed_uri = @http_version = nil
51
52 @request_uri = @host = @port = @path = nil
53 @script_name = @path_info = nil
54 @query_string = nil
55 @query = nil
56 @form_data = nil
57
58 @raw_header = Array.new
59 @header = nil
60 @cookies = []
61 @accept = []
62 @accept_charset = []
63 @accept_encoding = []
64 @accept_language = []
65 @body = ""
66
67 @addr = @peeraddr = nil
68 @attributes = {}
69 @user = nil
70 @keep_alive = false
71 @request_time = nil
72
73 @remaining_size = nil
74 @socket = nil
75 end
76
77 def parse(socket=nil)
78 @socket = socket
79 begin
80 @peeraddr = socket.respond_to?(:peeraddr) ? socket.peeraddr : []
81 @addr = socket.respond_to?(:addr) ? socket.addr : []
82 rescue Errno::ENOTCONN
83 raise HTTPStatus::EOFError
84 end
85
86 read_request_line(socket)
87 if @http_version.major > 0
88 read_header(socket)
89 @header['cookie'].each{|cookie|
90 @cookies += Cookie::parse(cookie)
91 }
92 @accept = HTTPUtils.parse_qvalues(self['accept'])
93 @accept_charset = HTTPUtils.parse_qvalues(self['accept-charset'])
94 @accept_encoding = HTTPUtils.parse_qvalues(self['accept-encoding'])
95 @accept_language = HTTPUtils.parse_qvalues(self['accept-language'])
96 end
97 return if @request_method == "CONNECT"
98 return if @unparsed_uri == "*"
99
100 begin
101 @request_uri = parse_uri(@unparsed_uri)
102 @path = HTTPUtils::unescape(@request_uri.path)
103 @path = HTTPUtils::normalize_path(@path)
104 @host = @request_uri.host
105 @port = @request_uri.port
106 @query_string = @request_uri.query
107 @script_name = ""
108 @path_info = @path.dup
109 rescue
110 raise HTTPStatus::BadRequest, "bad URI `#{@unparsed_uri}'."
111 end
112
113 if /close/io =~ self["connection"]
114 @keep_alive = false
115 elsif /keep-alive/io =~ self["connection"]
116 @keep_alive = true
117 elsif @http_version < "1.1"
118 @keep_alive = false
119 else
120 @keep_alive = true
121 end
122 end
123
124 def body(&block)
125 block ||= Proc.new{|chunk| @body << chunk }
126 read_body(@socket, block)
127 @body.empty? ? nil : @body
128 end
129
130 def query
131 unless @query
132 parse_query()
133 end
134 @query
135 end
136
137 def content_length
138 return Integer(self['content-length'])
139 end
140
141 def content_type
142 return self['content-type']
143 end
144
145 def [](header_name)
146 if @header
147 value = @header[header_name.downcase]
148 value.empty? ? nil : value.join(", ")
149 end
150 end
151
152 def each
153 @header.each{|k, v|
154 value = @header[k]
155 yield(k, value.empty? ? nil : value.join(", "))
156 }
157 end
158
159 def keep_alive?
160 @keep_alive
161 end
162
163 def to_s
164 ret = @request_line.dup
165 @raw_header.each{|line| ret << line }
166 ret << CRLF
167 ret << body if body
168 ret
169 end
170
171 def fixup()
172 begin
173 body{|chunk| } # read remaining body
174 rescue HTTPStatus::Error => ex
175 @logger.error("HTTPRequest#fixup: #{ex.class} occured.")
176 @keep_alive = false
177 rescue => ex
178 @logger.error(ex)
179 @keep_alive = false
180 end
181 end
182
183 def meta_vars
184 # This method provides the metavariables defined by the revision 3
185 # of ``The WWW Common Gateway Interface Version 1.1''.
186 # (http://Web.Golux.Com/coar/cgi/)
187
188 meta = Hash.new
189
190 cl = self["Content-Length"]
191 ct = self["Content-Type"]
192 meta["CONTENT_LENGTH"] = cl if cl.to_i > 0
193 meta["CONTENT_TYPE"] = ct.dup if ct
194 meta["GATEWAY_INTERFACE"] = "CGI/1.1"
195 meta["PATH_INFO"] = @path_info ? @path_info.dup : ""
196 #meta["PATH_TRANSLATED"] = nil # no plan to be provided
197 meta["QUERY_STRING"] = @query_string ? @query_string.dup : ""
198 meta["REMOTE_ADDR"] = @peeraddr[3]
199 meta["REMOTE_HOST"] = @peeraddr[2]
200 #meta["REMOTE_IDENT"] = nil # no plan to be provided
201 meta["REMOTE_USER"] = @user
202 meta["REQUEST_METHOD"] = @request_method.dup
203 meta["REQUEST_URI"] = @request_uri.to_s
204 meta["SCRIPT_NAME"] = @script_name.dup
205 meta["SERVER_NAME"] = @host
206 meta["SERVER_PORT"] = @port.to_s
207 meta["SERVER_PROTOCOL"] = "HTTP/" + @config[:HTTPVersion].to_s
208 meta["SERVER_SOFTWARE"] = @config[:ServerSoftware].dup
209
210 self.each{|key, val|
211 next if /^content-type$/i =~ key
212 next if /^content-length$/i =~ key
213 name = "HTTP_" + key
214 name.gsub!(/-/o, "_")
215 name.upcase!
216 meta[name] = val
217 }
218
219 meta
220 end
221
222 private
223
224 def read_request_line(socket)
225 @request_line = read_line(socket) if socket
226 @request_time = Time.now
227 raise HTTPStatus::EOFError unless @request_line
228 if /^(\S+)\s+(\S+?)(?:\s+HTTP\/(\d+\.\d+))?\r?\n/mo =~ @request_line
229 @request_method = $1
230 @unparsed_uri = $2
231 @http_version = HTTPVersion.new($3 ? $3 : "0.9")
232 else
233 rl = @request_line.sub(/\x0d?\x0a\z/o, '')
234 raise HTTPStatus::BadRequest, "bad Request-Line `#{rl}'."
235 end
236 end
237
238 def read_header(socket)
239 if socket
240 while line = read_line(socket)
241 break if /\A(#{CRLF}|#{LF})\z/om =~ line
242 @raw_header << line
243 end
244 end
245 @header = HTTPUtils::parse_header(@raw_header.join)
246 end
247
248 def parse_uri(str, scheme="http")
249 if @config[:Escape8bitURI]
250 str = HTTPUtils::escape8bit(str)
251 end
252 str.sub!(%r{\A/+}o, '/')
253 uri = URI::parse(str)
254 return uri if uri.absolute?
255 if self["host"]
256 pattern = /\A(#{URI::REGEXP::PATTERN::HOST})(?::(\d+))?\z/n
257 host, port = *self['host'].scan(pattern)[0]
258 elsif @addr.size > 0
259 host, port = @addr[2], @addr[1]
260 else
261 host, port = @config[:ServerName], @config[:Port]
262 end
263 uri.scheme = scheme
264 uri.host = host
265 uri.port = port ? port.to_i : nil
266 return URI::parse(uri.to_s)
267 end
268
269 def read_body(socket, block)
270 return unless socket
271 if tc = self['transfer-encoding']
272 case tc
273 when /chunked/io then read_chunked(socket, block)
274 else raise HTTPStatus::NotImplemented, "Transfer-Encoding: #{tc}."
275 end
276 elsif self['content-length'] || @remaining_size
277 @remaining_size ||= self['content-length'].to_i
278 while @remaining_size > 0
279 sz = BUFSIZE < @remaining_size ? BUFSIZE : @remaining_size
280 break unless buf = read_data(socket, sz)
281 @remaining_size -= buf.size
282 block.call(buf)
283 end
284 if @remaining_size > 0 && @socket.eof?
285 raise HTTPStatus::BadRequest, "invalid body size."
286 end
287 elsif BODY_CONTAINABLE_METHODS.member?(@request_method)
288 raise HTTPStatus::LengthRequired
289 end
290 return @body
291 end
292
293 def read_chunk_size(socket)
294 line = read_line(socket)
295 if /^([0-9a-fA-F]+)(?:;(\S+))?/ =~ line
296 chunk_size = $1.hex
297 chunk_ext = $2
298 [ chunk_size, chunk_ext ]
299 else
300 raise HTTPStatus::BadRequest, "bad chunk `#{line}'."
301 end
302 end
303
304 def read_chunked(socket, block)
305 chunk_size, = read_chunk_size(socket)
306 while chunk_size > 0
307 data = ""
308 while data.size < chunk_size
309 tmp = read_data(socket, chunk_size-data.size) # read chunk-data
310 break unless tmp
311 data << tmp
312 end
313 if data.nil? || data.size != chunk_size
314 raise BadRequest, "bad chunk data size."
315 end
316 read_line(socket) # skip CRLF
317 block.call(data)
318 chunk_size, = read_chunk_size(socket)
319 end
320 read_header(socket) # trailer + CRLF
321 @header.delete("transfer-encoding")
322 @remaining_size = 0
323 end
324
325 def _read_data(io, method, arg)
326 begin
327 timeout(@config[:RequestTimeout]){
328 return io.__send__(method, arg)
329 }
330 rescue Errno::ECONNRESET
331 return nil
332 rescue TimeoutError
333 raise HTTPStatus::RequestTimeout
334 end
335 end
336
337 def read_line(io)
338 _read_data(io, :gets, LF)
339 end
340
341 def read_data(io, size)
342 _read_data(io, :read, size)
343 end
344
345 def parse_query()
346 begin
347 if @request_method == "GET" || @request_method == "HEAD"
348 @query = HTTPUtils::parse_query(@query_string)
349 elsif self['content-type'] =~ /^application\/x-www-form-urlencoded/
350 @query = HTTPUtils::parse_query(body)
351 elsif self['content-type'] =~ /^multipart\/form-data; boundary=(.+)/
352 boundary = HTTPUtils::dequote($1)
353 @query = HTTPUtils::parse_form_data(body, boundary)
354 else
355 @query = Hash.new
356 end
357 rescue => ex
358 raise HTTPStatus::BadRequest, ex.message
359 end
360 end
361 end
362 end