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 | begin
|
---|
246 | @header = HTTPUtils::parse_header(@raw_header)
|
---|
247 | rescue => ex
|
---|
248 | raise HTTPStatus::BadRequest, ex.message
|
---|
249 | end
|
---|
250 | end
|
---|
251 |
|
---|
252 | def parse_uri(str, scheme="http")
|
---|
253 | if @config[:Escape8bitURI]
|
---|
254 | str = HTTPUtils::escape8bit(str)
|
---|
255 | end
|
---|
256 | uri = URI::parse(str)
|
---|
257 | return uri if uri.absolute?
|
---|
258 | if self["host"]
|
---|
259 | pattern = /\A(#{URI::REGEXP::PATTERN::HOST})(?::(\d+))?\z/n
|
---|
260 | host, port = *self['host'].scan(pattern)[0]
|
---|
261 | elsif @addr.size > 0
|
---|
262 | host, port = @addr[2], @addr[1]
|
---|
263 | else
|
---|
264 | host, port = @config[:ServerName], @config[:Port]
|
---|
265 | end
|
---|
266 | uri.scheme = scheme
|
---|
267 | uri.host = host
|
---|
268 | uri.port = port ? port.to_i : nil
|
---|
269 | return URI::parse(uri.to_s)
|
---|
270 | end
|
---|
271 |
|
---|
272 | def read_body(socket, block)
|
---|
273 | return unless socket
|
---|
274 | if tc = self['transfer-encoding']
|
---|
275 | case tc
|
---|
276 | when /chunked/io then read_chunked(socket, block)
|
---|
277 | else raise HTTPStatus::NotImplemented, "Transfer-Encoding: #{tc}."
|
---|
278 | end
|
---|
279 | elsif self['content-length'] || @remaining_size
|
---|
280 | @remaining_size ||= self['content-length'].to_i
|
---|
281 | while @remaining_size > 0
|
---|
282 | sz = BUFSIZE < @remaining_size ? BUFSIZE : @remaining_size
|
---|
283 | break unless buf = read_data(socket, sz)
|
---|
284 | @remaining_size -= buf.size
|
---|
285 | block.call(buf)
|
---|
286 | end
|
---|
287 | if @remaining_size > 0 && @socket.eof?
|
---|
288 | raise HTTPStatus::BadRequest, "invalid body size."
|
---|
289 | end
|
---|
290 | elsif BODY_CONTAINABLE_METHODS.member?(@request_method)
|
---|
291 | raise HTTPStatus::LengthRequired
|
---|
292 | end
|
---|
293 | return @body
|
---|
294 | end
|
---|
295 |
|
---|
296 | def read_chunk_size(socket)
|
---|
297 | line = read_line(socket)
|
---|
298 | if /^([0-9a-fA-F]+)(?:;(\S+))?/ =~ line
|
---|
299 | chunk_size = $1.hex
|
---|
300 | chunk_ext = $2
|
---|
301 | [ chunk_size, chunk_ext ]
|
---|
302 | else
|
---|
303 | raise HTTPStatus::BadRequest, "bad chunk `#{line}'."
|
---|
304 | end
|
---|
305 | end
|
---|
306 |
|
---|
307 | def read_chunked(socket, block)
|
---|
308 | chunk_size, = read_chunk_size(socket)
|
---|
309 | while chunk_size > 0
|
---|
310 | data = ""
|
---|
311 | while data.size < chunk_size
|
---|
312 | tmp = read_data(socket, chunk_size-data.size) # read chunk-data
|
---|
313 | break unless tmp
|
---|
314 | data << tmp
|
---|
315 | end
|
---|
316 | if data.nil? || data.size != chunk_size
|
---|
317 | raise BadRequest, "bad chunk data size."
|
---|
318 | end
|
---|
319 | read_line(socket) # skip CRLF
|
---|
320 | block.call(data)
|
---|
321 | chunk_size, = read_chunk_size(socket)
|
---|
322 | end
|
---|
323 | read_header(socket) # trailer + CRLF
|
---|
324 | @header.delete("transfer-encoding")
|
---|
325 | @remaining_size = 0
|
---|
326 | end
|
---|
327 |
|
---|
328 | def _read_data(io, method, arg)
|
---|
329 | begin
|
---|
330 | timeout(@config[:RequestTimeout]){
|
---|
331 | return io.__send__(method, arg)
|
---|
332 | }
|
---|
333 | rescue Errno::ECONNRESET
|
---|
334 | return nil
|
---|
335 | rescue TimeoutError
|
---|
336 | raise HTTPStatus::RequestTimeout
|
---|
337 | end
|
---|
338 | end
|
---|
339 |
|
---|
340 | def read_line(io)
|
---|
341 | _read_data(io, :gets, LF)
|
---|
342 | end
|
---|
343 |
|
---|
344 | def read_data(io, size)
|
---|
345 | _read_data(io, :read, size)
|
---|
346 | end
|
---|
347 |
|
---|
348 | def parse_query()
|
---|
349 | begin
|
---|
350 | if @request_method == "GET" || @request_method == "HEAD"
|
---|
351 | @query = HTTPUtils::parse_query(@query_string)
|
---|
352 | elsif self['content-type'] =~ /^application\/x-www-form-urlencoded/
|
---|
353 | @query = HTTPUtils::parse_query(body)
|
---|
354 | elsif self['content-type'] =~ /^multipart\/form-data; boundary=(.+)/
|
---|
355 | boundary = HTTPUtils::dequote($1)
|
---|
356 | @query = HTTPUtils::parse_form_data(body, boundary)
|
---|
357 | else
|
---|
358 | @query = Hash.new
|
---|
359 | end
|
---|
360 | rescue => ex
|
---|
361 | raise HTTPStatus::BadRequest, ex.message
|
---|
362 | end
|
---|
363 | end
|
---|
364 | end
|
---|
365 | end
|
---|