| | 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 | # Allow up to ten blank lines before we get a real response line. |
|---|
| | 68 | # Allow no more than 100 lines in the header. |
|---|
| | 69 | # |
|---|
| | 70 | def receive_line ln |
|---|
| | 71 | if ln.length == 0 |
|---|
| | 72 | if @header_lines.length > 0 |
|---|
| | 73 | process_header |
|---|
| | 74 | else |
|---|
| | 75 | @blanks += 1 |
|---|
| | 76 | if @blanks > 10 |
|---|
| | 77 | @conn.close_connection |
|---|
| | 78 | end |
|---|
| | 79 | end |
|---|
| | 80 | else |
|---|
| | 81 | @header_lines << ln |
|---|
| | 82 | if @header_lines.length > 100 |
|---|
| | 83 | @internal_error = :bad_header |
|---|
| | 84 | @conn.close_connection |
|---|
| | 85 | end |
|---|
| | 86 | end |
|---|
| | 87 | end |
|---|
| | 88 | |
|---|
| | 89 | |
|---|
| | 90 | #-- |
|---|
| | 91 | # |
|---|
| | 92 | # |
|---|
| | 93 | HttpResponseRE = /\AHTTP\/(1.[01]) ([\d]{3})/i |
|---|
| | 94 | ClenRE = /\AContent-length:\s*(\d+)/i |
|---|
| | 95 | ColonRE = /\:\s*/ |
|---|
| | 96 | |
|---|
| | 97 | def process_header |
|---|
| | 98 | unless @header_lines.first =~ HttpResponseRE |
|---|
| | 99 | @conn.close_connection |
|---|
| | 100 | @internal_error = :bad_request |
|---|
| | 101 | end |
|---|
| | 102 | @version = $1.dup |
|---|
| | 103 | @status = $2.dup.to_i |
|---|
| | 104 | |
|---|
| | 105 | clen = nil |
|---|
| | 106 | @header_lines.each_with_index do |e,ix| |
|---|
| | 107 | if ix > 0 |
|---|
| | 108 | hdr,val = e.split(ColonRE) |
|---|
| | 109 | (@headers[hdr.downcase] ||= []) << val |
|---|
| | 110 | end |
|---|
| | 111 | if clen == nil and e =~ ClenRE |
|---|
| | 112 | clen = $1.dup.to_i |
|---|
| | 113 | end |
|---|
| | 114 | end |
|---|
| | 115 | |
|---|
| | 116 | if clen |
|---|
| | 117 | @conn.set_text_mode clen |
|---|
| | 118 | else |
|---|
| | 119 | # Chunked transfer, multipart, or end-of-connection. |
|---|
| | 120 | # For end-of-connection, we need to go the unbind |
|---|
| | 121 | # method and suppress its desire to fail us. |
|---|
| | 122 | p "NO CLEN" |
|---|
| | 123 | @internal_error = :unsupported_clen |
|---|
| | 124 | @conn.close_connection |
|---|
| | 125 | end |
|---|
| | 126 | end |
|---|
| | 127 | private :process_header |
|---|
| | 128 | |
|---|
| | 129 | #-- |
|---|
| | 130 | # At the present time, we only handle contents that have a length |
|---|
| | 131 | # specified by the content-length header. |
|---|
| | 132 | # |
|---|
| | 133 | def receive_text text |
|---|
| | 134 | @content = text |
|---|
| | 135 | @conn.pop_request |
|---|
| | 136 | succeed self |
|---|
| | 137 | end |
|---|
| | 138 | end |
|---|
| | 139 | |
|---|
| | 140 | # Make a connection to a remote HTTP server. |
|---|
| | 141 | # Can take either a pair of arguments (which will be interpreted as |
|---|
| | 142 | # a hostname/ip-address and a port), or a hash. |
|---|
| | 143 | # If the arguments are a hash, then supported values include: |
|---|
| | 144 | # :host => a hostname or ip-address; |
|---|
| | 145 | # :port => a port number |
|---|
| | 146 | #-- |
|---|
| | 147 | # TODO, support optional encryption arguments like :ssl |
|---|
| | 148 | def self.connect *args |
|---|
| | 149 | if args.length == 2 |
|---|
| | 150 | args = {:host=>args[0], :port=>args[1]} |
|---|
| | 151 | else |
|---|
| | 152 | args = args.first |
|---|
| | 153 | end |
|---|
| | 154 | |
|---|
| | 155 | h,prt,ssl = args[:host], Integer(args[:port]), (args[:tls] || args[:ssl]) |
|---|
| | 156 | conn = EM.connect( h, prt, self ) |
|---|
| | 157 | # TODO, start_tls if necessary |
|---|
| | 158 | conn.set_default_host_header( h, prt, ssl ) |
|---|
| | 159 | conn |
|---|
| | 160 | end |
|---|
| | 161 | |
|---|
| | 162 | |
|---|
| | 163 | #-- |
|---|
| | 164 | # Compute and remember a string to be used as the host header in HTTP requests |
|---|
| | 165 | # unless the user overrides it with an argument to #request. |
|---|
| | 166 | # |
|---|
| | 167 | def set_default_host_header host, port, ssl |
|---|
| | 168 | if (ssl and port != 443) or (!ssl and port != 80) |
|---|
| | 169 | @host_header = "#{host}:#{port}" |
|---|
| | 170 | else |
|---|
| | 171 | @host_header = host |
|---|
| | 172 | end |
|---|
| | 173 | end |
|---|
| | 174 | |
|---|
| | 175 | |
|---|
| | 176 | def post_init |
|---|
| | 177 | super |
|---|
| | 178 | @connected = EM::DefaultDeferrable.new |
|---|
| | 179 | end |
|---|
| | 180 | |
|---|
| | 181 | def connection_completed |
|---|
| | 182 | super |
|---|
| | 183 | @connected.succeed |
|---|
| | 184 | end |
|---|
| | 185 | |
|---|
| | 186 | #-- |
|---|
| | 187 | # All pending requests, if any, must fail. |
|---|
| | 188 | # We might come here without ever passing through connection_completed |
|---|
| | 189 | # in case we can't connect to the server. We'll also get here when the |
|---|
| | 190 | # connection closes (either because the server closes it, or we close it |
|---|
| | 191 | # due to detecting an internal error or security violation). |
|---|
| | 192 | # In either case, run down all pending requests, if any, and signal failure |
|---|
| | 193 | # on them. |
|---|
| | 194 | # |
|---|
| | 195 | # Set and remember a flag (@closed) so we can immediately fail any |
|---|
| | 196 | # subsequent requests. |
|---|
| | 197 | # |
|---|
| | 198 | def unbind |
|---|
| | 199 | super |
|---|
| | 200 | @closed = true |
|---|
| | 201 | (@requests || []).each {|r| r.fail r} |
|---|
| | 202 | end |
|---|
| | 203 | |
|---|
| | 204 | |
|---|
| | 205 | def get args |
|---|
| | 206 | if args.is_a?(String) |
|---|
| | 207 | args = {:uri=>args} |
|---|
| | 208 | end |
|---|
| | 209 | args[:verb] = "GET" |
|---|
| | 210 | request args |
|---|
| | 211 | end |
|---|
| | 212 | |
|---|
| | 213 | def request args |
|---|
| | 214 | args[:host_header] = @host_header unless args.has_key?(:host_header) |
|---|
| | 215 | r = Request.new self, args |
|---|
| | 216 | if @closed |
|---|
| | 217 | r.fail |
|---|
| | 218 | else |
|---|
| | 219 | (@requests ||= []).unshift r |
|---|
| | 220 | @connected.callback {r.send_request} |
|---|
| | 221 | end |
|---|
| | 222 | r |
|---|
| | 223 | end |
|---|
| | 224 | |
|---|
| | 225 | def receive_line ln |
|---|
| | 226 | @requests.last.receive_line ln |
|---|
| | 227 | end |
|---|
| | 228 | def receive_binary_data text |
|---|
| | 229 | @requests.last.receive_text text |
|---|
| | 230 | end |
|---|
| | 231 | |
|---|
| | 232 | #-- |
|---|
| | 233 | # Called by a Request object when it completes. |
|---|
| | 234 | # |
|---|
| | 235 | def pop_request |
|---|
| | 236 | @requests.pop |
|---|
| | 237 | end |
|---|
| | 238 | end |
|---|
| | 239 | |
|---|
| | 240 | |
|---|
| | 241 | =begin |
|---|
| | 242 | class HttpClient2x < Connection |
|---|