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 11require 'uri' 12require 'webrick/httpversion' 13require 'webrick/httpstatus' 14require 'webrick/httputils' 15require 'webrick/cookie' 16 17module WEBrick 18 19 ## 20 # An HTTP request. This is consumed by service and do_* methods in 21 # WEBrick servlets 22 23 class HTTPRequest 24 25 BODY_CONTAINABLE_METHODS = [ "POST", "PUT" ] # :nodoc: 26 27 # :section: Request line 28 29 ## 30 # The complete request line such as: 31 # 32 # GET / HTTP/1.1 33 34 attr_reader :request_line 35 36 ## 37 # The request method, GET, POST, PUT, etc. 38 39 attr_reader :request_method 40 41 ## 42 # The unparsed URI of the request 43 44 attr_reader :unparsed_uri 45 46 ## 47 # The HTTP version of the request 48 49 attr_reader :http_version 50 51 # :section: Request-URI 52 53 ## 54 # The parsed URI of the request 55 56 attr_reader :request_uri 57 58 ## 59 # The request path 60 61 attr_reader :path 62 63 ## 64 # The script name (CGI variable) 65 66 attr_accessor :script_name 67 68 ## 69 # The path info (CGI variable) 70 71 attr_accessor :path_info 72 73 ## 74 # The query from the URI of the request 75 76 attr_accessor :query_string 77 78 # :section: Header and entity body 79 80 ## 81 # The raw header of the request 82 83 attr_reader :raw_header 84 85 ## 86 # The parsed header of the request 87 88 attr_reader :header 89 90 ## 91 # The parsed request cookies 92 93 attr_reader :cookies 94 95 ## 96 # The Accept header value 97 98 attr_reader :accept 99 100 ## 101 # The Accept-Charset header value 102 103 attr_reader :accept_charset 104 105 ## 106 # The Accept-Encoding header value 107 108 attr_reader :accept_encoding 109 110 ## 111 # The Accept-Language header value 112 113 attr_reader :accept_language 114 115 # :section: 116 117 ## 118 # The remote user (CGI variable) 119 120 attr_accessor :user 121 122 ## 123 # The socket address of the server 124 125 attr_reader :addr 126 127 ## 128 # The socket address of the client 129 130 attr_reader :peeraddr 131 132 ## 133 # Hash of request attributes 134 135 attr_reader :attributes 136 137 ## 138 # Is this a keep-alive connection? 139 140 attr_reader :keep_alive 141 142 ## 143 # The local time this request was received 144 145 attr_reader :request_time 146 147 ## 148 # Creates a new HTTP request. WEBrick::Config::HTTP is the default 149 # configuration. 150 151 def initialize(config) 152 @config = config 153 @buffer_size = @config[:InputBufferSize] 154 @logger = config[:Logger] 155 156 @request_line = @request_method = 157 @unparsed_uri = @http_version = nil 158 159 @request_uri = @host = @port = @path = nil 160 @script_name = @path_info = nil 161 @query_string = nil 162 @query = nil 163 @form_data = nil 164 165 @raw_header = Array.new 166 @header = nil 167 @cookies = [] 168 @accept = [] 169 @accept_charset = [] 170 @accept_encoding = [] 171 @accept_language = [] 172 @body = "" 173 174 @addr = @peeraddr = nil 175 @attributes = {} 176 @user = nil 177 @keep_alive = false 178 @request_time = nil 179 180 @remaining_size = nil 181 @socket = nil 182 183 @forwarded_proto = @forwarded_host = @forwarded_port = 184 @forwarded_server = @forwarded_for = nil 185 end 186 187 ## 188 # Parses a request from +socket+. This is called internally by 189 # WEBrick::HTTPServer. 190 191 def parse(socket=nil) 192 @socket = socket 193 begin 194 @peeraddr = socket.respond_to?(:peeraddr) ? socket.peeraddr : [] 195 @addr = socket.respond_to?(:addr) ? socket.addr : [] 196 rescue Errno::ENOTCONN 197 raise HTTPStatus::EOFError 198 end 199 200 read_request_line(socket) 201 if @http_version.major > 0 202 read_header(socket) 203 @header['cookie'].each{|cookie| 204 @cookies += Cookie::parse(cookie) 205 } 206 @accept = HTTPUtils.parse_qvalues(self['accept']) 207 @accept_charset = HTTPUtils.parse_qvalues(self['accept-charset']) 208 @accept_encoding = HTTPUtils.parse_qvalues(self['accept-encoding']) 209 @accept_language = HTTPUtils.parse_qvalues(self['accept-language']) 210 end 211 return if @request_method == "CONNECT" 212 return if @unparsed_uri == "*" 213 214 begin 215 setup_forwarded_info 216 @request_uri = parse_uri(@unparsed_uri) 217 @path = HTTPUtils::unescape(@request_uri.path) 218 @path = HTTPUtils::normalize_path(@path) 219 @host = @request_uri.host 220 @port = @request_uri.port 221 @query_string = @request_uri.query 222 @script_name = "" 223 @path_info = @path.dup 224 rescue 225 raise HTTPStatus::BadRequest, "bad URI `#{@unparsed_uri}'." 226 end 227 228 if /close/io =~ self["connection"] 229 @keep_alive = false 230 elsif /keep-alive/io =~ self["connection"] 231 @keep_alive = true 232 elsif @http_version < "1.1" 233 @keep_alive = false 234 else 235 @keep_alive = true 236 end 237 end 238 239 ## 240 # Generate HTTP/1.1 100 continue response if the client expects it, 241 # otherwise does nothing. 242 243 def continue # :nodoc: 244 if self['expect'] == '100-continue' && @config[:HTTPVersion] >= "1.1" 245 @socket << "HTTP/#{@config[:HTTPVersion]} 100 continue#{CRLF}#{CRLF}" 246 @header.delete('expect') 247 end 248 end 249 250 ## 251 # Returns the request body. 252 253 def body(&block) # :yields: body_chunk 254 block ||= Proc.new{|chunk| @body << chunk } 255 read_body(@socket, block) 256 @body.empty? ? nil : @body 257 end 258 259 ## 260 # Request query as a Hash 261 262 def query 263 unless @query 264 parse_query() 265 end 266 @query 267 end 268 269 ## 270 # The content-length header 271 272 def content_length 273 return Integer(self['content-length']) 274 end 275 276 ## 277 # The content-type header 278 279 def content_type 280 return self['content-type'] 281 end 282 283 ## 284 # Retrieves +header_name+ 285 286 def [](header_name) 287 if @header 288 value = @header[header_name.downcase] 289 value.empty? ? nil : value.join(", ") 290 end 291 end 292 293 ## 294 # Iterates over the request headers 295 296 def each 297 if @header 298 @header.each{|k, v| 299 value = @header[k] 300 yield(k, value.empty? ? nil : value.join(", ")) 301 } 302 end 303 end 304 305 ## 306 # The host this request is for 307 308 def host 309 return @forwarded_host || @host 310 end 311 312 ## 313 # The port this request is for 314 315 def port 316 return @forwarded_port || @port 317 end 318 319 ## 320 # The server name this request is for 321 322 def server_name 323 return @forwarded_server || @config[:ServerName] 324 end 325 326 ## 327 # The client's IP address 328 329 def remote_ip 330 return self["client-ip"] || @forwarded_for || @peeraddr[3] 331 end 332 333 ## 334 # Is this an SSL request? 335 336 def ssl? 337 return @request_uri.scheme == "https" 338 end 339 340 ## 341 # Should the connection this request was made on be kept alive? 342 343 def keep_alive? 344 @keep_alive 345 end 346 347 def to_s # :nodoc: 348 ret = @request_line.dup 349 @raw_header.each{|line| ret << line } 350 ret << CRLF 351 ret << body if body 352 ret 353 end 354 355 ## 356 # Consumes any remaining body and updates keep-alive status 357 358 def fixup() # :nodoc: 359 begin 360 body{|chunk| } # read remaining body 361 rescue HTTPStatus::Error => ex 362 @logger.error("HTTPRequest#fixup: #{ex.class} occurred.") 363 @keep_alive = false 364 rescue => ex 365 @logger.error(ex) 366 @keep_alive = false 367 end 368 end 369 370 # This method provides the metavariables defined by the revision 3 371 # of "The WWW Common Gateway Interface Version 1.1" 372 # http://Web.Golux.Com/coar/cgi/ 373 374 def meta_vars 375 meta = Hash.new 376 377 cl = self["Content-Length"] 378 ct = self["Content-Type"] 379 meta["CONTENT_LENGTH"] = cl if cl.to_i > 0 380 meta["CONTENT_TYPE"] = ct.dup if ct 381 meta["GATEWAY_INTERFACE"] = "CGI/1.1" 382 meta["PATH_INFO"] = @path_info ? @path_info.dup : "" 383 #meta["PATH_TRANSLATED"] = nil # no plan to be provided 384 meta["QUERY_STRING"] = @query_string ? @query_string.dup : "" 385 meta["REMOTE_ADDR"] = @peeraddr[3] 386 meta["REMOTE_HOST"] = @peeraddr[2] 387 #meta["REMOTE_IDENT"] = nil # no plan to be provided 388 meta["REMOTE_USER"] = @user 389 meta["REQUEST_METHOD"] = @request_method.dup 390 meta["REQUEST_URI"] = @request_uri.to_s 391 meta["SCRIPT_NAME"] = @script_name.dup 392 meta["SERVER_NAME"] = @host 393 meta["SERVER_PORT"] = @port.to_s 394 meta["SERVER_PROTOCOL"] = "HTTP/" + @config[:HTTPVersion].to_s 395 meta["SERVER_SOFTWARE"] = @config[:ServerSoftware].dup 396 397 self.each{|key, val| 398 next if /^content-type$/i =~ key 399 next if /^content-length$/i =~ key 400 name = "HTTP_" + key 401 name.gsub!(/-/o, "_") 402 name.upcase! 403 meta[name] = val 404 } 405 406 meta 407 end 408 409 private 410 411 # :stopdoc: 412 413 MAX_URI_LENGTH = 2083 # :nodoc: 414 415 def read_request_line(socket) 416 @request_line = read_line(socket, MAX_URI_LENGTH) if socket 417 if @request_line.bytesize >= MAX_URI_LENGTH and @request_line[-1, 1] != LF 418 raise HTTPStatus::RequestURITooLarge 419 end 420 @request_time = Time.now 421 raise HTTPStatus::EOFError unless @request_line 422 if /^(\S+)\s+(\S++)(?:\s+HTTP\/(\d+\.\d+))?\r?\n/mo =~ @request_line 423 @request_method = $1 424 @unparsed_uri = $2 425 @http_version = HTTPVersion.new($3 ? $3 : "0.9") 426 else 427 rl = @request_line.sub(/\x0d?\x0a\z/o, '') 428 raise HTTPStatus::BadRequest, "bad Request-Line `#{rl}'." 429 end 430 end 431 432 def read_header(socket) 433 if socket 434 while line = read_line(socket) 435 break if /\A(#{CRLF}|#{LF})\z/om =~ line 436 @raw_header << line 437 end 438 end 439 @header = HTTPUtils::parse_header(@raw_header.join) 440 end 441 442 def parse_uri(str, scheme="http") 443 if @config[:Escape8bitURI] 444 str = HTTPUtils::escape8bit(str) 445 end 446 str.sub!(%r{\A/+}o, '/') 447 uri = URI::parse(str) 448 return uri if uri.absolute? 449 if @forwarded_host 450 host, port = @forwarded_host, @forwarded_port 451 elsif self["host"] 452 pattern = /\A(#{URI::REGEXP::PATTERN::HOST})(?::(\d+))?\z/n 453 host, port = *self['host'].scan(pattern)[0] 454 elsif @addr.size > 0 455 host, port = @addr[2], @addr[1] 456 else 457 host, port = @config[:ServerName], @config[:Port] 458 end 459 uri.scheme = @forwarded_proto || scheme 460 uri.host = host 461 uri.port = port ? port.to_i : nil 462 return URI::parse(uri.to_s) 463 end 464 465 def read_body(socket, block) 466 return unless socket 467 if tc = self['transfer-encoding'] 468 case tc 469 when /chunked/io then read_chunked(socket, block) 470 else raise HTTPStatus::NotImplemented, "Transfer-Encoding: #{tc}." 471 end 472 elsif self['content-length'] || @remaining_size 473 @remaining_size ||= self['content-length'].to_i 474 while @remaining_size > 0 475 sz = [@buffer_size, @remaining_size].min 476 break unless buf = read_data(socket, sz) 477 @remaining_size -= buf.bytesize 478 block.call(buf) 479 end 480 if @remaining_size > 0 && @socket.eof? 481 raise HTTPStatus::BadRequest, "invalid body size." 482 end 483 elsif BODY_CONTAINABLE_METHODS.member?(@request_method) 484 raise HTTPStatus::LengthRequired 485 end 486 return @body 487 end 488 489 def read_chunk_size(socket) 490 line = read_line(socket) 491 if /^([0-9a-fA-F]+)(?:;(\S+))?/ =~ line 492 chunk_size = $1.hex 493 chunk_ext = $2 494 [ chunk_size, chunk_ext ] 495 else 496 raise HTTPStatus::BadRequest, "bad chunk `#{line}'." 497 end 498 end 499 500 def read_chunked(socket, block) 501 chunk_size, = read_chunk_size(socket) 502 while chunk_size > 0 503 data = read_data(socket, chunk_size) # read chunk-data 504 if data.nil? || data.bytesize != chunk_size 505 raise BadRequest, "bad chunk data size." 506 end 507 read_line(socket) # skip CRLF 508 block.call(data) 509 chunk_size, = read_chunk_size(socket) 510 end 511 read_header(socket) # trailer + CRLF 512 @header.delete("transfer-encoding") 513 @remaining_size = 0 514 end 515 516 def _read_data(io, method, *arg) 517 begin 518 WEBrick::Utils.timeout(@config[:RequestTimeout]){ 519 return io.__send__(method, *arg) 520 } 521 rescue Errno::ECONNRESET 522 return nil 523 rescue TimeoutError 524 raise HTTPStatus::RequestTimeout 525 end 526 end 527 528 def read_line(io, size=4096) 529 _read_data(io, :gets, LF, size) 530 end 531 532 def read_data(io, size) 533 _read_data(io, :read, size) 534 end 535 536 def parse_query() 537 begin 538 if @request_method == "GET" || @request_method == "HEAD" 539 @query = HTTPUtils::parse_query(@query_string) 540 elsif self['content-type'] =~ /^application\/x-www-form-urlencoded/ 541 @query = HTTPUtils::parse_query(body) 542 elsif self['content-type'] =~ /^multipart\/form-data; boundary=(.+)/ 543 boundary = HTTPUtils::dequote($1) 544 @query = HTTPUtils::parse_form_data(body, boundary) 545 else 546 @query = Hash.new 547 end 548 rescue => ex 549 raise HTTPStatus::BadRequest, ex.message 550 end 551 end 552 553 PrivateNetworkRegexp = / 554 ^unknown$| 555 ^((::ffff:)?127.0.0.1|::1)$| 556 ^(::ffff:)?(10|172\.(1[6-9]|2[0-9]|3[01])|192\.168)\. 557 /ixo 558 559 # It's said that all X-Forwarded-* headers will contain more than one 560 # (comma-separated) value if the original request already contained one of 561 # these headers. Since we could use these values as Host header, we choose 562 # the initial(first) value. (apr_table_mergen() adds new value after the 563 # existing value with ", " prefix) 564 def setup_forwarded_info 565 if @forwarded_server = self["x-forwarded-server"] 566 @forwarded_server = @forwarded_server.split(",", 2).first 567 end 568 @forwarded_proto = self["x-forwarded-proto"] 569 if host_port = self["x-forwarded-host"] 570 host_port = host_port.split(",", 2).first 571 @forwarded_host, tmp = host_port.split(":", 2) 572 @forwarded_port = (tmp || (@forwarded_proto == "https" ? 443 : 80)).to_i 573 end 574 if addrs = self["x-forwarded-for"] 575 addrs = addrs.split(",").collect(&:strip) 576 addrs.reject!{|ip| PrivateNetworkRegexp =~ ip } 577 @forwarded_for = addrs.first 578 end 579 end 580 581 # :startdoc: 582 end 583end 584