root/trunk/lib/protocols/httpcli2.rb

Revision 668, 21.3 kB (checked in by blackhedd, 1 year ago)

migrated version_0 to trunk

  • Property svn:keywords set to Id
Line 
1 # $Id$
2 #
3 # Author:: Francis Cianfrocca (gmail: blackhedd)
4 # Homepage::  http://rubyeventmachine.com
5 # Date:: 16 July 2006
6 #
7 # See EventMachine and EventMachine::Connection for documentation and
8 # usage examples.
9 #
10 #----------------------------------------------------------------------------
11 #
12 # Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
13 # Gmail: blackhedd
14 #
15 # This program is free software; you can redistribute it and/or modify
16 # it under the terms of either: 1) the GNU General Public License
17 # as published by the Free Software Foundation; either version 2 of the
18 # License, or (at your option) any later version; or 2) Ruby's License.
19 #
20 # See the file COPYING for complete licensing information.
21 #
22 #---------------------------------------------------------------------------
23 #
24 #
25
26
27
28 module EventMachine
29 module Protocols
30
31
32         class HttpClient2 < Connection
33                 include LineText2
34
35
36                 class Request
37                         include Deferrable
38
39                         attr_reader :version
40                         attr_reader :status
41                         attr_reader :header_lines
42                         attr_reader :headers
43                         attr_reader :content
44                         attr_reader :internal_error
45
46                         def initialize conn, args
47                                 @conn = conn
48                                 @args = args
49                                 @header_lines = []
50                                 @headers = {}
51                                 @blanks = 0
52                         end
53
54                         def send_request
55                                 az = @args[:authorization] and az = "Authorization: #{az}\r\n"
56
57                                 r = [
58                                         "#{@args[:verb]} #{@args[:uri]} HTTP/#{@args[:version] || "1.1"}\r\n",
59                                         "Host: #{@args[:host_header] || "_"}\r\n",
60                                         az || "",
61                                         "\r\n"
62                                 ]
63                                 @conn.send_data r.join
64                         end
65
66
67                         #--
68                         #
69                         def receive_line ln
70                                 if @chunk_trailer
71                                         receive_chunk_trailer(ln)
72                                 elsif @chunking
73                                         receive_chunk_header(ln)
74                                 else
75                                         receive_header_line(ln)
76                                 end
77                         end
78
79                         #--
80                         #
81                         def receive_chunk_trailer ln
82                                 if ln.length == 0
83                                         @conn.pop_request
84                                         succeed
85                                 else
86                                         p "Received chunk trailer line"
87                                 end
88                         end
89
90                         #--
91                         # Allow up to ten blank lines before we get a real response line.
92                         # Allow no more than 100 lines in the header.
93                         #
94                         def receive_header_line ln
95                                 if ln.length == 0
96                                         if @header_lines.length > 0
97                                                 process_header
98                                         else
99                                                 @blanks += 1
100                                                 if @blanks > 10
101                                                         @conn.close_connection
102                                                 end
103                                         end
104                                 else
105                                         @header_lines << ln
106                                         if @header_lines.length > 100
107                                                 @internal_error = :bad_header
108                                                 @conn.close_connection
109                                         end
110                                 end
111                         end
112
113                         #--
114                         # Cf RFC 2616 pgh 3.6.1 for the format of HTTP chunks.
115                         #
116                         def receive_chunk_header ln
117                                 if ln.length > 0
118                                         chunksize = ln.to_i(16)
119                                         if chunksize > 0
120                                                 @conn.set_text_mode(ln.to_i(16))
121                                         else
122                                                 @content = @content.join
123                                                 @chunk_trailer = true
124                                         end
125                                 else
126                                         # We correctly come here after each chunk gets read.
127                                         p "Got A BLANK chunk line"
128                                 end
129
130                         end
131
132
133                         #--
134                         # We get a single chunk. Append it to the incoming content and switch back to line mode.
135                         #
136                         def receive_chunked_text text
137                                 p "RECEIVED #{text.length} CHUNK"
138                                 (@content ||= []) << text
139                         end
140
141
142                         #--
143                         # TODO, inefficient how we're handling this. Part of it is done so as to
144                         # make sure we don't have problems in detecting chunked-encoding, content-length,
145                         # etc.
146                         #
147                         #
148                         HttpResponseRE = /\AHTTP\/(1.[01]) ([\d]{3})/i
149                         ClenRE = /\AContent-length:\s*(\d+)/i
150                         ChunkedRE = /\ATransfer-encoding:\s*chunked/i
151                         ColonRE = /\:\s*/
152
153                         def process_header
154                                 unless @header_lines.first =~ HttpResponseRE
155                                         @conn.close_connection
156                                         @internal_error = :bad_request
157                                 end
158                                 @version = $1.dup
159                                 @status = $2.dup.to_i
160
161                                 clen = nil
162                                 chunks = nil
163                                 @header_lines.each_with_index do |e,ix|
164                                         if ix > 0
165                                                 hdr,val = e.split(ColonRE,2)
166                                                 (@headers[hdr.downcase] ||= []) << val
167                                         end
168
169                                         if clen == nil and e =~ ClenRE
170                                                 clen = $1.dup.to_i
171                                         end
172                                         if e =~ ChunkedRE
173                                                 chunks = true
174                                         end
175                                 end
176
177                                 if clen
178                                         @conn.set_text_mode clen
179                                 elsif chunks
180                                         @chunking = true
181                                 else
182                                         # Chunked transfer, multipart, or end-of-connection.
183                                         # For end-of-connection, we need to go the unbind
184                                         # method and suppress its desire to fail us.
185                                         p "NO CLEN"
186                                         p @args[:uri]
187                                         p @header_lines
188                                         @internal_error = :unsupported_clen
189                                         @conn.close_connection
190                                 end
191                         end
192                         private :process_header
193
194
195                         def receive_text text
196                                 @chunking ? receive_chunked_text(text) : receive_sized_text(text)
197                         end
198
199                         #--
200                         # At the present time, we only handle contents that have a length
201                         # specified by the content-length header.
202                         #
203                         def receive_sized_text text
204                                 @content = text
205                                 @conn.pop_request
206                                 succeed
207                         end
208                 end
209
210                 # Make a connection to a remote HTTP server.
211                 # Can take either a pair of arguments (which will be interpreted as
212                 # a hostname/ip-address and a port), or a hash.
213                 # If the arguments are a hash, then supported values include:
214                 #  :host => a hostname or ip-address;
215                 #  :port => a port number
216                 #--
217                 # TODO, support optional encryption arguments like :ssl
218                 def self.connect *args
219                         if args.length == 2
220                                 args = {:host=>args[0], :port=>args[1]}
221                         else
222                                 args = args.first
223                         end
224
225                         h,prt,ssl = args[:host], Integer(args[:port]), (args[:tls] || args[:ssl])
226                         conn = EM.connect( h, prt, self )
227                         # TODO, start_tls if necessary
228                         conn.set_default_host_header( h, prt, ssl )
229                         conn
230                 end
231
232
233                 #--
234                 # Compute and remember a string to be used as the host header in HTTP requests
235                 # unless the user overrides it with an argument to #request.
236                 #
237                 def set_default_host_header host, port, ssl
238                         if (ssl and port != 443) or (!ssl and port != 80)
239                                 @host_header = "#{host}:#{port}"
240                         else
241                                 @host_header = host
242                         end
243                 end
244
245
246                 def post_init
247                         super
248                         @connected = EM::DefaultDeferrable.new
249                 end
250
251                 def connection_completed
252                         super
253                         @connected.succeed
254                 end
255
256                 #--
257                 # All pending requests, if any, must fail.
258                 # We might come here without ever passing through connection_completed
259                 # in case we can't connect to the server. We'll also get here when the
260                 # connection closes (either because the server closes it, or we close it
261                 # due to detecting an internal error or security violation).
262                 # In either case, run down all pending requests, if any, and signal failure
263                 # on them.
264                 #
265                 # Set and remember a flag (@closed) so we can immediately fail any
266                 # subsequent requests.
267                 #
268                 def unbind
269                         super
270                         @closed = true
271                         (@requests || []).each {|r| r.fail}
272                 end
273
274
275                 def get args
276                         if args.is_a?(String)
277                                 args = {:uri=>args}
278                         end
279                         args[:verb] = "GET"
280                         request args
281                 end
282
283                 def post args
284                         if args.is_a?(String)
285                                 args = {:uri=>args}
286                         end
287                         args[:verb] = "POST"
288                         request args
289                 end
290
291                 def request args
292                         args[:host_header] = @host_header unless args.has_key?(:host_header)
293                         args[:authorization] = @authorization unless args.has_key?(:authorization)
294                         r = Request.new self, args
295                         if @closed
296                                 r.fail
297                         else
298                                 (@requests ||= []).unshift r
299                                 @connected.callback {r.send_request}
300                         end
301                         r
302                 end
303
304                 def receive_line ln
305                         if req = @requests.last
306                                 req.receive_line ln
307                         else
308                                 p "??????????"
309                                 p ln
310                         end
311
312                 end
313                 def receive_binary_data text
314                         @requests.last.receive_text text
315                 end
316
317                 #--
318                 # Called by a Request object when it completes.
319                 #
320                 def pop_request
321                         @requests.pop
322                 end
323         end
324
325
326 =begin
327         class HttpClient2x < Connection
328                 include LineText2
329
330                 # TODO: Make this behave appropriate in case a #connect fails.
331                 # Currently, this produces no errors.
332
333                 # Make a connection to a remote HTTP server.
334                 # Can take either a pair of arguments (which will be interpreted as
335                 # a hostname/ip-address and a port), or a hash.
336                 # If the arguments are a hash, then supported values include:
337                 #  :host => a hostname or ip-address;
338                 #  :port => a port number
339                 #--
340                 # TODO, support optional encryption arguments like :ssl
341                 def self.connect *args
342                         if args.length == 2
343                                 args = {:host=>args[0], :port=>args[1]}
344                         else
345                                 args = args.first
346                         end
347
348                         h,prt = args[:host],Integer(args[:port])
349                         EM.connect( h, prt, self, h, prt )
350                 end
351
352
353                 #--
354                 # Sugars a connection that makes a single request and then
355                 # closes the connection. Matches the behavior and the arguments
356                 # of the original implementation of class HttpClient.
357                 #
358                 # Intended primarily for back compatibility, but the idiom
359                 # is probably useful so it's not deprecated.
360                 # We return a Deferrable, as did the original implementation.
361                 #
362                 # Because we're improving the way we deal with errors and exceptions
363                 # (specifically, HTTP response codes other than 2xx will trigger the
364                 # errback rather than the callback), this may break some existing code.
365                 #
366                 def self.request args
367                         c = connect args
368                 end
369
370                 #--
371                 # Requests can be pipelined. When we get a request, add it to the
372                 # front of a queue as an array. The last element of the @requests
373                 # array is always the oldest request received. Each element of the
374                 # @requests array is a two-element array consisting of a hash with
375                 # the original caller's arguments, and an initially-empty Ostruct
376                 # containing the data we retrieve from the server's response.
377                 # Maintain the instance variable @current_response, which is the response
378                 # of the oldest pending request. That's just to make other code a little
379                 # easier. If the variable doesn't exist when we come here, we're
380                 # obviously the first request being made on the connection.
381                 #
382                 # The reason for keeping this method private (and requiring use of the
383                 # convenience methods #get, #post, #head, etc) is to avoid the small
384                 # performance penalty of canonicalizing the verb.
385                 #
386                 def request args
387                         d = EventMachine::DefaultDeferrable.new
388
389                         if @closed
390                                 d.fail
391                                 return d
392                         end
393
394                         o = OpenStruct.new
395                         o.deferrable = d
396                         (@requests ||= []).unshift [args, o]
397                         @current_response ||= @requests.last.last
398                         @connected.callback {
399                                 az = args[:authorization] and az = "Authorization: #{az}\r\n"
400
401                                 r = [
402                                         "#{args[:verb]} #{args[:uri]} HTTP/#{args[:version] || "1.1"}\r\n",
403                                         "Host: #{args[:host_header] || @host_header}\r\n",
404                                         az || "",
405                                         "\r\n"
406                                 ]
407                                 p r
408                                 send_data r.join
409                         }
410                         o.deferrable
411                 end
412                 private :request
413
414                 def get args
415                         if args.is_a?(String)
416                                 args = {:uri=>args}
417                         end
418                         args[:verb] = "GET"
419                         request args
420                 end
421
422                 def initialize host, port
423                         super
424                         @host_header = "#{host}:#{port}"
425                 end
426                 def post_init
427                         super
428                         @connected = EM::DefaultDeferrable.new
429                 end
430
431
432                 def connection_completed
433                         super
434                         @connected.succeed
435                 end
436
437                 #--
438                 # Make sure to throw away any leftover incoming data if we've
439                 # been closed due to recognizing an error.
440                 #
441                 # Generate an internal error if we get an unreasonable number of
442                 # header lines. It could be malicious.
443                 #
444                 def receive_line ln
445                         p ln
446                         return if @closed
447
448                         if ln.length > 0
449                                 (@current_response.headers ||= []).push ln
450                                 abort_connection if @current_response.headers.length > 100
451                         else
452                                 process_received_headers
453                         end
454                 end
455
456                 #--
457                 # We come here when we've seen all the headers for a particular request.
458                 # What we do next depends on the response line (which should be the
459                 # first line in the header set), and whether there is content to read.
460                 # We may transition into a text-reading state to read content, or
461                 # we may abort the connection, or we may go right back into parsing
462                 # responses for the next response in the chain.
463                 #
464                 # We make an ASSUMPTION that the first line is an HTTP response.
465                 # Anything else produces an error that aborts the connection.
466                 # This may not be enough, because it may be that responses to pipelined
467                 # requests will come with a blank-line delimiter.
468                 #
469                 # Any non-2xx response will be treated as a fatal error, and abort the
470                 # connection. We will set up the status and other response parameters.
471                 # TODO: we will want to properly support 1xx responses, which some versions
472                 # of IIS copiously generate.
473                 # TODO: We need to give the option of not aborting the connection with certain
474                 # non-200 responses, in order to work with NTLM and other authentication
475                 # schemes that work at the level of individual connections.
476                 #
477                 # Some error responses will get sugarings. For example, we'll return the
478                 # Location header in the response in case of a 301/302 response.
479                 #
480                 # Possible dispositions here:
481                 # 1) No content to read (either content-length is zero or it's a HEAD request);
482                 # 2) Switch to text mode to read a specific number of bytes;
483                 # 3) Read a chunked or multipart response;
484                 # 4) Read till the server closes the connection.
485                 #
486                 # Our reponse to the client can be either to wait till all the content
487                 # has been read and then to signal caller's deferrable, or else to signal
488                 # it when we finish the processing the headers and then expect the caller
489                 # to have given us a block to call as the content comes in. And of course
490                 # the latter gets stickier with chunks and multiparts.
491                 #
492                 HttpResponseRE = /\AHTTP\/(1.[01]) ([\d]{3})/i
493                 ClenRE = /\AContent-length:\s*(\d+)/i
494                 def process_received_headers
495                         abort_connection unless @current_response.headers.first =~ HttpResponseRE
496                         @current_response.version = $1.dup
497                         st = $2.dup
498                         @current_response.status = st.to_i
499                         abort_connection unless st[0,1] == "2"
500
501                         clen = nil
502                         @current_response.headers.each do |e|
503                                 if clen == nil and e =~ ClenRE
504                                         clen = $1.dup.to_i
505                                 end
506                         end
507
508                         if clen
509                                 set_text_mode clen
510                         end
511                 end
512                 private :process_received_headers
513
514
515                 def receive_binary_data text
516                         @current_response.content = text
517                         @current_response.deferrable.succeed @current_response
518                         @requests.pop
519                         @current_response = (@requests.last || []).last
520                         set_line_mode
521                 end
522
523
524
525                 # We've received either a server error or an internal error.
526                 # Close the connection and abort any pending requests.
527                 #--
528                 # When should we call close_connection? It will cause #unbind
529                 # to be fired. Should the user expect to see #unbind before
530                 # we call #receive_http_error, or the other way around?
531                 #
532                 # Set instance variable @closed. That's used to inhibit further
533                 # processing of any inbound data after an error has been recognized.
534                 #
535                 # We shouldn't have to worry about any leftover outbound data,
536                 # because we call close_connection (not close_connection_after_writing).
537                 # That ensures that any pipelined requests received after an error
538                 # DO NOT get streamed out to the server on this connection.
539                 # Very important. TODO, write a unit-test to establish that behavior.
540                 #
541                 def abort_connection
542                         close_connection
543                         @closed = true
544                         @current_response.deferrable.fail( @current_response )
545                 end
546
547
548                 #------------------------
549                 # Below here are user-overridable methods.
550
551         end
552 =end
553 end
554 end
555
556
557 =begin
558 module EventMachine
559 module Protocols
560
561 class HttpClient < Connection
562   include EventMachine::Deferrable
563
564
565     MaxPostContentLength = 20 * 1024 * 1024
566
567   # USAGE SAMPLE:
568   #
569   # EventMachine.run {
570   #   http = EventMachine::Protocols::HttpClient.request(
571   #     :host => server,
572   #     :port => 80,
573   #     :request => "/index.html",
574   #     :query_string => "parm1=value1&parm2=value2"
575   #   )
576   #   http.callback {|response|
577   #     puts response[:status]
578   #     puts response[:headers]
579   #     puts response[:content]
580   #   }
581   # }
582   #
583
584   # TODO:
585   # Add streaming so we can support enormous POSTs. Current max is 20meg.
586   # Timeout for connections that run too long or hang somewhere in the middle.
587   # Persistent connections (HTTP/1.1), may need a associated delegate object.
588   # DNS: Some way to cache DNS lookups for hostnames we connect to. Ruby's
589   # DNS lookups are unbelievably slow.
590   # HEAD requests.
591   # Chunked transfer encoding.
592   # Convenience methods for requests. get, post, url, etc.
593   # SSL.
594   # Handle status codes like 304, 100, etc.
595   # Refactor this code so that protocol errors all get handled one way (an exception?),
596   # instead of sprinkling set_deferred_status :failed calls everywhere.
597
598   def self.request( args = {} )
599     args[:port] ||= 80
600     EventMachine.connect( args[:host], args[:port], self ) {|c|
601       # According to the docs, we will get here AFTER post_init is called.
602       c.instance_eval {@args = args}
603     }
604   end
605
606   def post_init
607     @start_time = Time.now
608     @data = ""
609     @read_state = :base
610   end
611
612   # We send the request when we get a connection.
613   # AND, we set an instance variable to indicate we passed through here.
614   # That allows #unbind to know whether there was a successful connection.
615   # NB: This naive technique won't work when we have to support multiple
616   # requests on a single connection.
617   def connection_completed
618     @connected = true
619     send_request @args
620   end
621
622   def send_request args
623     args[:verb] ||= args[:method] # Support :method as an alternative to :verb.
624     args[:verb] ||= :get # IS THIS A GOOD IDEA, to default to GET if nothing was specified?
625
626     verb = args[:verb].to_s.upcase
627     unless ["GET", "POST", "PUT", "DELETE", "HEAD"].include?(verb)
628       set_deferred_status :failed, {:status => 0} # TODO, not signalling the error type
629       return # NOTE THE EARLY RETURN, we're not sending any data.
630     end
631
632     request = args[:request] || "/"
633     unless request[0,1] == "/"
634       request = "/" + request
635     end
636
637     qs = args[:query_string] || ""
638     if qs.length > 0 and qs[0,1] != '?'
639       qs = "?" + qs
640     end
641
642     # Allow an override for the host header if it's not the connect-string.
643     host = args[:host_header] || args[:host] || "_"
644     # For now, ALWAYS tuck in the port string, although we may want to omit it if it's the default.
645     port = args[:port]
646
647     # POST items.
648     postcontenttype = args[:contenttype] || "application/octet-stream"
649     postcontent = args[:content] || ""
650     raise "oversized content in HTTP POST" if postcontent.length > MaxPostContentLength
651
652     # ESSENTIAL for the request's line-endings to be CRLF, not LF. Some servers misbehave otherwise.
653     # TODO: We ASSUME the caller wants to send a 1.1 request. May not be a good assumption.
654     req = [
655       "#{verb} #{request}#{qs} HTTP/1.1",
656       "Host: #{host}:#{port}",
657       "User-agent: Ruby EventMachine",
658     ]
659
660     if verb == "POST" || verb == "PUT"
661       req << "Content-type: #{postcontenttype}"
662       req << "Content-length: #{postcontent.length}"
663     end
664
665     # TODO, this cookie handler assumes it's getting a single, semicolon-delimited string.
666     # Eventually we will want to deal intelligently with arrays and hashes.
667     if args[:cookie]
668       req << "Cookie: #{args[:cookie]}"
669     end
670
671     req << ""
672     reqstring = req.map {|l| "#{l}\r\n"}.join
673     send_data reqstring
674
675     if verb == "POST" || verb == "PUT"
676       send_data postcontent
677     end
678   end
679
680
681   def receive_data data
682     while data and data.length > 0
683       case @read_state
684       when :base
685         # Perform any per-request initialization here and don't consume any data.
686         @data = ""
687         @headers = []
688         @content_length = nil # not zero
689         @content = ""
690         @status = nil
691         @read_state = :header
692       when :header
693         ary = data.split( /\r?\n/m, 2 )
694         if ary.length == 2
695           data = ary.last
696           if ary.first == ""
697               if @content_length and @content_length > 0
698                   @read_state = :content
699               else
700                   dispatch_response
701                   @read_state = :base
702               end
703           else
704             @headers << ary.first
705             if @headers.length == 1
706               parse_response_line
707             elsif ary.first =~ /\Acontent-length:\s*/i
708               # Only take the FIRST content-length header that appears,
709               # which we can distinguish because @content_length is nil.
710               # TODO, it's actually a fatal error if there is more than one
711               # content-length header, because the caller is presumptively
712               # a bad guy. (There is an exploit that depends on multiple
713               # content-length headers.)
714               @content_length ||= $'.to_i
715             end
716           end
717         else
718           @data << data
719           data = ""
720         end
721       when :content
722         # If there was no content-length header, we have to wait until the connection
723         # closes. Everything we get until that point is content.
724         # TODO: Must impose a content-size limit, and also must implement chunking.
725         # Also, must support either temporary files for large content, or calling
726         # a content-consumer block supplied by the user.
727         if @content_length
728           bytes_needed = @content_length - @content.length
729           @content += data[0, bytes_needed]
730           data = data[bytes_needed..-1] || ""
731           if @content_length == @content.length
732             dispatch_response
733             @read_state = :base
734           end
735         else
736           @content << data
737           data = ""
738         end
739       end
740     end
741   end
742
743
744   # We get called here when we have received an HTTP response line.
745   # It's an opportunity to throw an exception or trigger other exceptional
746   # handling.
747   def parse_response_line
748     if @headers.first =~ /\AHTTP\/1\.[01] ([\d]{3})/
749       @status = $1.to_i
750     else
751       set_deferred_status :failed, {
752         :status => 0 # crappy way of signifying an unrecognized response. TODO, find a better way to do this.
753       }
754       close_connection
755     end
756   end
757   private :parse_response_line
758
759   def dispatch_response
760     @read_state = :base
761     set_deferred_status :succeeded, {
762       :content => @content,
763       :headers => @headers,
764       :status => @status
765     }
766     # TODO, we close the connection for now, but this is wrong for persistent clients.
767     close_connection
768   end
769
770   def unbind
771     if !@connected
772       set_deferred_status :failed, {:status => 0} # YECCCCH. Find a better way to signal no-connect/network error.
773     elsif (@read_state == :content and @content_length == nil)
774       dispatch_response
775     end
776   end
777 end
778
779
780 end
781 end
782
783 =end
784
Note: See TracBrowser for help on using the browser.