├── .gitignore ├── Gemfile ├── README.markdown ├── Rakefile ├── design.markdown ├── lib └── net2 │ ├── backports.rb │ ├── http.rb │ ├── http │ ├── generic_request.rb │ ├── header.rb │ ├── readers.rb │ ├── request.rb │ ├── response.rb │ ├── statuses.rb │ └── version.rb │ ├── https.rb │ └── protocol.rb ├── net-http.gemspec └── test ├── http_test_base.rb ├── openssl ├── envutil.rb └── utils.rb ├── test_buffered_io.rb ├── test_http.rb ├── test_http_chunked.rb ├── test_http_gzip.rb ├── test_httpheader.rb ├── test_httpresponse.rb ├── test_https.rb ├── test_https_proxy.rb ├── test_reader.rb ├── utils.rb └── webrick └── utils.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in net-http.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | This repo contains a number of experimental modifications to the version of Net::HTTP that ships with Ruby 1.9. TODOs: 2 | 3 | * Porting the code to work on Ruby 1.8 4 | * Fixed gzip in GET requests when using a block as iterator 5 | * Support for gzip and deflate responses from any request 6 | * Support for incremental gzip and deflate with chunked encoding 7 | * Support for leaving the socket open when making a request, so 8 | it's possible to make requests that do not block on the body being 9 | returned 10 | * Cleaned up tests so that it's possible to combine tests for 11 | features, instead of using brittle checks for specific classes 12 | * Support for Net::HTTP.get(path), instead of needing to pass a 13 | URI or deconstruct the URL yourself 14 | * In keepalive situations, make sure to read any remaining body from the 15 | socket before initiating a new connection. 16 | * Support for partial reads from the response 17 | * Clean up the semantics of when #body can be called after a request. 18 | Specifically, if a block form is used, decide whether to buffer the 19 | outputted String and make it available as #body or to leave it up 20 | to the consumer to store a String if they want one. Either way, 21 | fomalize and document the semantics. 22 | * The body method should never return an Adapter. It should either 23 | return a String or nil (if well-defined semantics justify a nil) 24 | * Support for `read_nonblock ` to BufferedIO. This simply proxies to the 25 | underlying `read_nonblock` and make it easy to support HTTP-level 26 | `read_nonblock` 27 | * Support for `read_nonblock` on Net2::HTTP::Response 28 | * Other features as I think of them 29 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | 4 | desc "run the tests" 5 | task :test do 6 | $:.unshift "lib" 7 | $:.unshift "test" 8 | 9 | require "openssl/utils" 10 | 11 | Dir["test/test_*.rb"].each do |file| 12 | require file[%r{test/(.*)\.rb}, 1] 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /design.markdown: -------------------------------------------------------------------------------- 1 | Net::HTTP, as shipped with the Ruby standard library, is a robust, 2 | battle-tested HTTP library. As it stands today (Ruby 1.9.2), it already 3 | contains a number of very important, non-trivial features: 4 | 5 | * Support for keepalive and proper handling of sockets that 6 | inadvertantly close when keepalive is expected 7 | * Support for Transfer-Encoding: Chunked 8 | * Support for streaming request bodies 9 | * Support for gzip and deflate on GET request 10 | * Support for threaded operation 11 | * Proper handling of sockets: the normal Net::HTTP APIs make it 12 | extremely unlikely that a user of the API will leak sockets 13 | * Support for streaming multipart/form-data request 14 | * Support for application/x-www-form-urlencoded 15 | * Support for SSL and SSPI 16 | * Support for proxies 17 | * Support for Basic Auth 18 | 19 | In general, the API is more reasonable than most people think. Most 20 | obviously, the block-based API that is the normal way of using Net::HTTP 21 | ensures that resources are properly closed, while still allowing 22 | multiple Keepalive'd requests on the same connection: 23 | 24 | # open a socket to the host and port passed in 25 | Net::HTTP.start(host, port) do |http| 26 | http.get(path) do |chunk| # use the socket held by the Net::HTTP instance 27 | # do something with chunks 28 | end # guarantees that the socket position is after the response body 29 | 30 | http.get(path) do |chunk| # use the same socket via keepalive 31 | # note that if the server sent Connection: close 32 | # or the socket inadvertantly got disconnected, 33 | # Net::HTTP will reopen the socket for you 34 | end 35 | end 36 | # close the socket automatically via +ensure+ 37 | 38 | The API is roughly analogous to the File API, which provides a block API 39 | in order to allow Ruby to automatically manage resources for you. It's 40 | also why the API takes a host and port, rather than a full path (sockets 41 | are opened to a particular host and port). 42 | 43 | Net::HTTP also provides a convenience method (analogous to File.read) if 44 | you just want to make a quick GET request a la curl: 45 | 46 | body = Net::HTTP.get(host, path, port) 47 | 48 | # if you want a response object: 49 | 50 | response = Net::HTTP.get_response(host, path, port) 51 | 52 | Unfortunately, Net::HTTP does not support `Net::HTTP.get(string_url)`, 53 | but that is easily remedied, and was one of the first things I did in 54 | Net2::HTTP. I have a pending patch to Net::HTTP proper. 55 | 56 | Additionally, if you use Net::HTTP without the block form, you get an 57 | instance of Net::HTTP with a live socket that will stay open until you 58 | manually close it. This allows you to distribute keepalive'd requests 59 | across several methods, and not need to worry about manually checking 60 | whether the socket stayed open. On the other hand, you will need to 61 | manually close the socket once you're done with it. 62 | 63 | This is the reason for the two APIs (block and no-block). Just like File 64 | provides a `&block` version of File.open to handle resource cleanup for you 65 | and a no-block version for when you want to manually manage the 66 | resource, Net::HTTP provides APIs for each side of that same tradeoff. 67 | 68 | # Deficiencies 69 | 70 | With all of that said, there are a few deficiencies of Net::HTTP. Fixing 71 | them would make it even better. In most cases, these deficiencies would 72 | not be addressed by switching to another HTTP library, because they are 73 | issues with features not fully supported in most other Ruby HTTP 74 | clients. 75 | 76 | In some cases, these issues could be readily addressed with a small 77 | patch to Net::HTTP. In others, the patch would be larger or more 78 | involved. When appropriate, I will submit small incremental patches back 79 | to Ruby, and will also periodically submit the entire Net2::HTTP project 80 | as a patch back against master. As you will see, some of the more 81 | involved changes may be challenging for ruby-core to accept, and I 82 | wanted to demonstrate their viability and make them available even if 83 | they could not make it into the core. 84 | 85 | ## Problem 1: Ruby 1.8 Support 86 | 87 | By definition, Net::HTTP is a library that is pinned to the version of 88 | Ruby it ships with. As a result, API improvements that do not rely on 89 | new Ruby features still do not make it into Ruby 1.8. 90 | 91 | The way to solve this problem is the same way that normal Rubygems do; 92 | ship a single version of the library that can support both Ruby 1.8 and 93 | Ruby 1.9. In the case of Net::HTTP, that means shimming 94 | encoding-specific changes and dealing with cases where Ruby 1.9 has much 95 | better support for non-blocking IO. 96 | 97 | That said, many of the changes between 1.8 and 1.9 are entangled with 98 | code that uses new Ruby 1.9 syntax features (like the new `{foo: bar}` 99 | Hash syntax), so reverting those changes is most of the necessary work. 100 | 101 | **Status:** Net2::HTTP is a modified version of Ruby 1.9.2's Net::HTTP, 102 | and passes all tests on Ruby 1.8. 103 | 104 | **Patch Potential:** A patch like this would conflict with the way that 105 | Ruby core handles the standard library in general. That said, Rubygems, 106 | to some degree, has this property, so it's worth asking. 107 | 108 | ## Problem 2: GZip Support is Limited 109 | 110 | Support for gzip in Net::HTTP was added between 1.8 and 1.9, and was 111 | tacked on in one specific place. Instead of making Net::HTTP's response 112 | object understand gzip, the code overrides `Net::HTTP#get` to modify the 113 | request with the proper Accept-Encoding, then runs the request through 114 | the stack and unzips the contents. 115 | 116 | This has several problems: 117 | 118 | 1. It only works with `Net::HTTP#get`. Using `Net::HTTP#request` 119 | directly, passing `get` as a parameter will bypass this logic. 120 | 2. Since it's not part of the Response object, the code cannot support 121 | the block form (`http.get(path) { |chunk| ... }`). Net::HTTP 122 | internally creates an object called a `ReadAdapter`, which duck-types 123 | like `String` well enough to receive `<<`es and yield them to the 124 | block. It cannot, however, serve as an IO for `GzipReader`. 125 | 3. Since the user doesn't necessarily know that gzip is being used (the 126 | whole idea is that it's transparent and will "just work" when 127 | available), this means that using a perfectly normal Net::HTTP API 128 | will break when the server sends the data back in gzip format. 129 | 130 | In solving this problem, ideally you would want to be able to use the 131 | block form of `#get` and still receive chunks of ungzipped data as they 132 | become available. 133 | 134 | Making `Net::HTTP::Response` aware of compression solves both problems: 135 | it makes all methods transparently compression-aware (because it would be 136 | encapsulated in `Response`) and it would allow the block form of `get` 137 | to work (because it would be wired tightly into the mechanics of `Response` 138 | and not to the mechanics of the `#get` public API method). 139 | 140 | Unfortunately, when I started working on wiring it into `Response`, I 141 | ran into a bit of a sticky problem. Because of the semantics of chunked 142 | encoding, it is not possible to simply pass the socket to `GzipReader` 143 | to unzip the contents. The version that comes with Ruby 1.9 gets around 144 | this problem by simply waiting for the entire body to be processed and 145 | then performs a switcheroo from the `get` method. Unfortunately, it has 146 | the problems outlined above. 147 | 148 | What I needed was a version of `GzipReader` that behaved more like 149 | `Zlib::Inflate`, which takes chunks as they become available and returns 150 | the decompressed content for the chunk. It's almost possible to just use 151 | `Zlib::Inflate`, except that gzip has a complex header that must be 152 | stripped before inflating the chunks. Thankfully, the Rubinius 153 | implementation of GzipReader implements the header parsing logic in 154 | pure-Ruby, so I was able to extract it and implement an object with the 155 | `Zlib::Inflate` API that knew how to handle headers. 156 | 157 | Since `deflate` is also supported, having a gzip parser with the same 158 | API as `Zlib::Inflate` also simplified the implementation in the 159 | `Response` object. 160 | 161 | **Status:** Done. Net2::HTTP includes gzip and deflate logic directly in 162 | the `Response` object, so any response that indicates that it is 163 | compressed will be properly decompressed. Additionally, since it's wired 164 | into the mechanism that handles block arguments to the public-facing 165 | API, consumers of the API will receive decompressed chunks as they 166 | become available. 167 | 168 | **Patch Potential**: It would probably be possible to implement this as 169 | a standalone patch. It requires a new implementation of some gzip logic, 170 | and it also causes a divergence in some of the other work I am doing. It 171 | will be difficult to truly decompose all of the work in the area of 172 | streaming responses so that ruby-core can pick and choose from them at 173 | will. I will try. 174 | 175 | ## Problem 3: Forced Waiting on the Body 176 | 177 | (Incidentally, this problem is what caused me to start working on 178 | Net::HTTP in the first place). 179 | 180 | In order to guarantee that the socket is always cleaned up, and that 181 | keepalive'd connections can make new requests, the current Net::HTTP 182 | always reads in the entire body before it returns. This means that it is 183 | not possible to make a long-running request and move on until you 184 | actually need the body, or pass along the response to another part of 185 | the program that will handle it. 186 | 187 | It is possible to work around this problem using threads, and this is 188 | what we did at Strobe when we encountered this issue. Instead of making 189 | the request in the current thread, spawn a thread to make the request 190 | and have it communicate the body back on a Queue or other structure. 191 | Howeer, requiring a thread is not an ideal solution. 192 | 193 | Additionally, I plan to add support to Net::HTTP for non-blocking reads 194 | (see below for a longer description of this problem), which is 195 | incompatible with forcibly blocking on reading the entire request. 196 | 197 | Unfortunately, there are some very good reasons for always reading the 198 | entire body. The best reason is that it allows Net::HTTP to guarantee 199 | that your program doesn't leak sockets. In my view, this problem is 200 | handled extremely well by the `File` API, which uses a block form to 201 | guarantee manual socket cleanup, and a non-block form to give you more 202 | control but require you to clean up resources yourself. 203 | 204 | Net::HTTP already provides the necessary APIs, but it still tries to 205 | save you the cleanup cost by zealously reading in the body when it can. 206 | 207 | My solution is to modify Net::HTTP to leave the socket open and unread 208 | unless you use the block form: 209 | 210 | # don't use the block form so the connection will not be auto-closed 211 | http = Net::HTTP.new(host, port).start 212 | 213 | # don't use the block form so that the body will not be eagerly 214 | # read, and the socket will stay open 215 | response = http.get(path) 216 | 217 | something_else(response) 218 | 219 | There are two caveats: 220 | 221 | 1. This is definitely a semantic change with existing Net::HTTP. I think 222 | it's a very reasonable one, but existing programs that don't use the 223 | block form would now rely on the garbage collector to close the 224 | socket. 225 | 2. A second connection on a keepalive'd socket needs to read the body of 226 | the previous connection in order to be able to reuse the socket. This 227 | isn't a major problem, but it's a logical limitation. 228 | 229 | **Status:** Net2::HTTP has these semantics. I have not yet added a 230 | non-blocking read API, but this change makes it possible to conceptually 231 | add one. 232 | 233 | **Patch Potential:** This is a significant semantic change that is also 234 | a bit invasive. It would both be difficult to make it a standalone patch 235 | and unlikely to be accepted. I will discuss it with ruby-core before 236 | attmpting to extract a patch. 237 | 238 | ## Problem 4: Nonblocking Reads 239 | 240 | The current design of Net::HTTP is a blocking design. You make a request 241 | and it blocks until the full body has returned. Even with my solution to 242 | problem 3 above, retrieving the body is a one-shot blocking operation. 243 | This makes it difficult to wire up multiple Net::HTTP request to a select 244 | loop or other non-blocking strategy for handling multiple concurrent 245 | requests. 246 | 247 | Of course, blocking operations can run concurrently in a transparent way 248 | when using threads, so most of the time you can get decent concurrent 249 | performance out of Net::HTTP. That said, Ruby 1.9 in general has moved 250 | toward a more consistent non-blocking API for IO operations, and it 251 | would be great if Net::HTTP was a part of that. 252 | 253 | The biggest challenge is dealing with chunked encoding. With a normal 254 | HTTP response, it is possible to essentially proxy a non-blocking read 255 | to the underlying socket, limited to the size of the Content-Length. 256 | 257 | In contrast, chunked encoding responses contain a number of chunks, each 258 | of which starts with a line containing the number of bytes in the 259 | chunks, followed by a `\r\n`. This means that parsing a chunked response 260 | in a non-blocking way involves doing your own buffering and state 261 | management. 262 | 263 | The upside of this is that all kinds of responses, including responses 264 | that use both chunked encoding and gzip, would have a single non-blocking 265 | API for pulling decoded bytes off the stream. 266 | 267 | This solution builds on the previous solutions that I have already 268 | implemented, as it requires gzip support in the Response itself, as well 269 | as not always blocking on reading the full response body. 270 | 271 | **Status:** Next in the queue. All the groundwork is done. 272 | 273 | **Patch Potential:** Assuming the previous patches were accepted, this 274 | patch would be a pretty straight-forward one. That said, it will be 275 | very difficult, if not impossible, to make this patch a standalone 276 | patch, since it requires features introduced by earlier solutions. 277 | 278 | ## Problem 5: Project Structure 279 | 280 | This problem is perhaps the thorniest. In the current version of Ruby, 281 | Net::HTTP is a single 2,779 line file. When I first started working on 282 | it, I planned to leave it structured this way so that it would be easy 283 | to maintain my changes as patches to the original source. 284 | 285 | Unfortunately, it was extremely difficult to work with code structured 286 | this way, and I quickly succumbed to restructuring it into a number of 287 | smaller files. I also didn't hold back as much as I could have in terms 288 | of cleaning up stylistic issues, but those changes are rather minimal 289 | (things like not using empty parens for method calls with no args and 290 | using do/end rather than curlies for multiline blocks). 291 | 292 | In general, I believe that these changes are the most innocuous of the 293 | overall changes, and should pose a problem to be integrated, but large, 294 | mostly-stylistic patches typically run up against resistance in open 295 | source projects. 296 | 297 | I should be clear that I don't think that these patches are particularly 298 | important on a standalone basis, but (1) they made it significantly 299 | easier for me to work on other improvements, and (2) accepting them 300 | would make it a lot easier for me to submit the rest of my changes as 301 | patches without needing to rewrite them against master. 302 | 303 | **Status:** The most obvious changes are done, but I will probably 304 | continue to restructure things and I get deeper into different parts of 305 | the code. 306 | 307 | **Patch Potential:** Ironically, this has the least potential to cause 308 | problems and will probably be a very hard patch to get accepted. Like 309 | the Ruby 1.8 support, the existence of these changes make extracting 310 | other patches more difficult, but in both cases the patches improve the 311 | code quality. 312 | 313 | ## Problem 6: Documentation 314 | 315 | This problem is both related to overall documentation and documentation 316 | of specific semantics. For instance, the specific behavior of sockets 317 | and keepalive when using the various APIs is reasonably well ordered but 318 | not very well described. 319 | 320 | **Status:** I have been adding a lot of inline documentation to clarify 321 | things, especially where I have made changes. I plan to spend some time 322 | on overall documentation once I'm a bit further on. 323 | 324 | **Patch Potential:** It would be possible for me to port the patches 325 | that specifically apply to parts of the code back to trunk. The 326 | improvements to overall code structure (see above) have done more for 327 | making it easier to understand the code than trying to add documentation 328 | for poorly structured code. 329 | 330 | # Patches in General 331 | 332 | I plan to maintain Net2::HTTP as a library, as well as try to port as 333 | many of the changes as possible to patches. For the reasons I described, 334 | many of the patches would be contingent on other patches, and as I go 335 | deeper, more of the patches will rely on earlier work. 336 | 337 | I am extremely interested in feedback about the work I'm doing, as my 338 | ultimate goal is to propose that some of the more ambitious changes get 339 | accepted. 340 | -------------------------------------------------------------------------------- /lib/net2/backports.rb: -------------------------------------------------------------------------------- 1 | require "stringio" 2 | 3 | class StringIO 4 | def read_nonblock(*args) 5 | val = read(*args) 6 | raise EOFError if val.nil? 7 | val 8 | end 9 | end 10 | 11 | class File 12 | def to_path 13 | path 14 | end 15 | end 16 | 17 | class IO 18 | def self.copy_stream(a, b) 19 | FileUtils.copy_stream(a, b) 20 | end 21 | end 22 | 23 | module SecureRandom 24 | def self.urlsafe_base64(n=nil, padding=false) 25 | s = [random_bytes(n)].pack("m*") 26 | s.delete!("\n") 27 | s.tr!("+/", "-_") 28 | s.delete!("=") if !padding 29 | s 30 | end 31 | end 32 | 33 | module OpenSSL 34 | module Buffering 35 | 36 | # TODO: Make this raise EWOULDBLOCK instead of blocking 37 | def read_nonblock(maxlen, buf=nil) 38 | if maxlen == 0 39 | if buf 40 | buf.clear 41 | return buf 42 | else 43 | return "" 44 | end 45 | end 46 | 47 | if @rbuffer.empty? 48 | return sysread(maxlen, buf) 49 | end 50 | ret = consume_rbuff(maxlen) 51 | if buf 52 | buf.replace(ret) 53 | ret = buf 54 | end 55 | raise EOFError if ret.empty? 56 | ret 57 | end 58 | 59 | end 60 | 61 | module SSL 62 | class SSLSocket 63 | include Buffering 64 | end 65 | end 66 | end 67 | 68 | module URI 69 | TBLENCWWWCOMP_ = {} # :nodoc: 70 | TBLDECWWWCOMP_ = {} # :nodoc: 71 | 72 | # Encode given +str+ to URL-encoded form data. 73 | # 74 | # This method doesn't convert *, -, ., 0-9, A-Z, _, a-z, but does convert SP 75 | # (ASCII space) to + and converts others to %XX. 76 | # 77 | # This is an implementation of 78 | # http://www.w3.org/TR/html5/forms.html#url-encoded-form-data 79 | # 80 | # See URI.decode_www_form_component, URI.encode_www_form 81 | def self.encode_www_form_component(str) 82 | if TBLENCWWWCOMP_.empty? 83 | 256.times do |i| 84 | TBLENCWWWCOMP_[i.chr] = '%%%02X' % i 85 | end 86 | TBLENCWWWCOMP_[' '] = '+' 87 | TBLENCWWWCOMP_.freeze 88 | end 89 | str = str.to_s.dup 90 | str.gsub!(/[^*\-.0-9A-Z_a-z]/) { |m| TBLENCWWWCOMP_[m] } 91 | str 92 | end 93 | 94 | # Decode given +str+ of URL-encoded form data. 95 | # 96 | # This decods + to SP. 97 | # 98 | # See URI.encode_www_form_component, URI.decode_www_form 99 | def self.decode_www_form_component(str) 100 | if TBLDECWWWCOMP_.empty? 101 | 256.times do |i| 102 | h, l = i>>4, i&15 103 | TBLDECWWWCOMP_['%%%X%X' % [h, l]] = i.chr 104 | TBLDECWWWCOMP_['%%%x%X' % [h, l]] = i.chr 105 | TBLDECWWWCOMP_['%%%X%x' % [h, l]] = i.chr 106 | TBLDECWWWCOMP_['%%%x%x' % [h, l]] = i.chr 107 | end 108 | TBLDECWWWCOMP_['+'] = ' ' 109 | TBLDECWWWCOMP_.freeze 110 | end 111 | raise ArgumentError, "invalid %-encoding (#{str})" unless /\A(?:%\h\h|[^%]+)*\z/ =~ str 112 | str.gsub(/\+|%\h\h/, TBLDECWWWCOMP_) 113 | end 114 | 115 | # Generate URL-encoded form data from given +enum+. 116 | # 117 | # This generates application/x-www-form-urlencoded data defined in HTML5 118 | # from given an Enumerable object. 119 | # 120 | # This internally uses URI.encode_www_form_component(str). 121 | # 122 | # This method doesn't convert the encoding of given items, so convert them 123 | # before call this method if you want to send data as other than original 124 | # encoding or mixed encoding data. (Strings which are encoded in an HTML5 125 | # ASCII incompatible encoding are converted to UTF-8.) 126 | # 127 | # This method doesn't handle files. When you send a file, use 128 | # multipart/form-data. 129 | # 130 | # This is an implementation of 131 | # http://www.w3.org/TR/html5/forms.html#url-encoded-form-data 132 | # 133 | # URI.encode_www_form([["q", "ruby"], ["lang", "en"]]) 134 | # #=> "q=ruby&lang=en" 135 | # URI.encode_www_form("q" => "ruby", "lang" => "en") 136 | # #=> "q=ruby&lang=en" 137 | # URI.encode_www_form("q" => ["ruby", "perl"], "lang" => "en") 138 | # #=> "q=ruby&q=perl&lang=en" 139 | # URI.encode_www_form([["q", "ruby"], ["q", "perl"], ["lang", "en"]]) 140 | # #=> "q=ruby&q=perl&lang=en" 141 | # 142 | # See URI.encode_www_form_component, URI.decode_www_form 143 | def self.encode_www_form(enum) 144 | enum.map do |k,v| 145 | if v.nil? 146 | encode_www_form_component(k) 147 | elsif v.respond_to?(:to_ary) 148 | v.to_ary.map do |w| 149 | str = encode_www_form_component(k) 150 | unless w.nil? 151 | str << '=' 152 | str << encode_www_form_component(w) 153 | end 154 | end.join('&') 155 | else 156 | str = encode_www_form_component(k) 157 | str << '=' 158 | str << encode_www_form_component(v) 159 | end 160 | end.join('&') 161 | end 162 | end 163 | -------------------------------------------------------------------------------- /lib/net2/http.rb: -------------------------------------------------------------------------------- 1 | # 2 | # = net/http.rb 3 | # 4 | # Copyright (c) 1999-2007 Yukihiro Matsumoto 5 | # Copyright (c) 1999-2007 Minero Aoki 6 | # Copyright (c) 2001 GOTOU Yuuzou 7 | # 8 | # Written and maintained by Minero Aoki . 9 | # HTTPS support added by GOTOU Yuuzou . 10 | # 11 | # This file is derived from "http-access.rb". 12 | # 13 | # Documented by Minero Aoki; converted to RDoc by William Webber. 14 | # 15 | # This program is free software. You can re-distribute and/or 16 | # modify this program under the same terms of ruby itself --- 17 | # Ruby Distribution License or GNU General Public License. 18 | # 19 | # See Net::HTTP for an overview and examples. 20 | # 21 | 22 | require 'net2/protocol' 23 | autoload :OpenSSL, 'openssl' 24 | require 'uri' 25 | autoload :SecureRandom, 'securerandom' 26 | 27 | if RUBY_VERSION < "1.9" 28 | require "net2/backports" 29 | end 30 | 31 | module URI 32 | class Generic 33 | unless URI::Generic.allocate.respond_to?(:hostname) 34 | def hostname 35 | v = self.host 36 | /\A\[(.*)\]\z/ =~ v ? $1 : v 37 | end 38 | end 39 | end 40 | end 41 | 42 | module Net2 #:nodoc: 43 | 44 | # :stopdoc: 45 | class HTTPBadResponse < StandardError; end 46 | class HTTPHeaderSyntaxError < StandardError; end 47 | # :startdoc: 48 | 49 | # == An HTTP client API for Ruby. 50 | # 51 | # Net::HTTP provides a rich library which can be used to build HTTP 52 | # user-agents. For more details about HTTP see 53 | # [RFC2616](http://www.ietf.org/rfc/rfc2616.txt) 54 | # 55 | # Net::HTTP is designed to work closely with URI. URI::HTTP#host, 56 | # URI::HTTP#port and URI::HTTP#request_uri are designed to work with 57 | # Net::HTTP. 58 | # 59 | # If you are only performing a few GET requests you should try OpenURI. 60 | # 61 | # == Simple Examples 62 | # 63 | # All examples assume you have loaded Net::HTTP with: 64 | # 65 | # require 'net/http' 66 | # 67 | # This will also require 'uri' so you don't need to require it separately. 68 | # 69 | # The Net::HTTP methods in the following section do not persist 70 | # connections. They are not recommended if you are performing many HTTP 71 | # requests. 72 | # 73 | # === GET 74 | # 75 | # Net::HTTP.get('example.com', '/index.html') # => String 76 | # 77 | # === GET by URI 78 | # 79 | # uri = URI('http://example.com/index.html?count=10') 80 | # Net::HTTP.get(uri) # => String 81 | # 82 | # === GET with Dynamic Parameters 83 | # 84 | # uri = URI('http://example.com/index.html') 85 | # params = { :limit => 10, :page => 3 } 86 | # uri.query = URI.encode_www_form(params) 87 | # 88 | # res = Net::HTTP.get_response(uri) 89 | # puts res.body if res.is_a?(Net::HTTPSuccess) 90 | # 91 | # === POST 92 | # 93 | # uri = URI('http://www.example.com/search.cgi') 94 | # res = Net::HTTP.post_form(uri, 'q' => 'ruby', 'max' => '50') 95 | # puts res.body 96 | # 97 | # === POST with Multiple Values 98 | # 99 | # uri = URI('http://www.example.com/search.cgi') 100 | # res = Net::HTTP.post_form(uri, 'q' => ['ruby', 'perl'], 'max' => '50') 101 | # puts res.body 102 | # 103 | # == How to use Net::HTTP 104 | # 105 | # The following example code can be used as the basis of a HTTP user-agent 106 | # which can perform a variety of request types using persistent 107 | # connections. 108 | # 109 | # uri = URI('http://example.com/some_path?query=string') 110 | # 111 | # Net::HTTP.start(uri.host, uri.port) do |http| 112 | # request = Net::HTTP::Get.new uri.request_uri 113 | # 114 | # response = http.request request # Net::HTTPResponse object 115 | # end 116 | # 117 | # Net::HTTP::start immediately creates a connection to an HTTP server which 118 | # is kept open for the duration of the block. The connection will remain 119 | # open for multiple requests in the block if the server indicates it 120 | # supports persistent connections. 121 | # 122 | # The request types Net::HTTP supports are listed below in the section "HTTP 123 | # Request Classes". 124 | # 125 | # If you wish to re-use a connection across multiple HTTP requests without 126 | # automatically closing it you can use ::new instead of ::start. #request 127 | # will automatically open a connection to the server if one is not currently 128 | # open. You can manually close the connection with #close. 129 | # 130 | # === Response Data 131 | # 132 | # uri = URI('http://example.com/index.html') 133 | # res = Net::HTTP.get_response(uri) 134 | # 135 | # # Headers 136 | # res['Set-Cookie'] # => String 137 | # res.get_fields('set-cookie') # => Array 138 | # res.to_hash['set-cookie'] # => Array 139 | # puts "Headers: #{res.to_hash.inspect}" 140 | # 141 | # # Status 142 | # puts res.code # => '200' 143 | # puts res.message # => 'OK' 144 | # puts res.class.name # => 'HTTPOK' 145 | # 146 | # # Body 147 | # puts res.body if res.response_body_permitted? 148 | # 149 | # === Following Redirection 150 | # 151 | # Each Net::HTTPResponse object belongs to a class for its response code. 152 | # 153 | # For example, all 2XX responses are instances of a Net::HTTPSuccess 154 | # subclass, a 3XX response is an instance of a Net::HTTPRedirection 155 | # subclass and a 200 response is an instance of the Net::HTTPOK class. For 156 | # details of response classes, see the section "HTTP Response Classes" 157 | # below. 158 | # 159 | # Using a case statement you can handle various types of responses properly: 160 | # 161 | # def fetch(uri_str, limit = 10) 162 | # # You should choose a better exception. 163 | # raise ArgumentError, 'too many HTTP redirects' if limit == 0 164 | # 165 | # response = Net::HTTP.get_response(URI(uri_str)) 166 | # 167 | # case response 168 | # when Net::HTTPSuccess then 169 | # response 170 | # when Net::HTTPRedirection then 171 | # location = response['location'] 172 | # warn "redirected to #{location}" 173 | # fetch(location, limit - 1) 174 | # else 175 | # response.value 176 | # end 177 | # end 178 | # 179 | # print fetch('http://www.ruby-lang.org') 180 | # 181 | # === POST 182 | # 183 | # A POST can be made using the Net::HTTP::Post request class. This example 184 | # creates a urlencoded POST body: 185 | # 186 | # uri = URI('http://www.example.com/todo.cgi') 187 | # req = Net::HTTP::Post.new(uri.path) 188 | # req.set_form_data('from' => '2005-01-01', 'to' => '2005-03-31') 189 | # 190 | # res = Net::HTTP.start(uri.hostname, uri.port) do |http| 191 | # http.request(req) 192 | # end 193 | # 194 | # case res 195 | # when Net::HTTPSuccess, Net::HTTPRedirection 196 | # # OK 197 | # else 198 | # res.value 199 | # end 200 | # 201 | # At this time Net::HTTP does not support multipart/form-data. To send 202 | # multipart/form-data use Net::HTTPRequest#body= and 203 | # Net::HTTPRequest#content_type=: 204 | # 205 | # req = Net::HTTP::Post.new(uri.path) 206 | # req.body = multipart_data 207 | # req.content_type = 'multipart/form-data' 208 | # 209 | # Other requests that can contain a body such as PUT can be created in the 210 | # same way using the corresponding request class (Net::HTTP::Put). 211 | # 212 | # === Setting Headers 213 | # 214 | # The following example performs a conditional GET using the 215 | # If-Modified-Since header. If the files has not been modified since the 216 | # time in the header a Not Modified response will be returned. See RFC 2616 217 | # section 9.3 for further details. 218 | # 219 | # uri = URI('http://example.com/cached_response') 220 | # file = File.stat 'cached_response' 221 | # 222 | # req = Net::HTTP::Get.new(uri.request_uri) 223 | # req['If-Modified-Since'] = file.mtime.rfc2822 224 | # 225 | # res = Net::HTTP.start(uri.hostname, uri.port) {|http| 226 | # http.request(req) 227 | # } 228 | # 229 | # open 'cached_response', 'w' do |io| 230 | # io.write res.body 231 | # end if res.is_a?(Net::HTTPSuccess) 232 | # 233 | # === Basic Authentication 234 | # 235 | # Basic authentication is performed according to 236 | # [RFC2617](http://www.ietf.org/rfc/rfc2617.txt) 237 | # 238 | # uri = URI('http://example.com/index.html?key=value') 239 | # 240 | # req = Net::HTTP::Get.new(uri.request_uri) 241 | # req.basic_auth 'user', 'pass' 242 | # 243 | # res = Net::HTTP.start(uri.hostname, uri.port) {|http| 244 | # http.request(req) 245 | # } 246 | # puts res.body 247 | # 248 | # === Streaming Response Bodies 249 | # 250 | # By default Net::HTTP reads an entire response into memory. If you are 251 | # handling large files or wish to implement a progress bar you can instead 252 | # stream the body directly to an IO. 253 | # 254 | # uri = URI('http://example.com/large_file') 255 | # 256 | # Net::HTTP.start(uri.host, uri.port) do |http| 257 | # request = Net::HTTP::Get.new uri.request_uri 258 | # 259 | # http.request request do |response| 260 | # open 'large_file', 'w' do |io| 261 | # response.read_body do |chunk| 262 | # io.write chunk 263 | # end 264 | # end 265 | # end 266 | # end 267 | # 268 | # === HTTPS 269 | # 270 | # HTTPS is enabled for an HTTP connection by Net::HTTP#use_ssl=. 271 | # 272 | # uri = URI('https://secure.example.com/some_path?query=string') 273 | # 274 | # Net::HTTP.start(uri.host, uri.port, 275 | # :use_ssl => uri.scheme == 'https').start do |http| 276 | # request = Net::HTTP::Get.new uri.request_uri 277 | # 278 | # response = http.request request # Net::HTTPResponse object 279 | # end 280 | # 281 | # In previous versions of ruby you would need to require 'net/https' to use 282 | # HTTPS. This is no longer true. 283 | # 284 | # === Proxies 285 | # 286 | # Net::HTTP::Proxy has the same methods as Net::HTTP but its instances always 287 | # connect via the proxy instead of directly to the given host. 288 | # 289 | # proxy_addr = 'your.proxy.host' 290 | # proxy_port = 8080 291 | # 292 | # Net::HTTP::Proxy(proxy_addr, proxy_port).start('www.example.com') {|http| 293 | # # always connect to your.proxy.addr:8080 294 | # } 295 | # 296 | # Net::HTTP::Proxy returns a Net::HTTP instance when proxy_addr is nil so 297 | # there is no need for conditional code. 298 | # 299 | # See Net::HTTP::Proxy for further details and examples such as proxies that 300 | # require a username and password. 301 | # 302 | # == HTTP Request Classes 303 | # 304 | # Here is the HTTP request class hierarchy. 305 | # 306 | # * Net::HTTPRequest 307 | # * Net::HTTP::Get 308 | # * Net::HTTP::Head 309 | # * Net::HTTP::Post 310 | # * Net::HTTP::Put 311 | # * Net::HTTP::Proppatch 312 | # * Net::HTTP::Lock 313 | # * Net::HTTP::Unlock 314 | # * Net::HTTP::Options 315 | # * Net::HTTP::Propfind 316 | # * Net::HTTP::Delete 317 | # * Net::HTTP::Move 318 | # * Net::HTTP::Copy 319 | # * Net::HTTP::Mkcol 320 | # * Net::HTTP::Trace 321 | # 322 | # == HTTP Response Classes 323 | # 324 | # Here is HTTP response class hierarchy. All classes are defined in Net 325 | # module and are subclasses of Net::HTTPResponse. 326 | # 327 | # HTTPUnknownResponse:: For unhandled HTTP extenensions 328 | # HTTPInformation:: 1xx 329 | # HTTPContinue:: 100 330 | # HTTPSwitchProtocol:: 101 331 | # HTTPSuccess:: 2xx 332 | # HTTPOK:: 200 333 | # HTTPCreated:: 201 334 | # HTTPAccepted:: 202 335 | # HTTPNonAuthoritativeInformation:: 203 336 | # HTTPNoContent:: 204 337 | # HTTPResetContent:: 205 338 | # HTTPPartialContent:: 206 339 | # HTTPRedirection:: 3xx 340 | # HTTPMultipleChoice:: 300 341 | # HTTPMovedPermanently:: 301 342 | # HTTPFound:: 302 343 | # HTTPSeeOther:: 303 344 | # HTTPNotModified:: 304 345 | # HTTPUseProxy:: 305 346 | # HTTPTemporaryRedirect:: 307 347 | # HTTPClientError:: 4xx 348 | # HTTPBadRequest:: 400 349 | # HTTPUnauthorized:: 401 350 | # HTTPPaymentRequired:: 402 351 | # HTTPForbidden:: 403 352 | # HTTPNotFound:: 404 353 | # HTTPMethodNotAllowed:: 405 354 | # HTTPNotAcceptable:: 406 355 | # HTTPProxyAuthenticationRequired:: 407 356 | # HTTPRequestTimeOut:: 408 357 | # HTTPConflict:: 409 358 | # HTTPGone:: 410 359 | # HTTPLengthRequired:: 411 360 | # HTTPPreconditionFailed:: 412 361 | # HTTPRequestEntityTooLarge:: 413 362 | # HTTPRequestURITooLong:: 414 363 | # HTTPUnsupportedMediaType:: 415 364 | # HTTPRequestedRangeNotSatisfiable:: 416 365 | # HTTPExpectationFailed:: 417 366 | # HTTPServerError:: 5xx 367 | # HTTPInternalServerError:: 500 368 | # HTTPNotImplemented:: 501 369 | # HTTPBadGateway:: 502 370 | # HTTPServiceUnavailable:: 503 371 | # HTTPGatewayTimeOut:: 504 372 | # HTTPVersionNotSupported:: 505 373 | # 374 | # There is also the Net::HTTPBadResponse exception which is raised when 375 | # there is a protocol error. 376 | # 377 | class HTTP < Protocol 378 | 379 | # :stopdoc: 380 | Revision = %q$Revision$.split[1] 381 | HTTPVersion = '1.1' 382 | begin 383 | require 'zlib' 384 | require 'stringio' #for our purposes (unpacking gzip) lump these together 385 | HAVE_ZLIB=true 386 | rescue LoadError 387 | HAVE_ZLIB=false 388 | end 389 | # :startdoc: 390 | 391 | # Turns on net/http 1.2 (ruby 1.8) features. 392 | # Defaults to ON in ruby 1.8 or later. 393 | def HTTP.version_1_2 394 | true 395 | end 396 | 397 | # Returns true if net/http is in version 1.2 mode. 398 | # Defaults to true. 399 | def HTTP.version_1_2? 400 | true 401 | end 402 | 403 | # :nodoc: 404 | def HTTP.version_1_1? 405 | false 406 | end 407 | 408 | class << self 409 | alias is_version_1_1? version_1_1? #:nodoc: 410 | alias is_version_1_2? version_1_2? #:nodoc: 411 | end 412 | 413 | # 414 | # short cut methods 415 | # 416 | 417 | # 418 | # Gets the body text from the target and outputs it to $stdout. The 419 | # target can either be specified as 420 | # (+uri+), or as (+host+, +path+, +port+ = 80); so: 421 | # 422 | # Net::HTTP.get_print URI('http://www.example.com/index.html') 423 | # 424 | # or: 425 | # 426 | # Net::HTTP.get_print 'www.example.com', '/index.html' 427 | # 428 | def self.get_print(uri_or_host, path = nil, port = nil) 429 | get_response(uri_or_host, path, port) do |res| 430 | res.read_body do |chunk| 431 | $stdout.print chunk 432 | end 433 | res.close 434 | end 435 | nil 436 | end 437 | 438 | # Sends a GET request to the target and returns the HTTP response 439 | # as a string. The target can either be specified as 440 | # (+uri+), or as (+host+, +path+, +port+ = 80); so: 441 | # 442 | # print Net::HTTP.get(URI('http://www.example.com/index.html')) 443 | # 444 | # or: 445 | # 446 | # print Net::HTTP.get('www.example.com', '/index.html') 447 | # 448 | def self.get(uri_or_host, path = nil, port = nil) 449 | get_response(uri_or_host, path, port) do |response| 450 | return response.body 451 | end 452 | end 453 | 454 | # Sends a GET request to the target and returns the HTTP response 455 | # as a Net::HTTPResponse object. The target can either be specified as 456 | # (+uri+), or as (+host+, +path+, +port+ = 80); so: 457 | # 458 | # res = Net::HTTP.get_response(URI('http://www.example.com/index.html')) 459 | # print res.body 460 | # 461 | # or: 462 | # 463 | # res = Net::HTTP.get_response('www.example.com', '/index.html') 464 | # print res.body 465 | # 466 | def self.get_response(uri_or_host, path = nil, port = nil, &block) 467 | if uri_or_host.respond_to?(:hostname) 468 | host = uri_or_host.hostname 469 | port = uri_or_host.port 470 | path = uri_or_host.request_uri 471 | elsif path 472 | host = uri_or_host 473 | else 474 | uri = URI.parse(uri_or_host) 475 | return get_response(uri, &block) 476 | end 477 | 478 | http = new(host, port || HTTP.default_port).start 479 | http.request_get(path, &block) 480 | end 481 | 482 | # Posts HTML form data to the specified URI object. 483 | # The form data must be provided as a Hash mapping from String to String. 484 | # Example: 485 | # 486 | # { "cmd" => "search", "q" => "ruby", "max" => "50" } 487 | # 488 | # This method also does Basic Authentication iff +url+.user exists. 489 | # But userinfo for authentication is deprecated (RFC3986). 490 | # So this feature will be removed. 491 | # 492 | # Example: 493 | # 494 | # require 'net/http' 495 | # require 'uri' 496 | # 497 | # HTTP.post_form URI('http://www.example.com/search.cgi'), 498 | # { "q" => "ruby", "max" => "50" } 499 | # 500 | def self.post_form(url, params) 501 | req = Post.new(url.path) 502 | req.form_data = params 503 | req.basic_auth url.user, url.password if url.user 504 | new(url.host, url.port).start do |http| 505 | response = http.request(req) 506 | 507 | # we're using the block form, so make sure to read the 508 | # body before the socket is closed. 509 | response.close 510 | response 511 | end 512 | end 513 | 514 | # 515 | # HTTP session management 516 | # 517 | 518 | # The default port to use for HTTP requests; defaults to 80. 519 | def self.default_port 520 | http_default_port() 521 | end 522 | 523 | # The default port to use for HTTP requests; defaults to 80. 524 | def self.http_default_port 525 | 80 526 | end 527 | 528 | # The default port to use for HTTPS requests; defaults to 443. 529 | def self.https_default_port 530 | 443 531 | end 532 | 533 | def self.socket_type #:nodoc: obsolete 534 | BufferedIO 535 | end 536 | 537 | # call-seq: 538 | # HTTP.start(address, port, p_addr, p_port, p_user, p_pass, &block) 539 | # HTTP.start(address, port=nil, p_addr=nil, p_port=nil, p_user=nil, p_pass=nil, opt, &block) 540 | # 541 | # Creates a new Net::HTTP object, then additionally opens the TCP 542 | # connection and HTTP session. 543 | # 544 | # Argments are following: 545 | # _address_ :: hostname or IP address of the server 546 | # _port_ :: port of the server 547 | # _p_addr_ :: address of proxy 548 | # _p_port_ :: port of proxy 549 | # _p_user_ :: user of proxy 550 | # _p_pass_ :: pass of proxy 551 | # _opt_ :: optional hash 552 | # 553 | # _opt_ sets following values by its accessor. 554 | # The keys are ca_file, ca_path, cert, cert_store, ciphers, 555 | # close_on_empty_response, key, open_timeout, read_timeout, ssl_timeout, 556 | # ssl_version, use_ssl, verify_callback, verify_depth and verify_mode. 557 | # If you set :use_ssl as true, you can use https and default value of 558 | # verify_mode is set as OpenSSL::SSL::VERIFY_PEER. 559 | # 560 | # If the optional block is given, the newly 561 | # created Net::HTTP object is passed to it and closed when the 562 | # block finishes. In this case, the return value of this method 563 | # is the return value of the block. If no block is given, the 564 | # return value of this method is the newly created Net::HTTP object 565 | # itself, and the caller is responsible for closing it upon completion 566 | # using the finish() method. 567 | def self.start(address, *arg, &block) # :yield: +http+ 568 | arg.pop if opt = Hash.try_convert(arg[-1]) 569 | port, p_addr, p_port, p_user, p_pass = *arg 570 | port = https_default_port if !port && opt && opt[:use_ssl] 571 | http = new(address, port, p_addr, p_port, p_user, p_pass) 572 | 573 | if opt 574 | opt = {:verify_mode => OpenSSL::SSL::VERIFY_PEER}.update(opt) if opt[:use_ssl] 575 | http.methods.grep(/\A(\w+)=\z/) do |meth| 576 | key = $1.to_sym 577 | opt.key?(key) or next 578 | http.__send__(meth, opt[key]) 579 | end 580 | end 581 | 582 | http.start(&block) 583 | end 584 | 585 | class << self 586 | alias newobj new 587 | end 588 | 589 | # Creates a new Net::HTTP object without opening a TCP connection or 590 | # HTTP session. 591 | # The +address+ should be a DNS hostname or IP address. 592 | # If +p_addr+ is given, creates a Net::HTTP object with proxy support. 593 | def self.new(address, port = nil, p_addr = nil, p_port = nil, p_user = nil, p_pass = nil) 594 | Proxy(p_addr, p_port, p_user, p_pass).newobj(address, port) 595 | end 596 | 597 | # Creates a new Net::HTTP object for the specified server address, 598 | # without opening the TCP connection or initializing the HTTP session. 599 | # The +address+ should be a DNS hostname or IP address. 600 | def initialize(address, port = nil) 601 | @address = address 602 | @port = (port || HTTP.default_port) 603 | @curr_http_version = HTTPVersion 604 | @no_keepalive_server = false 605 | @close_on_empty_response = false 606 | @socket = nil 607 | @started = false 608 | @open_timeout = nil 609 | @read_timeout = 60 610 | @debug_output = nil 611 | @use_ssl = false 612 | @ssl_context = nil 613 | @enable_post_connection_check = true 614 | @compression = nil 615 | @sspi_enabled = false 616 | if defined?(SSL_ATTRIBUTES) 617 | SSL_ATTRIBUTES.each do |name| 618 | instance_variable_set "@#{name}", nil 619 | end 620 | end 621 | end 622 | 623 | def inspect 624 | "#<#{self.class} #{@address}:#{@port} open=#{started?}>" 625 | end 626 | 627 | # *WARNING* This method opens a serious security hole. 628 | # Never use this method in production code. 629 | # 630 | # Sets an output stream for debugging. 631 | # 632 | # http = Net::HTTP.new 633 | # http.set_debug_output $stderr 634 | # http.start { .... } 635 | # 636 | def set_debug_output(output) 637 | warn 'Net::HTTP#set_debug_output called after HTTP started' if started? 638 | @debug_output = output 639 | end 640 | 641 | # The socket 642 | attr_reader :socket 643 | 644 | # The DNS host name or IP address to connect to. 645 | attr_reader :address 646 | 647 | # The port number to connect to. 648 | attr_reader :port 649 | 650 | # Number of seconds to wait for the connection to open. 651 | # If the HTTP object cannot open a connection in this many seconds, 652 | # it raises a TimeoutError exception. 653 | attr_accessor :open_timeout 654 | 655 | # Number of seconds to wait for one block to be read (via one read(2) 656 | # call). If the HTTP object cannot read data in this many seconds, 657 | # it raises a TimeoutError exception. 658 | attr_reader :read_timeout 659 | 660 | # Setter for the read_timeout attribute. 661 | def read_timeout=(sec) 662 | @socket.read_timeout = sec if @socket 663 | @read_timeout = sec 664 | end 665 | 666 | # Returns true if the HTTP session has been started. 667 | def started? 668 | @started 669 | end 670 | 671 | alias active? started? #:nodoc: obsolete 672 | 673 | attr_accessor :close_on_empty_response 674 | 675 | # Returns true if SSL/TLS is being used with HTTP. 676 | def use_ssl? 677 | @use_ssl 678 | end 679 | 680 | # Turn on/off SSL. 681 | # This flag must be set before starting session. 682 | # If you change use_ssl value after session started, 683 | # a Net::HTTP object raises IOError. 684 | def use_ssl=(flag) 685 | flag = (flag ? true : false) 686 | if started? and @use_ssl != flag 687 | raise IOError, "use_ssl value changed, but session already started" 688 | end 689 | @use_ssl = flag 690 | end 691 | 692 | SSL_ATTRIBUTES = %w( 693 | ssl_version key cert ca_file ca_path cert_store ciphers 694 | verify_mode verify_callback verify_depth ssl_timeout 695 | ) 696 | 697 | # Sets path of a CA certification file in PEM format. 698 | # 699 | # The file can contain several CA certificates. 700 | attr_accessor :ca_file 701 | 702 | # Sets path of a CA certification directory containing certifications in 703 | # PEM format. 704 | attr_accessor :ca_path 705 | 706 | # Sets an OpenSSL::X509::Certificate object as client certificate. 707 | # (This method is appeared in Michal Rokos's OpenSSL extension). 708 | attr_accessor :cert 709 | 710 | # Sets the X509::Store to verify peer certificate. 711 | attr_accessor :cert_store 712 | 713 | # Sets the available ciphers. See OpenSSL::SSL::SSLContext#ciphers= 714 | attr_accessor :ciphers 715 | 716 | # Sets an OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object. 717 | # (This method is appeared in Michal Rokos's OpenSSL extension.) 718 | attr_accessor :key 719 | 720 | # Sets the SSL timeout seconds. 721 | attr_accessor :ssl_timeout 722 | 723 | # Sets the SSL version. See OpenSSL::SSL::SSLContext#ssl_version= 724 | attr_accessor :ssl_version 725 | 726 | # Sets the verify callback for the server certification verification. 727 | attr_accessor :verify_callback 728 | 729 | # Sets the maximum depth for the certificate chain verification. 730 | attr_accessor :verify_depth 731 | 732 | # Sets the flags for server the certification verification at beginning of 733 | # SSL/TLS session. 734 | # 735 | # OpenSSL::SSL::VERIFY_NONE or OpenSSL::SSL::VERIFY_PEER are acceptable. 736 | attr_accessor :verify_mode 737 | 738 | # Returns the X.509 certificates the server presented. 739 | def peer_cert 740 | if not use_ssl? or not @socket 741 | return nil 742 | end 743 | @socket.io.peer_cert 744 | end 745 | 746 | # Opens a TCP connection and HTTP session. 747 | # 748 | # When this method is called with a block, it passes the Net::HTTP 749 | # object to the block, and closes the TCP connection and HTTP session 750 | # after the block has been executed. 751 | # 752 | # When called with a block, it returns the return value of the 753 | # block; otherwise, it returns self. 754 | # 755 | def start # :yield: http 756 | raise IOError, 'HTTP session already opened' if @started 757 | if block_given? 758 | begin 759 | do_start 760 | return yield(self) 761 | ensure 762 | do_finish 763 | end 764 | end 765 | do_start 766 | self 767 | end 768 | 769 | def do_start 770 | connect 771 | @started = true 772 | end 773 | private :do_start 774 | 775 | def connect 776 | D "opening connection to #{conn_address()}..." 777 | s = timeout(@open_timeout) { TCPSocket.open(conn_address(), conn_port()) } 778 | D "opened" 779 | if use_ssl? 780 | ssl_parameters = Hash.new 781 | iv_list = instance_variables 782 | iv_list = iv_list.map { |name| name.to_sym } unless iv_list.first.is_a?(Symbol) 783 | 784 | SSL_ATTRIBUTES.each do |name| 785 | ivname = "@#{name}".intern 786 | if iv_list.include?(ivname) and 787 | value = instance_variable_get(ivname) 788 | ssl_parameters[name] = value 789 | end 790 | end 791 | @ssl_context = OpenSSL::SSL::SSLContext.new 792 | @ssl_context.set_params(ssl_parameters) 793 | s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context) 794 | s.sync_close = true 795 | end 796 | @socket = BufferedIO.new(s) 797 | @socket.read_timeout = @read_timeout 798 | @socket.debug_output = @debug_output 799 | if use_ssl? 800 | begin 801 | if proxy? 802 | @socket.writeline sprintf('CONNECT %s:%s HTTP/%s', 803 | @address, @port, HTTPVersion) 804 | @socket.writeline "Host: #{@address}:#{@port}" 805 | if proxy_user 806 | credential = ["#{proxy_user}:#{proxy_pass}"].pack('m') 807 | credential.delete!("\r\n") 808 | @socket.writeline "Proxy-Authorization: Basic #{credential}" 809 | end 810 | @socket.writeline '' 811 | HTTPResponse.read_new(@socket).value 812 | end 813 | timeout(@open_timeout) { s.connect } 814 | if @ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE 815 | s.post_connection_check(@address) 816 | end 817 | rescue => exception 818 | D "Conn close because of connect error #{exception}" 819 | @socket.close if @socket and not @socket.closed? 820 | raise exception 821 | end 822 | end 823 | on_connect 824 | end 825 | private :connect 826 | 827 | def on_connect 828 | end 829 | private :on_connect 830 | 831 | # Finishes the HTTP session and closes the TCP connection. 832 | # Raises IOError if the session has not been started. 833 | def finish 834 | raise IOError, 'HTTP session not yet started' unless started? 835 | do_finish 836 | end 837 | 838 | def do_finish 839 | @started = false 840 | @socket.close if @socket and not @socket.closed? 841 | @socket = nil 842 | end 843 | private :do_finish 844 | 845 | # 846 | # proxy 847 | # 848 | 849 | public 850 | 851 | # no proxy 852 | @is_proxy_class = false 853 | @proxy_addr = nil 854 | @proxy_port = nil 855 | @proxy_user = nil 856 | @proxy_pass = nil 857 | 858 | # Creates an HTTP proxy class which behaves like Net::HTTP, but 859 | # performs all access via the specified proxy. 860 | # 861 | # The arguments are the DNS name or IP address of the proxy host, 862 | # the port to use to access the proxy, and a username and password 863 | # if authorization is required to use the proxy. 864 | # 865 | # You can replace any use of the Net::HTTP class with use of the 866 | # proxy class created. 867 | # 868 | # If +p_addr+ is nil, this method returns self (a Net::HTTP object). 869 | # 870 | # # Example 871 | # proxy_class = Net::HTTP::Proxy('proxy.example.com', 8080) 872 | # 873 | # proxy_class.start('www.ruby-lang.org') {|http| 874 | # # connecting proxy.foo.org:8080 875 | # } 876 | # 877 | # You may use them to work with authorization-enabled proxies: 878 | # 879 | # proxy_host = 'your.proxy.example' 880 | # proxy_port = 8080 881 | # proxy_user = 'user' 882 | # proxy_pass = 'pass' 883 | # 884 | # proxy = Net::HTTP::Proxy(proxy_host, proxy_port, proxy_user, proxy_pass) 885 | # proxy.start('www.example.com') { |http| 886 | # # always connect to your.proxy.example:8080 using specified username 887 | # # and password 888 | # } 889 | # 890 | # Note that net/http does not use the HTTP_PROXY environment variable. 891 | # If you want to use a proxy, you must set it explicitly. 892 | # 893 | def self.Proxy(p_addr, p_port = nil, p_user = nil, p_pass = nil) 894 | return self unless p_addr 895 | 896 | delta = ProxyDelta 897 | proxyclass = Class.new(self) 898 | 899 | proxyclass.module_eval do 900 | include delta 901 | # with proxy 902 | @is_proxy_class = true 903 | @proxy_address = p_addr 904 | @proxy_port = p_port || default_port() 905 | @proxy_user = p_user 906 | @proxy_pass = p_pass 907 | end 908 | 909 | proxyclass 910 | end 911 | 912 | class << HTTP 913 | # returns true if self is a class which was created by HTTP::Proxy. 914 | def proxy_class? 915 | @is_proxy_class 916 | end 917 | 918 | attr_reader :proxy_address 919 | attr_reader :proxy_port 920 | attr_reader :proxy_user 921 | attr_reader :proxy_pass 922 | end 923 | 924 | # True if self is a HTTP proxy class. 925 | def proxy? 926 | self.class.proxy_class? 927 | end 928 | 929 | # Address of proxy host. If self does not use a proxy, nil. 930 | def proxy_address 931 | self.class.proxy_address 932 | end 933 | 934 | # Port number of proxy host. If self does not use a proxy, nil. 935 | def proxy_port 936 | self.class.proxy_port 937 | end 938 | 939 | # User name for accessing proxy. If self does not use a proxy, nil. 940 | def proxy_user 941 | self.class.proxy_user 942 | end 943 | 944 | # User password for accessing proxy. If self does not use a proxy, nil. 945 | def proxy_pass 946 | self.class.proxy_pass 947 | end 948 | 949 | alias proxyaddr proxy_address #:nodoc: obsolete 950 | alias proxyport proxy_port #:nodoc: obsolete 951 | 952 | private 953 | 954 | # without proxy 955 | 956 | def conn_address 957 | address() 958 | end 959 | 960 | def conn_port 961 | port() 962 | end 963 | 964 | def edit_path(path) 965 | path 966 | end 967 | 968 | module ProxyDelta #:nodoc: internal use only 969 | private 970 | 971 | def conn_address 972 | proxy_address() 973 | end 974 | 975 | def conn_port 976 | proxy_port() 977 | end 978 | 979 | def edit_path(path) 980 | use_ssl? ? path : "http://#{addr_port()}#{path}" 981 | end 982 | end 983 | 984 | # 985 | # HTTP operations 986 | # 987 | 988 | public 989 | 990 | # Gets data from +path+ on the connected-to host. 991 | # +initheader+ must be a Hash like { 'Accept' => '*/*', ... }, 992 | # and it defaults to an empty hash. 993 | # If +initheader+ doesn't have the key 'accept-encoding', then 994 | # a value of "gzip;q=1.0,deflate;q=0.6,identity;q=0.3" is used, 995 | # so that gzip compression is used in preference to deflate 996 | # compression, which is used in preference to no compression. 997 | # Ruby doesn't have libraries to support the compress (Lempel-Ziv) 998 | # compression, so that is not supported. The intent of this is 999 | # to reduce bandwidth by default. If this routine sets up 1000 | # compression, then it does the decompression also, removing 1001 | # the header as well to prevent confusion. Otherwise 1002 | # it leaves the body as it found it. 1003 | # 1004 | # This method returns a Net::HTTPResponse object. 1005 | # 1006 | # If called with a block, yields each fragment of the 1007 | # entity body in turn as a string as it is read from 1008 | # the socket. Note that in this case, the returned response 1009 | # object will *not* contain a (meaningful) body. 1010 | # 1011 | # +dest+ argument is obsolete. 1012 | # It still works but you must not use it. 1013 | # 1014 | # This method never raises an exception. 1015 | # 1016 | # response = http.get('/index.html') 1017 | # 1018 | # # using block 1019 | # File.open('result.txt', 'w') {|f| 1020 | # http.get('/~foo/') do |str| 1021 | # f.write str 1022 | # end 1023 | # } 1024 | # 1025 | def get(path, initheader = {}, dest = nil, &block) # :yield: +body_segment+ 1026 | response = nil 1027 | 1028 | request(Get.new(path, initheader)) do |r| 1029 | response = r 1030 | r.read_body(dest, &block) 1031 | r.close 1032 | end 1033 | 1034 | response 1035 | end 1036 | 1037 | # Gets only the header from +path+ on the connected-to host. 1038 | # +header+ is a Hash like { 'Accept' => '*/*', ... }. 1039 | # 1040 | # This method returns a Net::HTTPResponse object. 1041 | # 1042 | # This method never raises an exception. 1043 | # 1044 | # response = nil 1045 | # Net::HTTP.start('some.www.server', 80) {|http| 1046 | # response = http.head('/index.html') 1047 | # } 1048 | # p response['content-type'] 1049 | # 1050 | def head(path, initheader = nil) 1051 | request(Head.new(path, initheader)) 1052 | end 1053 | 1054 | # Posts +data+ (must be a String) to +path+. +header+ must be a Hash 1055 | # like { 'Accept' => '*/*', ... }. 1056 | # 1057 | # This method returns a Net::HTTPResponse object. 1058 | # 1059 | # If called with a block, yields each fragment of the 1060 | # entity body in turn as a string as it is read from 1061 | # the socket. Note that in this case, the returned response 1062 | # object will *not* contain a (meaningful) body. 1063 | # 1064 | # +dest+ argument is obsolete. 1065 | # It still works but you must not use it. 1066 | # 1067 | # This method never raises exception. 1068 | # 1069 | # response = http.post('/cgi-bin/search.rb', 'query=foo') 1070 | # 1071 | # # using block 1072 | # File.open('result.txt', 'w') {|f| 1073 | # http.post('/cgi-bin/search.rb', 'query=foo') do |str| 1074 | # f.write str 1075 | # end 1076 | # } 1077 | # 1078 | # You should set Content-Type: header field for POST. 1079 | # If no Content-Type: field given, this method uses 1080 | # "application/x-www-form-urlencoded" by default. 1081 | # 1082 | def post(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segment+ 1083 | send_entity(path, data, initheader, dest, Post, &block) 1084 | end 1085 | 1086 | # Sends a PATCH request to the +path+ and gets a response, 1087 | # as an HTTPResponse object. 1088 | def patch(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segment+ 1089 | send_entity(path, data, initheader, dest, Patch, &block) 1090 | end 1091 | 1092 | def put(path, data, initheader = nil) #:nodoc: 1093 | request(Put.new(path, initheader), data) 1094 | end 1095 | 1096 | # Sends a PROPPATCH request to the +path+ and gets a response, 1097 | # as an HTTPResponse object. 1098 | def proppatch(path, body, initheader = nil) 1099 | request(Proppatch.new(path, initheader), body) 1100 | end 1101 | 1102 | # Sends a LOCK request to the +path+ and gets a response, 1103 | # as an HTTPResponse object. 1104 | def lock(path, body, initheader = nil) 1105 | request(Lock.new(path, initheader), body) 1106 | end 1107 | 1108 | # Sends a UNLOCK request to the +path+ and gets a response, 1109 | # as an HTTPResponse object. 1110 | def unlock(path, body, initheader = nil) 1111 | request(Unlock.new(path, initheader), body) 1112 | end 1113 | 1114 | # Sends a OPTIONS request to the +path+ and gets a response, 1115 | # as an HTTPResponse object. 1116 | def options(path, initheader = nil) 1117 | request(Options.new(path, initheader)) 1118 | end 1119 | 1120 | # Sends a PROPFIND request to the +path+ and gets a response, 1121 | # as an HTTPResponse object. 1122 | def propfind(path, body = nil, initheader = {'Depth' => '0'}) 1123 | request(Propfind.new(path, initheader), body) 1124 | end 1125 | 1126 | # Sends a DELETE request to the +path+ and gets a response, 1127 | # as an HTTPResponse object. 1128 | def delete(path, initheader = {'Depth' => 'Infinity'}) 1129 | request(Delete.new(path, initheader)) 1130 | end 1131 | 1132 | # Sends a MOVE request to the +path+ and gets a response, 1133 | # as an HTTPResponse object. 1134 | def move(path, initheader = nil) 1135 | request(Move.new(path, initheader)) 1136 | end 1137 | 1138 | # Sends a COPY request to the +path+ and gets a response, 1139 | # as an HTTPResponse object. 1140 | def copy(path, initheader = nil) 1141 | request(Copy.new(path, initheader)) 1142 | end 1143 | 1144 | # Sends a MKCOL request to the +path+ and gets a response, 1145 | # as an HTTPResponse object. 1146 | def mkcol(path, body = nil, initheader = nil) 1147 | request(Mkcol.new(path, initheader), body) 1148 | end 1149 | 1150 | # Sends a TRACE request to the +path+ and gets a response, 1151 | # as an HTTPResponse object. 1152 | def trace(path, initheader = nil) 1153 | request(Trace.new(path, initheader)) 1154 | end 1155 | 1156 | # Sends a GET request to the +path+. 1157 | # Returns the response as a Net::HTTPResponse object. 1158 | # 1159 | # When called with a block, passes an HTTPResponse object to the block. 1160 | # The body of the response will not have been read yet; 1161 | # the block can process it using HTTPResponse#read_body, 1162 | # if desired. 1163 | # 1164 | # Returns the response. 1165 | # 1166 | # This method never raises Net::* exceptions. 1167 | # 1168 | # response = http.request_get('/index.html') 1169 | # # The entity body is already read in this case. 1170 | # p response['content-type'] 1171 | # puts response.body 1172 | # 1173 | # # Using a block 1174 | # http.request_get('/index.html') {|response| 1175 | # p response['content-type'] 1176 | # response.read_body do |str| # read body now 1177 | # print str 1178 | # end 1179 | # } 1180 | # 1181 | def request_get(path, initheader = nil, &block) # :yield: +response+ 1182 | request(Get.new(path, initheader), &block) 1183 | end 1184 | 1185 | # Sends a HEAD request to the +path+ and returns the response 1186 | # as a Net::HTTPResponse object. 1187 | # 1188 | # Returns the response. 1189 | # 1190 | # This method never raises Net::* exceptions. 1191 | # 1192 | # response = http.request_head('/index.html') 1193 | # p response['content-type'] 1194 | # 1195 | def request_head(path, initheader = nil, &block) 1196 | request(Head.new(path, initheader), &block) 1197 | end 1198 | 1199 | # Sends a POST request to the +path+. 1200 | # 1201 | # Returns the response as a Net::HTTPResponse object. 1202 | # 1203 | # When called with a block, the block is passed an HTTPResponse 1204 | # object. The body of that response will not have been read yet; 1205 | # the block can process it using HTTPResponse#read_body, if desired. 1206 | # 1207 | # Returns the response. 1208 | # 1209 | # This method never raises Net::* exceptions. 1210 | # 1211 | # # example 1212 | # response = http.request_post('/cgi-bin/nice.rb', 'datadatadata...') 1213 | # p response.status 1214 | # puts response.body # body is already read in this case 1215 | # 1216 | # # using block 1217 | # http.request_post('/cgi-bin/nice.rb', 'datadatadata...') {|response| 1218 | # p response.status 1219 | # p response['content-type'] 1220 | # response.read_body do |str| # read body now 1221 | # print str 1222 | # end 1223 | # } 1224 | # 1225 | def request_post(path, data, initheader = nil, &block) # :yield: +response+ 1226 | request Post.new(path, initheader), data, &block 1227 | end 1228 | 1229 | def request_put(path, data, initheader = nil, &block) #:nodoc: 1230 | request Put.new(path, initheader), data, &block 1231 | end 1232 | 1233 | alias get2 request_get #:nodoc: obsolete 1234 | alias head2 request_head #:nodoc: obsolete 1235 | alias post2 request_post #:nodoc: obsolete 1236 | alias put2 request_put #:nodoc: obsolete 1237 | 1238 | def to_io 1239 | @socket 1240 | end 1241 | 1242 | # Sends an HTTP request to the HTTP server. 1243 | # Also sends a DATA string if +data+ is given. 1244 | # 1245 | # Returns a Net::HTTPResponse object. 1246 | # 1247 | # This method never raises Net::* exceptions. 1248 | # 1249 | # response = http.send_request('GET', '/index.html') 1250 | # puts response.body 1251 | # 1252 | def send_request(name, path, data = nil, header = nil) 1253 | r = HTTPGenericRequest.new(name,(data ? true : false),true,path,header) 1254 | request r, data 1255 | end 1256 | 1257 | # Sends an HTTPRequest object +req+ to the HTTP server. 1258 | # 1259 | # If +req+ is a Net::HTTP::Post or Net::HTTP::Put request containing 1260 | # data, the data is also sent. Providing data for a Net::HTTP::Head or 1261 | # Net::HTTP::Get request results in an ArgumentError. 1262 | # 1263 | # Returns an HTTPResponse object. 1264 | # 1265 | # When called with a block, passes an HTTPResponse object to the block. 1266 | # The body of the response will not have been read yet; 1267 | # the block can process it using HTTPResponse#read_body, 1268 | # if desired. 1269 | # 1270 | # This method never raises Net::* exceptions. 1271 | # 1272 | def request(req, body = nil, &block) # :yield: +response+ 1273 | # If a request is made, and the connection hasn't been started, 1274 | # wrap the request in a start block, which will create a new 1275 | # connection and read the body so that it can close the socket. 1276 | # 1277 | # If you want to make several requests reusing the same 1278 | # connection, use Net::HTTP.start: 1279 | # 1280 | # Net::HTTP.start(host, port) do |http| 1281 | # http.get("/path") do |chunk| 1282 | # 1283 | # end 1284 | # 1285 | # http.get("/another_path") do |chunk| 1286 | # 1287 | # end 1288 | # end 1289 | unless started? 1290 | start { 1291 | req['connection'] ||= 'close' 1292 | return request(req, body, &block) 1293 | } 1294 | end 1295 | if proxy_user() 1296 | req.proxy_basic_auth proxy_user(), proxy_pass() unless use_ssl? 1297 | end 1298 | req.set_body_internal body 1299 | res = transport_request(req, &block) 1300 | if sspi_auth?(res) 1301 | sspi_auth(req) 1302 | res = transport_request(req, &block) 1303 | end 1304 | res 1305 | end 1306 | 1307 | private 1308 | 1309 | # Executes a request which uses a representation 1310 | # and returns its body. 1311 | def send_entity(path, data, initheader, dest, type, &block) 1312 | res = nil 1313 | request(type.new(path, initheader), data) {|r| 1314 | r.read_body dest, &block 1315 | r.close 1316 | res = r 1317 | } 1318 | res 1319 | end 1320 | 1321 | def transport_request(req) 1322 | begin_transport req 1323 | 1324 | req.exec @socket, @curr_http_version, edit_path(req.path) 1325 | begin 1326 | res = HTTPResponse.read_new(@socket) 1327 | end while res.kind_of?(HTTPContinue) 1328 | 1329 | res.request = req 1330 | 1331 | if block_given? 1332 | yield res 1333 | res.close 1334 | end 1335 | end_transport req, res, block_given? 1336 | @current_response = res 1337 | res 1338 | rescue => exception 1339 | D "Conn close because of error #{exception}" 1340 | @socket.close if @socket and not @socket.closed? 1341 | raise exception 1342 | end 1343 | 1344 | def begin_transport(req) 1345 | if @socket.closed? 1346 | connect 1347 | else 1348 | if @current_response && !@current_response.finished? 1349 | raise "You can't start a new request on the same socket until you have read the entirety of the previous request" 1350 | end 1351 | end 1352 | 1353 | # If close_on_empty_response is set, and the response is not 1354 | # allowed to have a body (i.e. HEAD requests), turn off keepalive 1355 | if not req.response_body_permitted? and @close_on_empty_response 1356 | req['connection'] ||= 'close' 1357 | end 1358 | 1359 | req['host'] ||= addr_port 1360 | end 1361 | 1362 | def end_transport(req, res, block_form) 1363 | @curr_http_version = res.http_version 1364 | if @socket.closed? 1365 | D 'Conn socket closed' 1366 | elsif @close_on_empty_response && !res.body 1367 | D 'Conn close' 1368 | @socket.close 1369 | elsif keep_alive?(req, res) 1370 | D 'Conn keep-alive' 1371 | elsif block_form 1372 | D 'Conn close' 1373 | @socket.close 1374 | end 1375 | end 1376 | 1377 | def keep_alive?(req, res) 1378 | return false if req.connection_close? 1379 | if @curr_http_version <= '1.0' 1380 | res.connection_keep_alive? 1381 | else # HTTP/1.1 or later 1382 | not res.connection_close? 1383 | end 1384 | end 1385 | 1386 | def sspi_auth?(res) 1387 | return false unless @sspi_enabled 1388 | if res.kind_of?(HTTPProxyAuthenticationRequired) and 1389 | proxy? and res["Proxy-Authenticate"].include?("Negotiate") 1390 | begin 1391 | require 'win32/sspi' 1392 | true 1393 | rescue LoadError 1394 | false 1395 | end 1396 | else 1397 | false 1398 | end 1399 | end 1400 | 1401 | def sspi_auth(req) 1402 | n = Win32::SSPI::NegotiateAuth.new 1403 | req["Proxy-Authorization"] = "Negotiate #{n.get_initial_token}" 1404 | # Some versions of ISA will close the connection if this isn't present. 1405 | req["Connection"] = "Keep-Alive" 1406 | req["Proxy-Connection"] = "Keep-Alive" 1407 | res = transport_request(req) 1408 | authphrase = res["Proxy-Authenticate"] or return res 1409 | req["Proxy-Authorization"] = "Negotiate #{n.complete_authentication(authphrase)}" 1410 | rescue => err 1411 | raise HTTPAuthenticationError.new('HTTP authentication failed', err) 1412 | end 1413 | 1414 | # 1415 | # utils 1416 | # 1417 | 1418 | private 1419 | 1420 | def addr_port 1421 | if use_ssl? 1422 | address + (port == HTTP.https_default_port ? '' : ":#{port()}") 1423 | else 1424 | address + (port == HTTP.http_default_port ? '' : ":#{port()}") 1425 | end 1426 | end 1427 | 1428 | def D(msg) 1429 | return unless @debug_output 1430 | @debug_output << msg 1431 | @debug_output << "\n" 1432 | end 1433 | 1434 | end 1435 | 1436 | HTTPSession = HTTP 1437 | 1438 | require "net2/http/header" 1439 | require "net2/http/generic_request" 1440 | require "net2/http/request" 1441 | 1442 | ### 1443 | ### Response 1444 | ### 1445 | 1446 | # HTTP response class. 1447 | # 1448 | # This class wraps together the response header and the response body (the 1449 | # entity requested). 1450 | # 1451 | # It mixes in the HTTPHeader module, which provides access to response 1452 | # header values both via hash-like methods and via individual readers. 1453 | # 1454 | # Note that each possible HTTP response code defines its own 1455 | # HTTPResponse subclass. These are listed below. 1456 | # 1457 | # All classes are 1458 | # defined under the Net module. Indentation indicates inheritance. 1459 | # 1460 | # xxx HTTPResponse 1461 | # 1462 | # 1xx HTTPInformation 1463 | # 100 HTTPContinue 1464 | # 101 HTTPSwitchProtocol 1465 | # 1466 | # 2xx HTTPSuccess 1467 | # 200 HTTPOK 1468 | # 201 HTTPCreated 1469 | # 202 HTTPAccepted 1470 | # 203 HTTPNonAuthoritativeInformation 1471 | # 204 HTTPNoContent 1472 | # 205 HTTPResetContent 1473 | # 206 HTTPPartialContent 1474 | # 1475 | # 3xx HTTPRedirection 1476 | # 300 HTTPMultipleChoice 1477 | # 301 HTTPMovedPermanently 1478 | # 302 HTTPFound 1479 | # 303 HTTPSeeOther 1480 | # 304 HTTPNotModified 1481 | # 305 HTTPUseProxy 1482 | # 307 HTTPTemporaryRedirect 1483 | # 1484 | # 4xx HTTPClientError 1485 | # 400 HTTPBadRequest 1486 | # 401 HTTPUnauthorized 1487 | # 402 HTTPPaymentRequired 1488 | # 403 HTTPForbidden 1489 | # 404 HTTPNotFound 1490 | # 405 HTTPMethodNotAllowed 1491 | # 406 HTTPNotAcceptable 1492 | # 407 HTTPProxyAuthenticationRequired 1493 | # 408 HTTPRequestTimeOut 1494 | # 409 HTTPConflict 1495 | # 410 HTTPGone 1496 | # 411 HTTPLengthRequired 1497 | # 412 HTTPPreconditionFailed 1498 | # 413 HTTPRequestEntityTooLarge 1499 | # 414 HTTPRequestURITooLong 1500 | # 415 HTTPUnsupportedMediaType 1501 | # 416 HTTPRequestedRangeNotSatisfiable 1502 | # 417 HTTPExpectationFailed 1503 | # 1504 | # 5xx HTTPServerError 1505 | # 500 HTTPInternalServerError 1506 | # 501 HTTPNotImplemented 1507 | # 502 HTTPBadGateway 1508 | # 503 HTTPServiceUnavailable 1509 | # 504 HTTPGatewayTimeOut 1510 | # 505 HTTPVersionNotSupported 1511 | # 1512 | # xxx HTTPUnknownResponse 1513 | # 1514 | class HTTP 1515 | class Response 1516 | CODE_CLASS_TO_OBJ = {} 1517 | CODE_TO_OBJ = {} 1518 | 1519 | # true if the response has a body. 1520 | def self.body_permitted? 1521 | self::HAS_BODY 1522 | end 1523 | 1524 | def self.exception_type # :nodoc: internal use only 1525 | self::EXCEPTION_TYPE 1526 | end 1527 | end # reopened after 1528 | end 1529 | 1530 | HTTPResponse = HTTP::Response 1531 | 1532 | require "net2/http/statuses" 1533 | require "net2/http/response" 1534 | 1535 | # :enddoc: 1536 | 1537 | #-- 1538 | # for backward compatibility 1539 | class HTTP 1540 | ProxyMod = ProxyDelta 1541 | end 1542 | module NetPrivate 1543 | HTTPRequest = ::Net2::HTTPRequest 1544 | end 1545 | 1546 | HTTPInformationCode = HTTPInformation 1547 | HTTPSuccessCode = HTTPSuccess 1548 | HTTPRedirectionCode = HTTPRedirection 1549 | HTTPRetriableCode = HTTPRedirection 1550 | HTTPClientErrorCode = HTTPClientError 1551 | HTTPFatalErrorCode = HTTPClientError 1552 | HTTPServerErrorCode = HTTPServerError 1553 | HTTPResponceReceiver = HTTPResponse 1554 | 1555 | end # module Net 1556 | 1557 | -------------------------------------------------------------------------------- /lib/net2/http/generic_request.rb: -------------------------------------------------------------------------------- 1 | require "net2/http/header" 2 | require "rbconfig" 3 | 4 | module Net2 5 | class HTTP 6 | # HTTPGenericRequest is the parent of the HTTPRequest class. 7 | # Do not use this directly; use a subclass of HTTPRequest. 8 | # 9 | # Mixes in the HTTPHeader module to provide easier access to HTTP headers. 10 | # 11 | class GenericRequest 12 | 13 | include Header 14 | 15 | config = Config::CONFIG 16 | engine = defined?(RUBY_ENGINE) ? RUBY_ENGINE : "ruby" 17 | 18 | HTTP_ACCEPT_ENCODING = "gzip;q=1.0,deflate;q=0.6,identity;q=0.3" if HAVE_ZLIB 19 | HTTP_ACCEPT = "*/*" 20 | HTTP_USER_AGENT = "Ruby/#{RUBY_VERSION} (#{engine}) #{RUBY_DESCRIPTION}" 21 | 22 | def initialize(m, req_body_allowed, resp_body_allowed, path, headers = nil) 23 | raise ArgumentError, "no HTTP request path given" unless path 24 | raise ArgumentError, "HTTP request path is empty" if path.empty? 25 | 26 | @method = m 27 | @path = path 28 | 29 | @request_has_body = req_body_allowed 30 | @response_has_body = resp_body_allowed 31 | 32 | self.headers = headers 33 | 34 | self['Accept-Encoding'] ||= HTTP_ACCEPT_ENCODING if HTTP_ACCEPT_ENCODING 35 | self['Accept'] ||= HTTP_ACCEPT 36 | self['User-Agent'] ||= HTTP_USER_AGENT 37 | 38 | @body = @body_stream = @body_data = nil 39 | end 40 | 41 | attr_reader :method 42 | attr_reader :path 43 | 44 | def inspect 45 | "\#<#{self.class} #{@method}>" 46 | end 47 | 48 | def request_body_permitted? 49 | @request_has_body 50 | end 51 | 52 | def response_body_permitted? 53 | @response_has_body 54 | end 55 | 56 | def body_exist? 57 | warn "Net::HTTPRequest#body_exist? is obsolete; use response_body_permitted?" if $VERBOSE 58 | response_body_permitted? 59 | end 60 | 61 | attr_reader :body 62 | 63 | def body=(str) 64 | return self.body_stream = str if str.respond_to?(:read) 65 | 66 | @body = str 67 | @body_stream = nil 68 | @body_data = nil 69 | str 70 | end 71 | 72 | attr_reader :body_stream 73 | 74 | def body_stream=(input) 75 | @body = nil 76 | @body_stream = input 77 | @body_data = nil 78 | input 79 | end 80 | 81 | def set_body_internal(str) #:nodoc: internal use only 82 | raise ArgumentError, "both of body argument and HTTPRequest#body set" if str and (@body or @body_stream) 83 | self.body = str if str 84 | end 85 | 86 | # 87 | # write 88 | # 89 | 90 | def exec(sock, ver, path) #:nodoc: internal use only 91 | if @body 92 | request_with_body sock, ver, path 93 | elsif @body_stream 94 | request_with_stream sock, ver, path 95 | elsif @body_data 96 | request_with_data sock, ver, path 97 | else 98 | write_header sock, ver, path 99 | end 100 | end 101 | 102 | private 103 | 104 | def supply_default_content_type 105 | return if content_type 106 | warn 'net/http: warning: Content-Type did not set; using application/x-www-form-urlencoded' if $VERBOSE 107 | set_content_type 'application/x-www-form-urlencoded' 108 | end 109 | 110 | def write_header(sock, ver, path) 111 | buf = "#{@method} #{path} HTTP/#{ver}\r\n" 112 | each_capitalized do |k,v| 113 | buf << "#{k}: #{v}\r\n" 114 | end 115 | buf << "\r\n" 116 | sock.write buf 117 | end 118 | 119 | def request_with_body(sock, ver, path, body = @body) 120 | self.content_length = body.bytesize 121 | delete 'Transfer-Encoding' 122 | 123 | supply_default_content_type 124 | write_header sock, ver, path 125 | 126 | sock.write body 127 | end 128 | 129 | def request_with_stream(sock, ver, path, f = @body_stream) 130 | unless content_length or chunked? 131 | raise ArgumentError, 132 | "Content-Length not given and Transfer-Encoding is not `chunked'" 133 | end 134 | 135 | supply_default_content_type 136 | write_header sock, ver, path 137 | 138 | if chunked? 139 | while s = f.read(1024) 140 | sock.write "#{s.length}\r\n#{s}\r\n" 141 | end 142 | sock.write "0\r\n\r\n" 143 | else 144 | while s = f.read(1024) 145 | sock.write s 146 | end 147 | end 148 | end 149 | 150 | def request_with_data(sock, ver, path, params = @body_data) 151 | # normalize URL encoded requests to normal requests with body 152 | if /\Amultipart\/form-data\z/i !~ self.content_type 153 | self.content_type = 'application/x-www-form-urlencoded' 154 | @body = URI.encode_www_form(params) 155 | return exec(sock, ver, path) 156 | end 157 | 158 | opt = @form_option.dup 159 | opt[:boundary] ||= SecureRandom.urlsafe_base64(40) 160 | self.set_content_type(self.content_type, :boundary => opt[:boundary]) 161 | if chunked? 162 | write_header sock, ver, path 163 | encode_multipart_form_data(sock, params, opt) 164 | else 165 | require 'tempfile' 166 | file = Tempfile.new('multipart') 167 | file.binmode 168 | encode_multipart_form_data(file, params, opt) 169 | file.rewind 170 | self.content_length = file.size 171 | write_header sock, ver, path 172 | IO.copy_stream(file, sock) 173 | end 174 | end 175 | 176 | def encode_multipart_form_data(out, params, opt) 177 | charset = opt[:charset] 178 | boundary = opt[:boundary] 179 | boundary ||= SecureRandom.urlsafe_base64(40) 180 | chunked_p = chunked? 181 | 182 | buf = '' 183 | params.each do |key, value, h| 184 | h ||= {} 185 | 186 | key = quote_string(key, charset) 187 | filename = 188 | h.key?(:filename) ? h[:filename] : 189 | value.respond_to?(:to_path) ? File.basename(value.to_path) : 190 | nil 191 | 192 | buf << "--#{boundary}\r\n" 193 | if filename 194 | filename = quote_string(filename, charset) 195 | type = h[:content_type] || 'application/octet-stream' 196 | buf << "Content-Disposition: form-data; " \ 197 | "name=\"#{key}\"; filename=\"#{filename}\"\r\n" \ 198 | "Content-Type: #{type}\r\n\r\n" 199 | if !out.respond_to?(:write) || !value.respond_to?(:read) 200 | # if +out+ is not an IO or +value+ is not an IO 201 | buf << (value.respond_to?(:read) ? value.read : value) 202 | elsif value.respond_to?(:size) && chunked_p 203 | # if +out+ is an IO and +value+ is a File, use IO.copy_stream 204 | flush_buffer(out, buf, chunked_p) 205 | out << "%x\r\n" % value.size if chunked_p 206 | IO.copy_stream(value, out) 207 | out << "\r\n" if chunked_p 208 | else 209 | # +out+ is an IO, and +value+ is not a File but an IO 210 | flush_buffer(out, buf, chunked_p) 211 | 1 while flush_buffer(out, value.read(4096), chunked_p) 212 | end 213 | else 214 | # non-file field: 215 | # HTML5 says, "The parts of the generated multipart/form-data 216 | # resource that correspond to non-file fields must not have a 217 | # Content-Type header specified." 218 | buf << "Content-Disposition: form-data; name=\"#{key}\"\r\n\r\n" 219 | buf << (value.respond_to?(:read) ? value.read : value) 220 | end 221 | buf << "\r\n" 222 | end 223 | buf << "--#{boundary}--\r\n" 224 | flush_buffer(out, buf, chunked_p) 225 | out << "0\r\n\r\n" if chunked_p 226 | end 227 | 228 | def quote_string(str, charset) 229 | str = str.encode(charset, :fallback => lambda {|c| '&#%d;'%c.encode("UTF-8").ord}) if charset 230 | str = str.gsub(/[\\"]/, '\\\\\&') 231 | end 232 | 233 | def flush_buffer(out, buf, chunked_p) 234 | return unless buf 235 | out << "%x\r\n"%buf.bytesize if chunked_p 236 | out << buf 237 | out << "\r\n" if chunked_p 238 | buf.gsub!(/\A.*\z/m, '') 239 | end 240 | 241 | end 242 | end 243 | 244 | HTTPGenericRequest = HTTP::GenericRequest 245 | end 246 | -------------------------------------------------------------------------------- /lib/net2/http/header.rb: -------------------------------------------------------------------------------- 1 | module Net2 2 | class HTTP 3 | # The HTTPHeader module defines methods for reading and writing 4 | # HTTP headers. 5 | # 6 | # It is used as a mixin by other classes, to provide hash-like 7 | # access to HTTP header values. Unlike raw hash access, HTTPHeader 8 | # provides access via case-insensitive keys. It also provides 9 | # methods for accessing commonly-used HTTP header values in more 10 | # convenient formats. 11 | # 12 | module Header 13 | 14 | def initialize_http_header(headers) 15 | @header = {} 16 | return unless headers 17 | 18 | headers.each do |key, value| 19 | warn "net/http: warning: duplicated HTTP header: #{key}" if $VERBOSE && key?(key) 20 | @header[key.downcase] = [value.strip] 21 | end 22 | end 23 | 24 | alias headers= initialize_http_header 25 | 26 | def size #:nodoc: obsolete 27 | @header.size 28 | end 29 | 30 | alias length size #:nodoc: obsolete 31 | 32 | # Returns the header field corresponding to the case-insensitive key. 33 | # For example, a key of "Content-Type" might return "text/html" 34 | def [](key) 35 | a = @header[key.downcase] or return nil 36 | a.join(', ') 37 | end 38 | 39 | # Sets the header field corresponding to the case-insensitive key. 40 | def []=(key, val) 41 | unless val 42 | @header.delete key.downcase 43 | return val 44 | end 45 | @header[key.downcase] = [val] 46 | end 47 | 48 | # [Ruby 1.8.3] 49 | # Adds a value to a named header field, instead of replacing its value. 50 | # Second argument +val+ must be a String. 51 | # See also #[]=, #[] and #get_fields. 52 | # 53 | # request.add_field 'X-My-Header', 'a' 54 | # p request['X-My-Header'] #=> "a" 55 | # p request.get_fields('X-My-Header') #=> ["a"] 56 | # request.add_field 'X-My-Header', 'b' 57 | # p request['X-My-Header'] #=> "a, b" 58 | # p request.get_fields('X-My-Header') #=> ["a", "b"] 59 | # request.add_field 'X-My-Header', 'c' 60 | # p request['X-My-Header'] #=> "a, b, c" 61 | # p request.get_fields('X-My-Header') #=> ["a", "b", "c"] 62 | # 63 | def add_field(key, val) 64 | if @header.key?(key.downcase) 65 | @header[key.downcase].push val 66 | else 67 | @header[key.downcase] = [val] 68 | end 69 | end 70 | 71 | # [Ruby 1.8.3] 72 | # Returns an array of header field strings corresponding to the 73 | # case-insensitive +key+. This method allows you to get duplicated 74 | # header fields without any processing. See also #[]. 75 | # 76 | # p response.get_fields('Set-Cookie') 77 | # #=> ["session=al98axx; expires=Fri, 31-Dec-1999 23:58:23", 78 | # "query=rubyscript; expires=Fri, 31-Dec-1999 23:58:23"] 79 | # p response['Set-Cookie'] 80 | # #=> "session=al98axx; expires=Fri, 31-Dec-1999 23:58:23, query=rubyscript; expires=Fri, 31-Dec-1999 23:58:23" 81 | # 82 | def get_fields(key) 83 | return nil unless @header[key.downcase] 84 | @header[key.downcase].dup 85 | end 86 | 87 | # Returns the header field corresponding to the case-insensitive key. 88 | # Returns the default value +args+, or the result of the block, or 89 | # raises an IndexError if there's no header field named +key+ 90 | # See Hash#fetch 91 | def fetch(key, *args, &block) #:yield: +key+ 92 | a = @header.fetch(key.downcase, *args, &block) 93 | a.kind_of?(Array) ? a.join(', ') : a 94 | end 95 | 96 | # Iterates through the header names and values, passing in the name 97 | # and value to the code block supplied. 98 | # 99 | # Example: 100 | # 101 | # response.header.each_header {|key,value| puts "#{key} = #{value}" } 102 | # 103 | def each_header #:yield: +key+, +value+ 104 | block_given? or return enum_for(__method__) 105 | @header.each do |k,va| 106 | yield k, va.join(', ') 107 | end 108 | end 109 | 110 | alias each each_header 111 | 112 | # Iterates through the header names in the header, passing 113 | # each header name to the code block. 114 | def each_name(&block) #:yield: +key+ 115 | block_given? or return enum_for(__method__) 116 | @header.each_key(&block) 117 | end 118 | 119 | alias each_key each_name 120 | 121 | # Iterates through the header names in the header, passing 122 | # capitalized header names to the code block. 123 | # 124 | # Note that header names are capitalized systematically; 125 | # capitalization may not match that used by the remote HTTP 126 | # server in its response. 127 | def each_capitalized_name #:yield: +key+ 128 | block_given? or return enum_for(__method__) 129 | @header.each_key do |k| 130 | yield capitalize(k) 131 | end 132 | end 133 | 134 | # Iterates through header values, passing each value to the 135 | # code block. 136 | def each_value #:yield: +value+ 137 | block_given? or return enum_for(__method__) 138 | @header.each_value do |va| 139 | yield va.join(', ') 140 | end 141 | end 142 | 143 | # Removes a header field, specified by case-insensitive key. 144 | def delete(key) 145 | @header.delete(key.downcase) 146 | end 147 | 148 | # true if +key+ header exists. 149 | def key?(key) 150 | @header.key?(key.downcase) 151 | end 152 | 153 | # Returns a Hash consisting of header names and values. 154 | # e.g. 155 | # {"cache-control" => "private", 156 | # "content-type" => "text/html", 157 | # "date" => "Wed, 22 Jun 2005 22:11:50 GMT"} 158 | def to_hash 159 | @header.dup 160 | end 161 | 162 | # As for #each_header, except the keys are provided in capitalized form. 163 | # 164 | # Note that header names are capitalized systematically; 165 | # capitalization may not match that used by the remote HTTP 166 | # server in its response. 167 | def each_capitalized 168 | block_given? or return enum_for(__method__) 169 | @header.each do |k,v| 170 | yield capitalize(k), v.join(', ') 171 | end 172 | end 173 | 174 | alias canonical_each each_capitalized 175 | 176 | def capitalize(name) 177 | name.split(/-/).map {|s| s.capitalize }.join('-') 178 | end 179 | private :capitalize 180 | 181 | # Returns an Array of Range objects which represent the Range: 182 | # HTTP header field, or +nil+ if there is no such header. 183 | def range 184 | return nil unless @header['range'] 185 | self['Range'].split(/,/).map {|spec| 186 | m = /bytes\s*=\s*(\d+)?\s*-\s*(\d+)?/i.match(spec) or 187 | raise HTTPHeaderSyntaxError, "wrong Range: #{spec}" 188 | d1 = m[1].to_i 189 | d2 = m[2].to_i 190 | if m[1] and m[2] then d1..d2 191 | elsif m[1] then d1..-1 192 | elsif m[2] then -d2..-1 193 | else 194 | raise HTTPHeaderSyntaxError, 'range is not specified' 195 | end 196 | } 197 | end 198 | 199 | # Sets the HTTP Range: header. 200 | # Accepts either a Range object as a single argument, 201 | # or a beginning index and a length from that index. 202 | # Example: 203 | # 204 | # req.range = (0..1023) 205 | # req.set_range 0, 1023 206 | # 207 | def set_range(r, e = nil) 208 | unless r 209 | @header.delete 'range' 210 | return r 211 | end 212 | r = (r...r+e) if e 213 | case r 214 | when Numeric 215 | n = r.to_i 216 | rangestr = (n > 0 ? "0-#{n-1}" : "-#{-n}") 217 | when Range 218 | first = r.first 219 | last = r.last 220 | last -= 1 if r.exclude_end? 221 | if last == -1 222 | rangestr = (first > 0 ? "#{first}-" : "-#{-first}") 223 | else 224 | raise HTTPHeaderSyntaxError, 'range.first is negative' if first < 0 225 | raise HTTPHeaderSyntaxError, 'range.last is negative' if last < 0 226 | raise HTTPHeaderSyntaxError, 'must be .first < .last' if first > last 227 | rangestr = "#{first}-#{last}" 228 | end 229 | else 230 | raise TypeError, 'Range/Integer is required' 231 | end 232 | @header['range'] = ["bytes=#{rangestr}"] 233 | r 234 | end 235 | 236 | alias range= set_range 237 | 238 | # Returns an Integer object which represents the HTTP Content-Length: 239 | # header field, or +nil+ if that field was not provided. 240 | def content_length 241 | return nil unless key?('Content-Length') 242 | len = self['Content-Length'].slice(/\d+/) or 243 | raise HTTPHeaderSyntaxError, 'wrong Content-Length format' 244 | len.to_i 245 | end 246 | 247 | def content_length=(len) 248 | unless len 249 | @header.delete 'content-length' 250 | return nil 251 | end 252 | @header['content-length'] = [len.to_i.to_s] 253 | end 254 | 255 | # Returns "true" if the "transfer-encoding" header is present and 256 | # set to "chunked". This is an HTTP/1.1 feature, allowing the 257 | # the content to be sent in "chunks" without at the outset 258 | # stating the entire content length. 259 | def chunked? 260 | return false unless @header['transfer-encoding'] 261 | field = self['Transfer-Encoding'] 262 | (/(?:\A|[^\-\w])chunked(?![\-\w])/i =~ field) ? true : false 263 | end 264 | 265 | # Returns a Range object which represents the value of the Content-Range: 266 | # header field. 267 | # For a partial entity body, this indicates where this fragment 268 | # fits inside the full entity body, as range of byte offsets. 269 | def content_range 270 | return nil unless @header['content-range'] 271 | m = %ri.match(self['Content-Range']) or 272 | raise HTTPHeaderSyntaxError, 'wrong Content-Range format' 273 | m[1].to_i .. m[2].to_i 274 | end 275 | 276 | # The length of the range represented in Content-Range: header. 277 | def range_length 278 | r = content_range() or return nil 279 | r.end - r.begin + 1 280 | end 281 | 282 | # Returns a content type string such as "text/html". 283 | # This method returns nil if Content-Type: header field does not exist. 284 | def content_type 285 | return nil unless main_type() 286 | if sub_type() 287 | then "#{main_type()}/#{sub_type()}" 288 | else main_type() 289 | end 290 | end 291 | 292 | # Returns a content type string such as "text". 293 | # This method returns nil if Content-Type: header field does not exist. 294 | def main_type 295 | return nil unless @header['content-type'] 296 | self['Content-Type'].split(';').first.to_s.split('/')[0].to_s.strip 297 | end 298 | 299 | # Returns a content type string such as "html". 300 | # This method returns nil if Content-Type: header field does not exist 301 | # or sub-type is not given (e.g. "Content-Type: text"). 302 | def sub_type 303 | return nil unless @header['content-type'] 304 | _, sub = *self['Content-Type'].split(';').first.to_s.split('/') 305 | return nil unless sub 306 | sub.strip 307 | end 308 | 309 | # Any parameters specified for the content type, returned as a Hash. 310 | # For example, a header of Content-Type: text/html; charset=EUC-JP 311 | # would result in type_params returning {'charset' => 'EUC-JP'} 312 | def type_params 313 | result = {} 314 | list = self['Content-Type'].to_s.split(';') 315 | list.shift 316 | list.each do |param| 317 | k, v = *param.split('=', 2) 318 | result[k.strip] = v.strip 319 | end 320 | result 321 | end 322 | 323 | # Sets the content type in an HTTP header. 324 | # The +type+ should be a full HTTP content type, e.g. "text/html". 325 | # The +params+ are an optional Hash of parameters to add after the 326 | # content type, e.g. {'charset' => 'iso-8859-1'} 327 | def set_content_type(type, params = {}) 328 | @header['content-type'] = [type + params.map{|k,v|"; #{k}=#{v}"}.join('')] 329 | end 330 | 331 | alias content_type= set_content_type 332 | 333 | # Set header fields and a body from HTML form data. 334 | # +params+ should be an Array of Arrays or 335 | # a Hash containing HTML form data. 336 | # Optional argument +sep+ means data record separator. 337 | # 338 | # Values are URL encoded as necessary and the content-type is set to 339 | # application/x-www-form-urlencoded 340 | # 341 | # Example: 342 | # http.form_data = {"q" => "ruby", "lang" => "en"} 343 | # http.form_data = {"q" => ["ruby", "perl"], "lang" => "en"} 344 | # http.set_form_data({"q" => "ruby", "lang" => "en"}, ';') 345 | # 346 | def set_form_data(params, sep = '&') 347 | query = URI.encode_www_form(params) 348 | query.gsub!(/&/, sep) if sep != '&' 349 | self.body = query 350 | self.content_type = 'application/x-www-form-urlencoded' 351 | end 352 | 353 | alias form_data= set_form_data 354 | 355 | # Set a HTML form data set. 356 | # +params+ is the form data set; it is an Array of Arrays or a Hash 357 | # +enctype is the type to encode the form data set. 358 | # It is application/x-www-form-urlencoded or multipart/form-data. 359 | # +formpot+ is an optional hash to specify the detail. 360 | # 361 | # boundary:: the boundary of the multipart message 362 | # charset:: the charset of the message. All names and the values of 363 | # non-file fields are encoded as the charset. 364 | # 365 | # Each item of params is an array and contains following items: 366 | # +name+:: the name of the field 367 | # +value+:: the value of the field, it should be a String or a File 368 | # +opt+:: an optional hash to specify additional information 369 | # 370 | # Each item is a file field or a normal field. 371 | # If +value+ is a File object or the +opt+ have a filename key, 372 | # the item is treated as a file field. 373 | # 374 | # If Transfer-Encoding is set as chunked, this send the request in 375 | # chunked encoding. Because chunked encoding is HTTP/1.1 feature, 376 | # you must confirm the server to support HTTP/1.1 before sending it. 377 | # 378 | # Example: 379 | # http.set_form([["q", "ruby"], ["lang", "en"]]) 380 | # 381 | # See also RFC 2388, RFC 2616, HTML 4.01, and HTML5 382 | # 383 | def set_form(params, enctype='application/x-www-form-urlencoded', formopt={}) 384 | @body_data = params 385 | @body = nil 386 | @body_stream = nil 387 | @form_option = formopt 388 | case enctype 389 | when "application/x-www-form-urlencoded", "multipart/form-data" 390 | self.content_type = enctype 391 | else 392 | raise ArgumentError, "invalid enctype: #{enctype}" 393 | end 394 | end 395 | 396 | # Set the Authorization: header for "Basic" authorization. 397 | def basic_auth(account, password) 398 | @header['authorization'] = [basic_encode(account, password)] 399 | end 400 | 401 | # Set Proxy-Authorization: header for "Basic" authorization. 402 | def proxy_basic_auth(account, password) 403 | @header['proxy-authorization'] = [basic_encode(account, password)] 404 | end 405 | 406 | def basic_encode(account, password) 407 | 'Basic ' + ["#{account}:#{password}"].pack('m').delete("\r\n") 408 | end 409 | private :basic_encode 410 | 411 | def connection_close? 412 | tokens(@header['connection']).include?('close') or 413 | tokens(@header['proxy-connection']).include?('close') 414 | end 415 | 416 | def connection_keep_alive? 417 | tokens(@header['connection']).include?('keep-alive') or 418 | tokens(@header['proxy-connection']).include?('keep-alive') 419 | end 420 | 421 | def tokens(vals) 422 | return [] unless vals 423 | vals.map {|v| v.split(',') }.flatten\ 424 | .reject {|str| str.strip.empty? }\ 425 | .map {|tok| tok.strip.downcase } 426 | end 427 | private :tokens 428 | 429 | end 430 | end 431 | 432 | # Backward compatibility 433 | HTTPHeader = HTTP::Header 434 | end 435 | -------------------------------------------------------------------------------- /lib/net2/http/readers.rb: -------------------------------------------------------------------------------- 1 | module Net2 2 | class HTTP 3 | # TODO: Deal with keepalive using the same socket but not sharing a reader 4 | class Reader 5 | BUFSIZE = 1024 6 | 7 | def initialize(socket, endpoint) 8 | @socket = socket 9 | @endpoint = endpoint 10 | end 11 | 12 | def read(bufsize, timeout=60) 13 | while true 14 | read_to_endpoint(bufsize) 15 | 16 | break if eof? 17 | wait timeout 18 | end 19 | 20 | @endpoint 21 | end 22 | 23 | def read_nonblock(len) 24 | saw_content = read_to_endpoint(len) 25 | 26 | raise Errno::EWOULDBLOCK unless saw_content 27 | 28 | @endpoint 29 | end 30 | 31 | def wait(timeout=nil) 32 | io = @socket.to_io 33 | 34 | if io.is_a?(OpenSSL::SSL::SSLSocket) 35 | return if IO.select [io], [io], nil, timeout 36 | else 37 | return if IO.select [io], [io], nil, timeout 38 | end 39 | 40 | raise Timeout::Error 41 | end 42 | end 43 | 44 | class BodyReader < Reader 45 | def initialize(socket, endpoint, content_length) 46 | super(socket, endpoint) 47 | 48 | @content_length = content_length 49 | @read = 0 50 | @eof = false 51 | end 52 | 53 | def read(timeout=2) 54 | super @content_length, timeout 55 | end 56 | 57 | def read_to_endpoint(len=@content_length) 58 | if @content_length && len 59 | remain = @content_length - @read 60 | 61 | raise EOFError if remain.zero? 62 | len = remain if len > remain 63 | else 64 | len = BUFSIZE 65 | end 66 | 67 | begin 68 | output = @socket.read_nonblock(len) 69 | @endpoint << output 70 | @read += output.size 71 | rescue Errno::EWOULDBLOCK, Errno::EAGAIN, OpenSSL::SSL::SSLError 72 | return false 73 | rescue EOFError 74 | @eof = true 75 | end 76 | end 77 | 78 | def eof? 79 | return true if @eof 80 | @content_length - @read == 0 if @content_length 81 | end 82 | end 83 | 84 | class ChunkedBodyReader < Reader 85 | def initialize(socket, endpoint="") 86 | super(socket, endpoint) 87 | 88 | @raw_buffer = "" 89 | @out_buffer = "" 90 | 91 | @chunk_size = nil 92 | @state = :process_size 93 | 94 | @handled_trailer = false 95 | end 96 | 97 | def read_to_endpoint(len=nil) 98 | blocked = fill_buffer 99 | 100 | raise EOFError if eof? 101 | 102 | if blocked == :blocked 103 | return false if @raw_buffer.empty? && @out_buffer.empty? 104 | end 105 | 106 | send @state 107 | 108 | if !len 109 | @endpoint << @out_buffer 110 | @out_buffer = "" 111 | elsif @out_buffer.size > len 112 | @endpoint << @out_buffer.slice!(0, len) 113 | else 114 | @endpoint << @out_buffer 115 | @out_buffer = "" 116 | end 117 | 118 | if blocked == :eof 119 | @eof = true 120 | end 121 | 122 | return true 123 | end 124 | 125 | def read(timeout=60) 126 | super BUFSIZE, timeout 127 | end 128 | 129 | def eof? 130 | @eof && @raw_buffer.empty? && @out_buffer.empty? 131 | end 132 | 133 | private 134 | def fill_buffer 135 | return :eof if @eof 136 | ret = @socket.read_nonblock(BUFSIZE) 137 | @raw_buffer << ret 138 | return false 139 | rescue Errno::EWOULDBLOCK 140 | return :blocked 141 | rescue EOFError 142 | return :eof 143 | end 144 | 145 | def process_size 146 | idx = @raw_buffer.index("\r\n") 147 | return unless idx 148 | 149 | size_str = @raw_buffer.slice!(0, idx) 150 | @raw_buffer.slice!(0, 2) 151 | 152 | if size_str == "0" 153 | @state = :process_trailer 154 | process_trailer 155 | return 156 | end 157 | 158 | @size = size_str.to_i(16) 159 | 160 | @state = :process_chunk 161 | process_chunk 162 | end 163 | 164 | # TODO: Make this handle chunk metadata 165 | def process_chunk 166 | if @raw_buffer.size >= @size 167 | @out_buffer << @raw_buffer.slice!(0, @size) 168 | @state = :process_size 169 | process_size 170 | else 171 | @size -= @raw_buffer.size 172 | @out_buffer << @raw_buffer 173 | @raw_buffer = "" 174 | end 175 | end 176 | 177 | # TODO: Make this handle trailers 178 | def process_trailer 179 | idx = @raw_buffer.index("\r\n") 180 | return unless idx 181 | 182 | @raw_buffer.slice!(0, idx + 2) 183 | 184 | if idx == 0 185 | @state = :process_eof if idx == 0 186 | process_eof 187 | else 188 | process_trailer 189 | end 190 | end 191 | 192 | def process_eof 193 | raise EOFError if eof? 194 | @eof = true 195 | end 196 | end 197 | end 198 | end 199 | 200 | -------------------------------------------------------------------------------- /lib/net2/http/request.rb: -------------------------------------------------------------------------------- 1 | module Net2 2 | class HTTP 3 | # 4 | # HTTP request class. 5 | # This class wraps together the request header and the request path. 6 | # You cannot use this class directly. Instead, you should use one of its 7 | # subclasses: Net::HTTP::Get, Net::HTTP::Post, Net::HTTP::Head. 8 | # 9 | class Request < GenericRequest 10 | 11 | # Creates HTTP request object. 12 | def initialize(path, initheader = nil) 13 | super self.class::METHOD, 14 | self.class::REQUEST_HAS_BODY, 15 | self.class::RESPONSE_HAS_BODY, 16 | path, initheader 17 | end 18 | end 19 | 20 | 21 | # 22 | # HTTP/1.1 methods --- RFC2616 23 | # 24 | 25 | # See Net::HTTPGenericRequest for attributes and methods. 26 | # See Net::HTTP for usage examples. 27 | class Get < Request 28 | METHOD = 'GET' 29 | REQUEST_HAS_BODY = false 30 | RESPONSE_HAS_BODY = true 31 | end 32 | 33 | # See Net::HTTPGenericRequest for attributes and methods. 34 | # See Net::HTTP for usage examples. 35 | class Head < Request 36 | METHOD = 'HEAD' 37 | REQUEST_HAS_BODY = false 38 | RESPONSE_HAS_BODY = false 39 | end 40 | 41 | # See Net::HTTPGenericRequest for attributes and methods. 42 | # See Net::HTTP for usage examples. 43 | class Post < Request 44 | METHOD = 'POST' 45 | REQUEST_HAS_BODY = true 46 | RESPONSE_HAS_BODY = true 47 | end 48 | 49 | # See Net::HTTPGenericRequest for attributes and methods. 50 | # See Net::HTTP for usage examples. 51 | class Put < Request 52 | METHOD = 'PUT' 53 | REQUEST_HAS_BODY = true 54 | RESPONSE_HAS_BODY = true 55 | end 56 | 57 | # See Net::HTTPGenericRequest for attributes and methods. 58 | # See Net::HTTP for usage examples. 59 | class Delete < Request 60 | METHOD = 'DELETE' 61 | REQUEST_HAS_BODY = false 62 | RESPONSE_HAS_BODY = true 63 | end 64 | 65 | # See Net::HTTPGenericRequest for attributes and methods. 66 | class Options < Request 67 | METHOD = 'OPTIONS' 68 | REQUEST_HAS_BODY = false 69 | RESPONSE_HAS_BODY = false 70 | end 71 | 72 | # See Net::HTTPGenericRequest for attributes and methods. 73 | class Trace < Request 74 | METHOD = 'TRACE' 75 | REQUEST_HAS_BODY = false 76 | RESPONSE_HAS_BODY = true 77 | end 78 | 79 | # 80 | # PATCH method --- RFC5789 81 | # 82 | 83 | # See Net::HTTPGenericRequest for attributes and methods. 84 | class Patch < Request 85 | METHOD = 'PATCH' 86 | REQUEST_HAS_BODY = true 87 | RESPONSE_HAS_BODY = true 88 | end 89 | 90 | # 91 | # WebDAV methods --- RFC2518 92 | # 93 | 94 | # See Net::HTTPGenericRequest for attributes and methods. 95 | class Propfind < Request 96 | METHOD = 'PROPFIND' 97 | REQUEST_HAS_BODY = true 98 | RESPONSE_HAS_BODY = true 99 | end 100 | 101 | # See Net::HTTPGenericRequest for attributes and methods. 102 | class Proppatch < Request 103 | METHOD = 'PROPPATCH' 104 | REQUEST_HAS_BODY = true 105 | RESPONSE_HAS_BODY = true 106 | end 107 | 108 | # See Net::HTTPGenericRequest for attributes and methods. 109 | class Mkcol < Request 110 | METHOD = 'MKCOL' 111 | REQUEST_HAS_BODY = true 112 | RESPONSE_HAS_BODY = true 113 | end 114 | 115 | # See Net::HTTPGenericRequest for attributes and methods. 116 | class Copy < Request 117 | METHOD = 'COPY' 118 | REQUEST_HAS_BODY = false 119 | RESPONSE_HAS_BODY = true 120 | end 121 | 122 | # See Net::HTTPGenericRequest for attributes and methods. 123 | class Move < Request 124 | METHOD = 'MOVE' 125 | REQUEST_HAS_BODY = false 126 | RESPONSE_HAS_BODY = true 127 | end 128 | 129 | # See Net::HTTPGenericRequest for attributes and methods. 130 | class Lock < Request 131 | METHOD = 'LOCK' 132 | REQUEST_HAS_BODY = true 133 | RESPONSE_HAS_BODY = true 134 | end 135 | 136 | # See Net::HTTPGenericRequest for attributes and methods. 137 | class Unlock < Request 138 | METHOD = 'UNLOCK' 139 | REQUEST_HAS_BODY = true 140 | RESPONSE_HAS_BODY = true 141 | end 142 | end 143 | 144 | HTTPRequest = HTTP::Request 145 | end 146 | -------------------------------------------------------------------------------- /lib/net2/http/response.rb: -------------------------------------------------------------------------------- 1 | require "net2/http/header" 2 | require "net2/http/readers" 3 | 4 | module Net2 5 | class HTTP 6 | class Response # reopen 7 | 8 | class << self 9 | def read_new(sock) #:nodoc: internal use only 10 | httpv, code, msg = read_status_line(sock) 11 | res = response_class(code).new(httpv, code, msg) 12 | 13 | each_response_header(sock) { |k,v| res.add_field k, v } 14 | res.socket = sock 15 | 16 | res 17 | end 18 | 19 | private 20 | 21 | def read_status_line(sock) 22 | str = sock.readline 23 | m = /\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)\s*(.*)\z/in.match(str) 24 | raise HTTPBadResponse, "wrong status line: #{str.dump}" unless m 25 | m.captures 26 | end 27 | 28 | def response_class(code) 29 | CODE_TO_OBJ[code] or 30 | CODE_CLASS_TO_OBJ[code[0,1]] or 31 | HTTPUnknownResponse 32 | end 33 | 34 | # Read the beginning of the response, looking for headers 35 | def each_response_header(sock) 36 | key = value = nil 37 | 38 | while true 39 | # read until a newline 40 | line = sock.readuntil("\n", true).rstrip 41 | 42 | # empty line means we're done with headers 43 | break if line.empty? 44 | 45 | first = line[0] 46 | line.strip! 47 | 48 | # initial whitespace means it's part of the last header 49 | if first == ?\s || first == ?\t && value 50 | value << ' ' unless value.empty? 51 | value << line 52 | else 53 | yield key, value if key 54 | key, value = line.split(/\s*:\s*/, 2) 55 | raise HTTPBadResponse, 'wrong header line format' if value.nil? 56 | end 57 | end 58 | 59 | yield key, value if key 60 | end 61 | end 62 | 63 | include HTTPHeader 64 | 65 | def initialize(httpv, code, msg) #:nodoc: internal use only 66 | @http_version = httpv 67 | @code = code 68 | @message = msg 69 | @middlewares = [DecompressionMiddleware] 70 | @used = false 71 | 72 | initialize_http_header nil 73 | @body = nil 74 | @read = false 75 | end 76 | 77 | # The HTTP version supported by the server. 78 | attr_reader :http_version 79 | 80 | # The HTTP result code string. For example, '302'. You can also 81 | # determine the response type by examining which response subclass 82 | # the response object is an instance of. 83 | attr_reader :code 84 | 85 | attr_accessor :socket 86 | alias to_io socket 87 | 88 | attr_accessor :request 89 | 90 | # The HTTP result message sent by the server. For example, 'Not Found'. 91 | attr_reader :message 92 | alias msg message # :nodoc: obsolete 93 | 94 | def inspect 95 | "#<#{self.class} #{@code} #{@message} readbody=#{@read}>" 96 | end 97 | 98 | # 99 | # response <-> exception relationship 100 | # 101 | 102 | def code_type #:nodoc: 103 | self.class 104 | end 105 | 106 | def error! #:nodoc: 107 | raise error_type().new(@code + ' ' + @message.dump, self) 108 | end 109 | 110 | def error_type #:nodoc: 111 | self.class::EXCEPTION_TYPE 112 | end 113 | 114 | # Raises an HTTP error if the response is not 2xx (success). 115 | def value 116 | error! unless self.kind_of?(HTTPSuccess) 117 | end 118 | 119 | # 120 | # header (for backward compatibility only; DO NOT USE) 121 | # 122 | 123 | def response #:nodoc: 124 | warn "#{caller(1)[0]}: warning: HTTPResponse#response is obsolete" if $VERBOSE 125 | self 126 | end 127 | 128 | def header #:nodoc: 129 | warn "#{caller(1)[0]}: warning: HTTPResponse#header is obsolete" if $VERBOSE 130 | self 131 | end 132 | 133 | def read_header #:nodoc: 134 | warn "#{caller(1)[0]}: warning: HTTPResponse#read_header is obsolete" if $VERBOSE 135 | self 136 | end 137 | 138 | def body_exist? 139 | request.response_body_permitted? && self.class.body_permitted? 140 | end 141 | 142 | def finished? 143 | @closed 144 | end 145 | 146 | alias closed? finished? 147 | 148 | def eof? 149 | @nonblock_reader ? @nonblock_reader.eof? : finished? 150 | end 151 | 152 | # Gets the entity body returned by the remote HTTP server. 153 | # 154 | # If a block is given, the body is passed to the block, and 155 | # the body is provided in fragments, as it is read in from the socket. 156 | # 157 | # Calling this method a second or subsequent time for the same 158 | # HTTPResponse object will return the value already read. 159 | # 160 | # http.request_get('/index.html') {|res| 161 | # puts res.read_body 162 | # } 163 | # 164 | # http.request_get('/index.html') {|res| 165 | # p res.read_body.object_id # 538149362 166 | # p res.read_body.object_id # 538149362 167 | # } 168 | # 169 | # # using iterator 170 | # http.request_get('/index.html') {|res| 171 | # res.read_body do |segment| 172 | # print segment 173 | # end 174 | # } 175 | # 176 | def read_body(dest = nil, &block) 177 | if @read 178 | raise IOError, "#{self.class}\#read_body called twice" if dest or block 179 | return @body 180 | end 181 | 182 | if dest && block 183 | raise ArgumentError, 'both arg and block given for HTTP method' 184 | end 185 | 186 | if block 187 | @buffer = ReadAdapter.new(block) 188 | else 189 | @buffer = StringAdapter.new(dest || '') 190 | end 191 | 192 | to = build_pipeline(@buffer) 193 | 194 | stream_check 195 | if body_exist? 196 | read_body_0 to 197 | @body ||= begin 198 | @buffer.respond_to?(:string) ? @buffer.string : nil 199 | end 200 | else 201 | @body = nil 202 | end 203 | @read = @closed = true 204 | 205 | @body 206 | end 207 | 208 | # When using the same connection for multiple requests, a response 209 | # must be closed before the next request can be initiated. Closing 210 | # a response ensures that all of the expected body has been read 211 | # from the socket. 212 | def close 213 | return if @closed 214 | read_body 215 | end 216 | 217 | # Returns the full entity body. 218 | # 219 | # Calling this method a second or subsequent time will return the 220 | # string already read. 221 | # 222 | # http.request_get('/index.html') {|res| 223 | # puts res.body 224 | # } 225 | # 226 | # http.request_get('/index.html') {|res| 227 | # p res.body.object_id # 538149362 228 | # p res.body.object_id # 538149362 229 | # } 230 | # 231 | def body 232 | read_body 233 | end 234 | 235 | # Because it may be necessary to modify the body, Eg, decompression 236 | # this method facilitates that. 237 | def body=(value) 238 | @body = value 239 | end 240 | 241 | def read_nonblock(len) 242 | @nonblock_endpoint ||= begin 243 | @buf = "" 244 | @buf.force_encoding("BINARY") if @buf.respond_to?(:force_encoding) 245 | build_pipeline(@buf) 246 | end 247 | 248 | @nonblock_reader ||= reader(@nonblock_endpoint) 249 | 250 | @nonblock_reader.read_nonblock(len) 251 | 252 | @closed = @nonblock_reader.eof? 253 | 254 | @buf.slice!(0, @buf.size) 255 | end 256 | 257 | def wait(timeout=60) 258 | return unless @nonblock_reader 259 | 260 | @nonblock_reader.wait(timeout) 261 | end 262 | 263 | alias entity body #:nodoc: obsolete 264 | 265 | def use(middleware) 266 | @middlewares = [] unless @used 267 | @used = true 268 | @middlewares.unshift middleware 269 | end 270 | 271 | private 272 | 273 | def build_pipeline(buffer) 274 | pipeline = buffer 275 | @middlewares.each do |middleware| 276 | pipeline = middleware.new(pipeline, self) 277 | end 278 | pipeline 279 | end 280 | 281 | def read_body_0(dest) 282 | reader(dest).read 283 | end 284 | 285 | def reader(dest) 286 | if chunked? 287 | ChunkedBodyReader.new(@socket, dest) 288 | else 289 | # TODO: Why do Range lengths raise EOF? 290 | clen = content_length || range_length || nil 291 | BodyReader.new(@socket, dest, clen) 292 | end 293 | end 294 | 295 | def stream_check 296 | raise IOError, 'attempt to read body out of block' if @socket.closed? 297 | end 298 | 299 | class DecompressionMiddleware 300 | class NoopInflater 301 | def inflate(chunk) 302 | chunk 303 | end 304 | end 305 | 306 | def initialize(upstream, res) 307 | @upstream = upstream 308 | 309 | case res["Content-Encoding"] 310 | when 'gzip' 311 | @inflater = Zlib::Inflate.new Zlib::MAX_WBITS + 16 312 | when 'deflate' 313 | @inflater = Zlib::Inflate.new 314 | else 315 | @inflater = NoopInflater.new 316 | end 317 | end 318 | 319 | def <<(chunk) 320 | result = @inflater.inflate(chunk) 321 | @upstream << result 322 | end 323 | 324 | def close 325 | inflater.close 326 | end 327 | end 328 | 329 | class StringAdapter 330 | def initialize(upstream) 331 | @buffer = upstream 332 | end 333 | 334 | def <<(chunk) 335 | @buffer << chunk 336 | end 337 | 338 | def string 339 | @buffer 340 | end 341 | end 342 | 343 | end 344 | end 345 | end 346 | -------------------------------------------------------------------------------- /lib/net2/http/statuses.rb: -------------------------------------------------------------------------------- 1 | module Net2 2 | class HTTP 3 | # HTTP exception class. 4 | # You cannot use HTTPExceptions directly; instead, you must use 5 | # its subclasses. 6 | module Exceptions 7 | def initialize(msg, res) #:nodoc: 8 | super msg 9 | @response = res 10 | end 11 | attr_reader :response 12 | alias data response #:nodoc: obsolete 13 | end 14 | 15 | class Error < ProtocolError 16 | include Exceptions 17 | end 18 | 19 | class RetriableError < ProtoRetriableError 20 | include Exceptions 21 | end 22 | 23 | class ServerException < ProtoServerError 24 | # We cannot use the name "HTTPServerError", it is the name of the response. 25 | include Exceptions 26 | end 27 | 28 | class FatalError < ProtoFatalError 29 | include Exceptions 30 | end 31 | end 32 | 33 | class HTTP 34 | class Response 35 | def self.subclass(name, code, options = {}) 36 | klass = HTTP.const_set name, Class.new(self) 37 | klass.const_set :HAS_BODY, options.key?(:body) ? options[:body] : self::HAS_BODY 38 | klass.const_set :EXCEPTION_TYPE, options[:error] || self::EXCEPTION_TYPE 39 | 40 | # for backwards compatibility with Net::HTTP 41 | Net2.const_set "HTTP#{name}", klass 42 | 43 | return unless code 44 | 45 | if code < 100 46 | CODE_CLASS_TO_OBJ[code.to_s] = klass 47 | else 48 | CODE_TO_OBJ[code.to_s] = klass 49 | end 50 | 51 | end 52 | end 53 | 54 | Response.subclass :Information, 1, :body => false, :error => Error 55 | Response.subclass :Success, 3, :body => true, :error => Error 56 | Response.subclass :Redirection, 4, :body => true, :error => RetriableError 57 | Response.subclass :ClientError, 5, :body => true, :error => ServerException 58 | Response.subclass :ServerError, 6, :body => true, :error => FatalError 59 | 60 | Response.subclass :UnknownResponse, nil, :body => true, :error => Error 61 | 62 | Information.subclass :Continue, 100 63 | Information.subclass :SwitchProtocol, 101 64 | 65 | Success.subclass :OK, 200 66 | Success.subclass :Created, 201 67 | Success.subclass :Accepted, 202 68 | Success.subclass :NonAuthoritativeInformation, 203 69 | Success.subclass :NoContent, 204, :body => false 70 | Success.subclass :ResetContent, 205, :body => false 71 | Success.subclass :PartialContent, 206 72 | 73 | Redirection.subclass :MultipleChoice, 300 74 | Redirection.subclass :MovedPermanently, 301 75 | Redirection.subclass :Found, 302 76 | Redirection.subclass :SeeOther, 303 77 | Redirection.subclass :NotModified, 304, :body => false 78 | Redirection.subclass :UseProxy, 305, :body => false 79 | # 306 unused 80 | Redirection.subclass :TemporaryRedirect, 307 81 | 82 | MovedTemporarily = Found 83 | 84 | ClientError.subclass :BadRequest, 400 85 | ClientError.subclass :Unauthorized, 401 86 | ClientError.subclass :PaymentRequired, 402 87 | ClientError.subclass :Forbidden, 403 88 | ClientError.subclass :NotFound, 404 89 | ClientError.subclass :MethodNotAllowed, 405 90 | ClientError.subclass :NotAcceptable, 406 91 | ClientError.subclass :ProxyAuthenticationRequired, 407 92 | ClientError.subclass :RequestTimeOut, 408 93 | ClientError.subclass :Conflict, 409 94 | ClientError.subclass :Gone, 410 95 | ClientError.subclass :LengthRequired, 411 96 | ClientError.subclass :PreconditionFailed, 412 97 | ClientError.subclass :RequestEntityTooLarge, 413 98 | ClientError.subclass :RequestURITooLong, 414 99 | ClientError.subclass :UnsupportedMediaType, 415 100 | ClientError.subclass :RequestedRangeNotSatisfiable, 416 101 | ClientError.subclass :ExpectationFailed, 417 102 | 103 | RequestURITooLarge = RequestURITooLong 104 | 105 | ServerError.subclass :InternalServerError, 500 106 | ServerError.subclass :NotImplemented, 501 107 | ServerError.subclass :BadGateway, 502 108 | ServerError.subclass :ServiceUnavailable, 503 109 | ServerError.subclass :GatewayTimeOut, 504 110 | ServerError.subclass :VersionNotSupported, 505 111 | end 112 | 113 | # :startdoc: 114 | 115 | HTTPExceptions = HTTP::Exceptions 116 | HTTPError = HTTP::Error 117 | HTTPRetriableError = HTTP::RetriableError 118 | HTTPServerException = HTTP::ServerException 119 | HTTPFatalError = HTTP::FatalError 120 | end 121 | -------------------------------------------------------------------------------- /lib/net2/http/version.rb: -------------------------------------------------------------------------------- 1 | module Net2 2 | VERSION = "1.9.2" 3 | end 4 | -------------------------------------------------------------------------------- /lib/net2/https.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | 3 | = net/https -- SSL/TLS enhancement for Net::HTTP. 4 | 5 | This file has been merged with net/http. There is no longer any need to 6 | require 'net2/https' to use HTTPS. 7 | 8 | See Net::HTTP for details on how to make HTTPS connections. 9 | 10 | == Info 11 | 'OpenSSL for Ruby 2' project 12 | Copyright (C) 2001 GOTOU Yuuzou 13 | All rights reserved. 14 | 15 | == Licence 16 | This program is licenced under the same licence as Ruby. 17 | (See the file 'LICENCE'.) 18 | 19 | =end 20 | 21 | require 'net2/http' 22 | require 'openssl' 23 | 24 | -------------------------------------------------------------------------------- /lib/net2/protocol.rb: -------------------------------------------------------------------------------- 1 | # 2 | # = net/protocol.rb 3 | # 4 | #-- 5 | # Copyright (c) 1999-2004 Yukihiro Matsumoto 6 | # Copyright (c) 1999-2004 Minero Aoki 7 | # 8 | # written and maintained by Minero Aoki 9 | # 10 | # This program is free software. You can re-distribute and/or 11 | # modify this program under the same terms as Ruby itself, 12 | # Ruby Distribute License or GNU General Public License. 13 | # 14 | # $Id$ 15 | #++ 16 | # 17 | # WARNING: This file is going to remove. 18 | # Do not rely on the implementation written in this file. 19 | # 20 | 21 | require 'socket' 22 | require 'timeout' 23 | require 'net2/protocol' 24 | 25 | module Net2 # :nodoc: 26 | 27 | class Protocol #:nodoc: internal use only 28 | private 29 | def Protocol.protocol_param(name, val) 30 | module_eval(<<-End, __FILE__, __LINE__ + 1) 31 | def #{name} 32 | #{val} 33 | end 34 | End 35 | end 36 | end 37 | 38 | 39 | class ProtocolError < StandardError; end 40 | class ProtoSyntaxError < ProtocolError; end 41 | class ProtoFatalError < ProtocolError; end 42 | class ProtoUnknownError < ProtocolError; end 43 | class ProtoServerError < ProtocolError; end 44 | class ProtoAuthError < ProtocolError; end 45 | class ProtoCommandError < ProtocolError; end 46 | class ProtoRetriableError < ProtocolError; end 47 | ProtocRetryError = ProtoRetriableError 48 | 49 | 50 | class BufferedIO #:nodoc: internal use only 51 | def initialize(io) 52 | @io = io 53 | @read_timeout = 60 54 | @debug_output = nil 55 | @rbuf = '' 56 | end 57 | 58 | attr_reader :io 59 | attr_accessor :read_timeout 60 | attr_accessor :debug_output 61 | 62 | alias to_io io 63 | 64 | def inspect 65 | "#<#{self.class} io=#{@io}>" 66 | end 67 | 68 | def eof? 69 | @io.eof? 70 | end 71 | 72 | def closed? 73 | @io.closed? 74 | end 75 | 76 | def close 77 | @io.close 78 | end 79 | 80 | # 81 | # Read 82 | # 83 | 84 | public 85 | 86 | def read(len, dest = '', ignore_eof = false) 87 | LOG "reading #{len} bytes..." 88 | read_bytes = 0 89 | begin 90 | while read_bytes + @rbuf.size < len 91 | s = rbuf_consume(@rbuf.size) 92 | dest << s 93 | read_bytes += s.size 94 | rbuf_fill 95 | end 96 | s = rbuf_consume(len - read_bytes) 97 | dest << s 98 | read_bytes += s.size 99 | rescue EOFError 100 | raise unless ignore_eof 101 | end 102 | LOG "read #{read_bytes} bytes" 103 | dest 104 | end 105 | 106 | def read_nonblock(len) 107 | LOG "reading #{len} bytes from #{@io.inspect} in nonblock mode..." 108 | 109 | bufsize = @rbuf.size 110 | 111 | if bufsize > len 112 | rbuf_consume(len) 113 | else 114 | ret = rbuf_consume(bufsize) 115 | 116 | begin 117 | ret << @io.read_nonblock(len - bufsize) 118 | rescue Errno::EWOULDBLOCK, Errno::EAGAIN, OpenSSL::SSL::SSLError 119 | rescue EOFError 120 | raise if ret.empty? 121 | end 122 | 123 | ret 124 | end 125 | end 126 | 127 | def read_all(dest = '') 128 | LOG 'reading all...' 129 | read_bytes = 0 130 | begin 131 | while true 132 | dest << (s = rbuf_consume(@rbuf.size)) 133 | read_bytes += s.size 134 | rbuf_fill 135 | end 136 | rescue EOFError 137 | ; 138 | end 139 | LOG "read #{read_bytes} bytes" 140 | dest 141 | end 142 | 143 | def readuntil(terminator, ignore_eof = false) 144 | begin 145 | until idx = @rbuf.index(terminator) 146 | rbuf_fill 147 | end 148 | return rbuf_consume(idx + terminator.size) 149 | rescue EOFError 150 | raise unless ignore_eof 151 | return rbuf_consume(@rbuf.size) 152 | end 153 | end 154 | 155 | def readline 156 | readuntil("\n").chop 157 | end 158 | 159 | private 160 | 161 | BUFSIZE = 1024 * 16 162 | 163 | def rbuf_fill 164 | begin 165 | @rbuf << @io.read_nonblock(BUFSIZE) 166 | rescue Errno::EWOULDBLOCK, Errno::EAGAIN, OpenSSL::SSL::SSLError 167 | if @io.is_a?(OpenSSL::SSL::SSLSocket) 168 | if IO.select(nil, [@io], nil, @read_timeout) 169 | retry 170 | else 171 | raise Timeout::Error 172 | end 173 | else 174 | if IO.select([@io], nil, nil, @read_timeout) 175 | retry 176 | else 177 | raise Timeout::Error 178 | end 179 | end 180 | end 181 | end 182 | 183 | def rbuf_consume(len) 184 | s = @rbuf.slice!(0, len) 185 | LOG %Q[-> #{s.dump}\n] 186 | s 187 | end 188 | 189 | # 190 | # Write 191 | # 192 | 193 | public 194 | 195 | def write(str) 196 | writing { 197 | write0 str 198 | } 199 | end 200 | 201 | alias << write 202 | 203 | def writeline(str) 204 | writing { 205 | write0 str + "\r\n" 206 | } 207 | end 208 | 209 | private 210 | 211 | def writing 212 | @written_bytes = 0 213 | @debug_output << '<- ' if @debug_output 214 | yield 215 | @debug_output << "\n" if @debug_output 216 | bytes = @written_bytes 217 | @written_bytes = nil 218 | bytes 219 | end 220 | 221 | def write0(str) 222 | @debug_output << str.dump if @debug_output 223 | len = @io.write(str) 224 | @written_bytes += len 225 | len 226 | end 227 | 228 | # 229 | # Logging 230 | # 231 | 232 | private 233 | 234 | def LOG_off 235 | @save_debug_out = @debug_output 236 | @debug_output = nil 237 | end 238 | 239 | def LOG_on 240 | @debug_output = @save_debug_out 241 | end 242 | 243 | def LOG(msg) 244 | return unless @debug_output 245 | @debug_output << msg + "\n" 246 | end 247 | end 248 | 249 | 250 | class InternetMessageIO < BufferedIO #:nodoc: internal use only 251 | def initialize(io) 252 | super 253 | @wbuf = nil 254 | end 255 | 256 | # 257 | # Read 258 | # 259 | 260 | def each_message_chunk 261 | LOG 'reading message...' 262 | LOG_off() 263 | read_bytes = 0 264 | while (line = readuntil("\r\n")) != ".\r\n" 265 | read_bytes += line.size 266 | yield line.sub(/\A\./, '') 267 | end 268 | LOG_on() 269 | LOG "read message (#{read_bytes} bytes)" 270 | end 271 | 272 | # *library private* (cannot handle 'break') 273 | def each_list_item 274 | while (str = readuntil("\r\n")) != ".\r\n" 275 | yield str.chop 276 | end 277 | end 278 | 279 | def write_message_0(src) 280 | prev = @written_bytes 281 | each_crlf_line(src) do |line| 282 | write0 line.sub(/\A\./, '..') 283 | end 284 | @written_bytes - prev 285 | end 286 | 287 | # 288 | # Write 289 | # 290 | 291 | def write_message(src) 292 | LOG "writing message from #{src.class}" 293 | LOG_off() 294 | len = writing { 295 | using_each_crlf_line { 296 | write_message_0 src 297 | } 298 | } 299 | LOG_on() 300 | LOG "wrote #{len} bytes" 301 | len 302 | end 303 | 304 | def write_message_by_block(&block) 305 | LOG 'writing message from block' 306 | LOG_off() 307 | len = writing { 308 | using_each_crlf_line { 309 | begin 310 | block.call(WriteAdapter.new(self, :write_message_0)) 311 | rescue LocalJumpError 312 | # allow `break' from writer block 313 | end 314 | } 315 | } 316 | LOG_on() 317 | LOG "wrote #{len} bytes" 318 | len 319 | end 320 | 321 | private 322 | 323 | def using_each_crlf_line 324 | @wbuf = '' 325 | yield 326 | if not @wbuf.empty? # unterminated last line 327 | write0 @wbuf.chomp + "\r\n" 328 | elsif @written_bytes == 0 # empty src 329 | write0 "\r\n" 330 | end 331 | write0 ".\r\n" 332 | @wbuf = nil 333 | end 334 | 335 | def each_crlf_line(src) 336 | buffer_filling(@wbuf, src) do 337 | while line = @wbuf.slice!(/\A.*(?:\n|\r\n|\r(?!\z))/n) 338 | yield line.chomp("\n") + "\r\n" 339 | end 340 | end 341 | end 342 | 343 | def buffer_filling(buf, src) 344 | case src 345 | when String # for speeding up. 346 | 0.step(src.size - 1, 1024) do |i| 347 | buf << src[i, 1024] 348 | yield 349 | end 350 | when File # for speeding up. 351 | while s = src.read(1024) 352 | buf << s 353 | yield 354 | end 355 | else # generic reader 356 | src.each do |str| 357 | buf << str 358 | yield if buf.size > 1024 359 | end 360 | yield unless buf.empty? 361 | end 362 | end 363 | end 364 | 365 | 366 | # 367 | # The writer adapter class 368 | # 369 | class WriteAdapter 370 | def initialize(socket, method) 371 | @socket = socket 372 | @method_id = method 373 | end 374 | 375 | def inspect 376 | "#<#{self.class} socket=#{@socket.inspect}>" 377 | end 378 | 379 | def write(str) 380 | @socket.__send__(@method_id, str) 381 | end 382 | 383 | alias print write 384 | 385 | def <<(str) 386 | write str 387 | self 388 | end 389 | 390 | def puts(str = '') 391 | write str.chomp("\n") + "\n" 392 | end 393 | 394 | def printf(*args) 395 | write sprintf(*args) 396 | end 397 | end 398 | 399 | 400 | class ReadAdapter #:nodoc: internal use only 401 | def initialize(block) 402 | @block = block 403 | end 404 | 405 | def inspect 406 | "#<#{self.class}>" 407 | end 408 | 409 | def <<(str) 410 | call_block(str, &@block) if @block 411 | end 412 | 413 | private 414 | 415 | # This method is needed because @block must be called by yield, 416 | # not Proc#call. You can see difference when using `break' in 417 | # the block. 418 | def call_block(str) 419 | yield str 420 | end 421 | end 422 | 423 | 424 | module NetPrivate #:nodoc: obsolete 425 | Socket = ::Net2::InternetMessageIO 426 | end 427 | 428 | end # module Net 429 | 430 | -------------------------------------------------------------------------------- /net-http.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "net2/http/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "net2-http" 7 | s.version = Net2::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["Yehuda Katz"] 10 | s.email = ["wycats@gmail.com"] 11 | s.homepage = "http://www.yehudakatz.com" 12 | s.summary = %q{A number of improvements to Net::HTTP} 13 | s.description = File.read(File.expand_path("../README.markdown", __FILE__)) 14 | 15 | s.rubyforge_project = "net-http" 16 | 17 | s.files = `git ls-files`.split("\n") 18 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 19 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 20 | s.require_paths = ["lib"] 21 | end 22 | -------------------------------------------------------------------------------- /test/http_test_base.rb: -------------------------------------------------------------------------------- 1 | module TestNetHTTP_version_1_1_methods 2 | 3 | def test_s_get 4 | assert_equal $test_net_http_data, 5 | Net::HTTP.get(config('host'), '/', config('port')) 6 | 7 | assert_equal $test_net_http_data, 8 | Net::HTTP.get("http://#{config('host')}:#{config('port')}/") 9 | 10 | assert_equal $test_net_http_data, 11 | Net::HTTP.get(URI.parse("http://#{config('host')}:#{config('port')}/")) 12 | end 13 | 14 | def test_head 15 | start {|http| 16 | res = http.head('/') 17 | assert_kind_of Net::HTTPResponse, res 18 | assert_equal $test_net_http_data_type, res['Content-Type'] 19 | if !chunked? && !gzip? 20 | assert_equal $test_net_http_data.size, res['Content-Length'].to_i 21 | end 22 | } 23 | end 24 | 25 | def test_get_with_block_start 26 | socket = nil 27 | 28 | start do |http| 29 | socket = http.socket 30 | _test_get__get http 31 | assert_equal socket, http.socket 32 | _test_get__iter http 33 | assert_equal socket, http.socket 34 | _test_get__chunked http 35 | assert_equal socket, http.socket 36 | 37 | assert !socket.closed? 38 | 39 | resp = http.get "/", "Connection" => "close" 40 | resp.read_body 41 | assert socket.closed? 42 | 43 | http.get "/" 44 | assert socket != http.socket 45 | end 46 | 47 | start do |http| 48 | socket2 = http.socket 49 | assert socket2 != socket 50 | end 51 | end 52 | 53 | def test_get_with_no_block 54 | http = start 55 | 56 | socket = http.socket 57 | _test_get__get http 58 | assert_equal socket, http.socket 59 | _test_get__read_nonblock http 60 | assert_equal socket, http.socket 61 | _test_get__iter http 62 | assert_equal socket, http.socket 63 | _test_get__chunked http 64 | assert_equal socket, http.socket 65 | 66 | assert !socket.closed? 67 | 68 | resp = http.get "/", "Connection" => "close" 69 | resp.read_body 70 | assert socket.closed? 71 | 72 | http.get "/" 73 | assert socket != http.socket 74 | end 75 | 76 | def _test_get__get(http) 77 | res = http.get('/') 78 | assert_kind_of Net::HTTPResponse, res 79 | assert_kind_of String, res.body 80 | if !chunked? && !gzip? 81 | assert_not_nil res['content-length'] 82 | assert_equal $test_net_http_data.size, res['content-length'].to_i 83 | end 84 | assert_equal $test_net_http_data_type, res['Content-Type'] 85 | assert_equal $test_net_http_data.size, res.body.size 86 | assert_equal $test_net_http_data, res.body 87 | 88 | assert_nothing_raised { 89 | res, body = http.get('/', { 'User-Agent' => 'test' }.freeze) 90 | } 91 | end 92 | 93 | def _test_get__iter(http) 94 | buf = '' 95 | res = http.get('/') {|s| buf << s } 96 | assert_kind_of Net::HTTPResponse, res 97 | if !chunked? && !gzip? 98 | assert_not_nil res['content-length'] 99 | assert_equal $test_net_http_data.size, res['content-length'].to_i 100 | end 101 | assert_equal $test_net_http_data_type, res['Content-Type'] 102 | assert_equal $test_net_http_data.size, buf.size 103 | assert_equal $test_net_http_data, buf 104 | end 105 | 106 | def _test_get__chunked(http) 107 | buf = '' 108 | res = http.get('/') {|s| buf << s } 109 | assert_kind_of Net::HTTPResponse, res 110 | if !chunked? && !gzip? 111 | assert_not_nil res['content-length'] 112 | assert_equal $test_net_http_data.size, res['content-length'].to_i 113 | end 114 | assert_equal $test_net_http_data_type, res['Content-Type'] 115 | assert_equal $test_net_http_data.size, buf.size 116 | assert_equal $test_net_http_data, buf 117 | 118 | res = http.get '/' 119 | assert_kind_of Net::HTTPResponse, res 120 | if !chunked? && !gzip? 121 | assert_not_nil res['content-length'] 122 | assert_equal $test_net_http_data.size, res['content-length'].to_i 123 | end 124 | assert_equal $test_net_http_data_type, res['Content-Type'] 125 | assert_equal $test_net_http_data.size, res.body.size 126 | assert_equal $test_net_http_data, res.body 127 | end 128 | 129 | def _test_get__read_nonblock(http) 130 | res = nil 131 | buf = nil 132 | 133 | res = http.request_get('/') 134 | assert_kind_of Net::HTTPResponse, res 135 | 136 | buf = '' 137 | 138 | until res.finished? 139 | res.wait(1) 140 | chunk = res.read_nonblock(1000) 141 | buf << chunk 142 | end 143 | 144 | assert_raises EOFError do 145 | res.read_nonblock(10) 146 | end 147 | 148 | assert_equal $test_net_http_data_type, res['Content-Type'] 149 | assert_equal $test_net_http_data, buf 150 | 151 | assert_nothing_raised do 152 | res, body = http.get('/', { 'User-Agent' => 'test' }.freeze) 153 | end 154 | 155 | end 156 | 157 | def wait(socket, timeout=nil) 158 | io = socket.to_io 159 | 160 | if io.is_a?(OpenSSL::SSL::SSLSocket) 161 | return if IO.select nil, [io], nil, timeout 162 | else 163 | return if IO.select [io], nil, nil, timeout 164 | end 165 | 166 | raise Timeout::Error 167 | end 168 | 169 | def test_get__break 170 | i = 0 171 | start {|http| 172 | http.get('/') do |str| 173 | i += 1 174 | break 175 | end 176 | } 177 | assert_equal 1, i 178 | end 179 | 180 | def test_get__implicit_start 181 | res = new().get('/') 182 | assert_kind_of Net::HTTPResponse, res 183 | assert_kind_of String, res.body 184 | if !chunked? && !gzip? 185 | assert_not_nil res['content-length'] 186 | end 187 | assert_equal $test_net_http_data_type, res['Content-Type'] 188 | assert_equal $test_net_http_data.size, res.body.size 189 | assert_equal $test_net_http_data, res.body 190 | end 191 | 192 | def test_get2 193 | start {|http| 194 | http.get2('/') {|res| 195 | assert_kind_of Net::HTTPResponse, res 196 | assert_kind_of Net::HTTPResponse, res.header 197 | if !chunked? && !gzip? 198 | assert_not_nil res['content-length'] 199 | end 200 | assert_equal $test_net_http_data_type, res['Content-Type'] 201 | assert_kind_of String, res.body 202 | assert_kind_of String, res.entity 203 | assert_equal $test_net_http_data.size, res.body.size 204 | assert_equal $test_net_http_data, res.body 205 | assert_equal $test_net_http_data, res.entity 206 | } 207 | } 208 | end 209 | 210 | def test_post 211 | start {|http| 212 | _test_post__base http 213 | _test_post__file http 214 | } 215 | end 216 | 217 | def _test_post__base(http) 218 | uheader = {} 219 | uheader['Accept'] = 'application/octet-stream' 220 | data = 'post data' 221 | res = http.post('/', data) 222 | assert_kind_of Net::HTTPResponse, res 223 | assert_kind_of String, res.body 224 | assert_equal data, res.body 225 | assert_equal data, res.entity 226 | end 227 | 228 | def _test_post__file(http) 229 | data = 'post data' 230 | f = StringIO.new 231 | http.post('/', data, nil, f) 232 | assert_equal data, f.string 233 | end 234 | 235 | def test_s_post_form 236 | res = Net::HTTP.post_form( 237 | URI.parse("http://#{config('host')}:#{config('port')}/"), 238 | "a" => "x") 239 | assert_equal ["a=x"], res.body.split(/[;&]/).sort 240 | 241 | res = Net::HTTP.post_form( 242 | URI.parse("http://#{config('host')}:#{config('port')}/"), 243 | "a" => "x", 244 | "b" => "y") 245 | assert_equal ["a=x", "b=y"], res.body.split(/[;&]/).sort 246 | 247 | # TODO: Why is this failing? 248 | #res = Net::HTTP.post_form( 249 | #URI.parse("http://#{config('host')}:#{config('port')}/"), 250 | #"a" => ["x1", "x2"], 251 | #"b" => "y") 252 | #assert_equal ["a=x1", "a=x2", "b=y"], res.body.split(/[;&]/).sort 253 | end 254 | 255 | def test_patch 256 | start {|http| 257 | _test_patch__base http 258 | } 259 | end 260 | 261 | def _test_patch__base(http) 262 | uheader = {} 263 | uheader['Accept'] = 'application/octet-stream' 264 | data = 'patch data' 265 | res = http.patch('/', data) 266 | assert_kind_of Net::HTTPResponse, res 267 | assert_kind_of String, res.body 268 | assert_equal data, res.body 269 | assert_equal data, res.entity 270 | end 271 | 272 | def test_timeout_during_HTTP_session 273 | bug4246 = "expected the HTTP session to have timed out but have not. c.f. [ruby-core:34203]" 274 | 275 | # listen for connections... but deliberately do not complete SSL handshake 276 | TCPServer.open(0) {|server| 277 | port = server.addr[1] 278 | 279 | conn = Net::HTTP.new('localhost', port) 280 | conn.read_timeout = 1 281 | conn.open_timeout = 1 282 | 283 | th = Thread.new do 284 | assert_raise(Timeout::Error) { 285 | conn.get('/') 286 | } 287 | end 288 | assert th.join(10), bug4246 289 | } 290 | end 291 | end 292 | 293 | 294 | module TestNetHTTP_version_1_2_methods 295 | 296 | def test_request 297 | start {|http| 298 | _test_request__GET http 299 | _test_request__file http 300 | # _test_request__range http # WEBrick does not support Range: header. 301 | _test_request__HEAD http 302 | _test_request__POST http 303 | _test_request__stream_body http, :body_stream= 304 | _test_request__stream_body http, :body= 305 | 306 | } 307 | end 308 | 309 | def _test_request__GET(http) 310 | req = Net::HTTP::Get.new('/') 311 | http.request(req) {|res| 312 | assert_kind_of Net::HTTPResponse, res 313 | assert_kind_of String, res.body 314 | if !chunked? && !gzip? 315 | assert_not_nil res['content-length'] 316 | assert_equal $test_net_http_data.size, res['content-length'].to_i 317 | end 318 | assert_equal $test_net_http_data.size, res.body.size 319 | assert_equal $test_net_http_data, res.body 320 | } 321 | end 322 | 323 | def _test_request__file(http) 324 | req = Net::HTTP::Get.new('/') 325 | http.request(req) {|res| 326 | assert_kind_of Net::HTTPResponse, res 327 | if !chunked? && !gzip? 328 | assert_not_nil res['content-length'] 329 | assert_equal $test_net_http_data.size, res['content-length'].to_i 330 | end 331 | str = "" 332 | str.force_encoding("BINARY") if str.encoding_aware? 333 | f = StringIO.new(str) 334 | res.read_body f 335 | assert_equal $test_net_http_data.bytesize, f.string.bytesize 336 | assert_equal $test_net_http_data.encoding, f.string.encoding if f.string.encoding_aware? 337 | assert_equal $test_net_http_data, f.string 338 | } 339 | end 340 | 341 | def _test_request__range(http) 342 | req = Net::HTTP::Get.new('/') 343 | req['range'] = 'bytes=0-5' 344 | assert_equal $test_net_http_data[0,6], http.request(req).body 345 | end 346 | 347 | def _test_request__HEAD(http) 348 | req = Net::HTTP::Head.new('/') 349 | http.request(req) {|res| 350 | assert_kind_of Net::HTTPResponse, res 351 | if !chunked? && !gzip? 352 | assert_not_nil res['content-length'] 353 | assert_equal $test_net_http_data.size, res['content-length'].to_i 354 | end 355 | assert_nil res.body 356 | } 357 | end 358 | 359 | def _test_request__POST(http) 360 | data = 'post data' 361 | req = Net::HTTP::Post.new('/') 362 | req['Accept'] = $test_net_http_data_type 363 | http.request(req, data) {|res| 364 | assert_kind_of Net::HTTPResponse, res 365 | if !chunked? && !gzip? 366 | assert_equal data.size, res['content-length'].to_i 367 | end 368 | assert_kind_of String, res.body 369 | assert_equal data, res.body 370 | } 371 | end 372 | 373 | def _test_request__stream_body(http, method = :body_stream=) 374 | req = Net::HTTP::Post.new('/') 375 | data = $test_net_http_data 376 | req.content_length = data.size 377 | req.send method, StringIO.new(data) 378 | res = http.request(req) 379 | assert_kind_of Net::HTTPResponse, res 380 | assert_kind_of String, res.body 381 | assert_equal data.size, res.body.size 382 | assert_equal data, res.body 383 | end 384 | 385 | def test_send_request 386 | start {|http| 387 | _test_send_request__GET http 388 | _test_send_request__POST http 389 | } 390 | end 391 | 392 | def _test_send_request__GET(http) 393 | res = http.send_request('GET', '/') 394 | assert_kind_of Net::HTTPResponse, res 395 | if !chunked? && !gzip? 396 | assert_equal $test_net_http_data.size, res['content-length'].to_i 397 | end 398 | assert_kind_of String, res.body 399 | assert_equal $test_net_http_data, res.body 400 | end 401 | 402 | def _test_send_request__POST(http) 403 | data = 'aaabbb cc ddddddddddd lkjoiu4j3qlkuoa' 404 | res = http.send_request('POST', '/', data) 405 | assert_kind_of Net::HTTPResponse, res 406 | assert_kind_of String, res.body 407 | assert_equal data.size, res.body.size 408 | assert_equal data, res.body 409 | end 410 | 411 | def test_set_form 412 | require 'tempfile' 413 | file = Tempfile.new('ruby-test') 414 | file << "\xE3\x83\x87\xE3\x83\xBC\xE3\x82\xBF" 415 | #file << "\u{30c7}\u{30fc}\u{30bf}" 416 | data = [ 417 | ['name', 'Gonbei Nanashi'], 418 | ['name', "\xE5\x90\x8D\xE7\x84\xA1\xE3\x81\x97\xE3\x81\xAE\xE6\xA8\xA9\xE5\x85\xB5\xE8\xA1\x9B"], 419 | #['name', "\u{540d}\u{7121}\u{3057}\u{306e}\u{6a29}\u{5175}\u{885b}"], 420 | ['s"i\o', StringIO.new("\xE3\x81\x82\xE3\x81\x84\xE4\xBA\x9C\xE9\x89\x9B")], 421 | #['s"i\o', StringIO.new("\u{3042 3044 4e9c 925b}")], 422 | ["file", file, {:filename => "ruby-test"}] 423 | ] 424 | expected = <<"__EOM__".gsub(/\n/, "\r\n") 425 | -- 426 | Content-Disposition: form-data; name="name" 427 | 428 | Gonbei Nanashi 429 | -- 430 | Content-Disposition: form-data; name="name" 431 | 432 | \xE5\x90\x8D\xE7\x84\xA1\xE3\x81\x97\xE3\x81\xAE\xE6\xA8\xA9\xE5\x85\xB5\xE8\xA1\x9B 433 | -- 434 | Content-Disposition: form-data; name="s\\"i\\\\o" 435 | 436 | \xE3\x81\x82\xE3\x81\x84\xE4\xBA\x9C\xE9\x89\x9B 437 | -- 438 | Content-Disposition: form-data; name="file"; filename="ruby-test" 439 | Content-Type: application/octet-stream 440 | 441 | \xE3\x83\x87\xE3\x83\xBC\xE3\x82\xBF 442 | ---- 443 | __EOM__ 444 | start {|http| 445 | _test_set_form_urlencoded(http, data.reject{|k,v|!v.is_a?(String)}) 446 | _test_set_form_multipart(http, false, data, expected) 447 | _test_set_form_multipart(http, true, data, expected) 448 | } 449 | end 450 | 451 | def _test_set_form_urlencoded(http, data) 452 | req = Net::HTTP::Post.new('/') 453 | req.set_form(data) 454 | res = http.request req 455 | assert_equal "name=Gonbei+Nanashi&name=%E5%90%8D%E7%84%A1%E3%81%97%E3%81%AE%E6%A8%A9%E5%85%B5%E8%A1%9B", res.body 456 | end 457 | 458 | def _test_set_form_multipart(http, chunked_p, data, expected) 459 | data.each{|k,v|v.rewind rescue nil} 460 | req = Net::HTTP::Post.new('/') 461 | req.set_form(data, 'multipart/form-data') 462 | req['Transfer-Encoding'] = 'chunked' if chunked_p 463 | res = http.request req 464 | body = res.body 465 | assert_match(/\A--(\S+)/, body) 466 | /\A--(\S+)/ =~ body 467 | expected = expected.gsub(//, $1) 468 | assert_equal(expected, body) 469 | end 470 | 471 | def test_set_form_with_file 472 | require 'tempfile' 473 | file = Tempfile.new('ruby-test') 474 | file.binmode 475 | file << $test_net_http_data 476 | filename = File.basename(file.to_path) 477 | data = [['file', file]] 478 | expected = <<"__EOM__".gsub(/\n/, "\r\n") 479 | -- 480 | Content-Disposition: form-data; name="file"; filename="" 481 | Content-Type: application/octet-stream 482 | 483 | 484 | ---- 485 | __EOM__ 486 | expected.sub!(//, filename) 487 | expected.sub!(//, $test_net_http_data) 488 | start {|http| 489 | data.each{|k,v|v.rewind rescue nil} 490 | req = Net::HTTP::Post.new('/') 491 | req.set_form(data, 'multipart/form-data') 492 | res = http.request req 493 | body = res.body 494 | header, _ = body.split(/\r\n\r\n/, 2) 495 | assert_match(/\A--(\S+)/, body) 496 | /\A--(\S+)/ =~ body 497 | expected = expected.gsub(//, $1) 498 | assert_match(/^--(\S+)\r\n/, header) 499 | assert_match( 500 | /^Content-Disposition: form-data; name="file"; filename="#{filename}"\r\n/, 501 | header) 502 | assert_equal(expected, body) 503 | 504 | data.each{|k,v|v.rewind rescue nil} 505 | req['Transfer-Encoding'] = 'chunked' 506 | res = http.request req 507 | #assert_equal(expected, res.body) 508 | } 509 | end 510 | end 511 | 512 | -------------------------------------------------------------------------------- /test/openssl/envutil.rb: -------------------------------------------------------------------------------- 1 | require "open3" 2 | require "timeout" 3 | 4 | module EnvUtil 5 | def rubybin 6 | unless ENV["RUBYOPT"] 7 | 8 | end 9 | if ruby = ENV["RUBY"] 10 | return ruby 11 | end 12 | ruby = "ruby" 13 | rubyexe = ruby+".exe" 14 | 3.times do 15 | if File.exist? ruby and File.executable? ruby and !File.directory? ruby 16 | return File.expand_path(ruby) 17 | end 18 | if File.exist? rubyexe and File.executable? rubyexe 19 | return File.expand_path(rubyexe) 20 | end 21 | ruby = File.join("..", ruby) 22 | end 23 | if defined?(RbConfig.ruby) 24 | RbConfig.ruby 25 | else 26 | "ruby" 27 | end 28 | end 29 | module_function :rubybin 30 | 31 | LANG_ENVS = %w"LANG LC_ALL LC_CTYPE" 32 | 33 | def invoke_ruby(args, stdin_data="", capture_stdout=false, capture_stderr=false, opt={}) 34 | in_c, in_p = IO.pipe 35 | out_p, out_c = IO.pipe if capture_stdout 36 | err_p, err_c = IO.pipe if capture_stderr && capture_stderr != :merge_to_stdout 37 | opt = opt.dup 38 | opt[:in] = in_c 39 | opt[:out] = out_c if capture_stdout 40 | opt[:err] = capture_stderr == :merge_to_stdout ? out_c : err_c if capture_stderr 41 | if enc = opt.delete(:encoding) 42 | out_p.set_encoding(enc) if out_p 43 | err_p.set_encoding(enc) if err_p 44 | end 45 | c = "C" 46 | child_env = {} 47 | LANG_ENVS.each {|lc| child_env[lc] = c} 48 | if Array === args and Hash === args.first 49 | child_env.update(args.shift) 50 | end 51 | args = [args] if args.kind_of?(String) 52 | spawn_args = [child_env, EnvUtil.rubybin] + args + [opt] 53 | pid = spawn(*spawn_args) 54 | in_c.close 55 | out_c.close if capture_stdout 56 | err_c.close if capture_stderr && capture_stderr != :merge_to_stdout 57 | if block_given? 58 | return yield in_p, out_p, err_p 59 | else 60 | th_stdout = Thread.new { out_p.read } if capture_stdout 61 | th_stderr = Thread.new { err_p.read } if capture_stderr && capture_stderr != :merge_to_stdout 62 | in_p.write stdin_data.to_str 63 | in_p.close 64 | timeout = opt.fetch(:timeout, 10) 65 | if (!th_stdout || th_stdout.join(timeout)) && (!th_stderr || th_stderr.join(timeout)) 66 | stdout = th_stdout.value if capture_stdout 67 | stderr = th_stderr.value if capture_stderr && capture_stderr != :merge_to_stdout 68 | else 69 | raise Timeout::Error 70 | end 71 | out_p.close if capture_stdout 72 | err_p.close if capture_stderr && capture_stderr != :merge_to_stdout 73 | Process.wait pid 74 | status = $? 75 | return stdout, stderr, status 76 | end 77 | ensure 78 | [in_c, in_p, out_c, out_p, err_c, err_p].each do |io| 79 | io.close if io && !io.closed? 80 | end 81 | [th_stdout, th_stderr].each do |th| 82 | (th.kill; th.join) if th 83 | end 84 | end 85 | module_function :invoke_ruby 86 | 87 | alias rubyexec invoke_ruby 88 | class << self 89 | alias rubyexec invoke_ruby 90 | end 91 | 92 | def verbose_warning 93 | class << (stderr = "") 94 | alias write << 95 | end 96 | stderr, $stderr, verbose, $VERBOSE = $stderr, stderr, $VERBOSE, true 97 | yield stderr 98 | ensure 99 | stderr, $stderr, $VERBOSE = $stderr, stderr, verbose 100 | return stderr 101 | end 102 | module_function :verbose_warning 103 | 104 | def suppress_warning 105 | verbose, $VERBOSE = $VERBOSE, nil 106 | yield 107 | ensure 108 | $VERBOSE = verbose 109 | end 110 | module_function :suppress_warning 111 | 112 | def under_gc_stress 113 | stress, GC.stress = GC.stress, true 114 | yield 115 | ensure 116 | GC.stress = stress 117 | end 118 | module_function :under_gc_stress 119 | end 120 | 121 | module Test 122 | module Unit 123 | module Assertions 124 | public 125 | def assert_normal_exit(testsrc, message = '', opt = {}) 126 | out, _, status = EnvUtil.invoke_ruby(%W'-W0', testsrc, true, :merge_to_stdout, opt) 127 | pid = status.pid 128 | faildesc = proc do 129 | signo = status.termsig 130 | signame = Signal.list.invert[signo] 131 | sigdesc = "signal #{signo}" 132 | if signame 133 | sigdesc = "SIG#{signame} (#{sigdesc})" 134 | end 135 | if status.coredump? 136 | sigdesc << " (core dumped)" 137 | end 138 | full_message = '' 139 | if !message.empty? 140 | full_message << message << "\n" 141 | end 142 | full_message << "pid #{pid} killed by #{sigdesc}" 143 | if !out.empty? 144 | out << "\n" if /\n\z/ !~ out 145 | full_message << "\n#{out.gsub(/^/, '| ')}" 146 | end 147 | full_message 148 | end 149 | assert !status.signaled?, faildesc 150 | end 151 | 152 | def assert_in_out_err(args, test_stdin = "", test_stdout = [], test_stderr = [], message = nil, opt={}) 153 | stdout, stderr, status = EnvUtil.invoke_ruby(args, test_stdin, true, true, opt) 154 | if block_given? 155 | yield(stdout.lines.map {|l| l.chomp }, stderr.lines.map {|l| l.chomp }) 156 | else 157 | if test_stdout.is_a?(Regexp) 158 | assert_match(test_stdout, stdout, message) 159 | else 160 | assert_equal(test_stdout, stdout.lines.map {|l| l.chomp }, message) 161 | end 162 | if test_stderr.is_a?(Regexp) 163 | assert_match(test_stderr, stderr, message) 164 | else 165 | assert_equal(test_stderr, stderr.lines.map {|l| l.chomp }, message) 166 | end 167 | status 168 | end 169 | end 170 | 171 | def assert_ruby_status(args, test_stdin="", message=nil, opt={}) 172 | _, _, status = EnvUtil.invoke_ruby(args, test_stdin, false, false, opt) 173 | m = message ? "#{message} (#{status.inspect})" : "ruby exit status is not success: #{status.inspect}" 174 | assert(status.success?, m) 175 | end 176 | 177 | def assert_warn(msg) 178 | stderr = EnvUtil.verbose_warning { yield } 179 | assert(msg === stderr, "warning message #{stderr.inspect} is expected to match #{msg.inspect}") 180 | end 181 | 182 | end 183 | end 184 | end 185 | 186 | begin 187 | require 'rbconfig' 188 | rescue LoadError 189 | else 190 | module RbConfig 191 | @ruby = EnvUtil.rubybin 192 | class << self 193 | undef ruby if method_defined?(:ruby) 194 | attr_reader :ruby 195 | end 196 | dir = File.dirname(ruby) 197 | name = File.basename(ruby, CONFIG['EXEEXT']) 198 | CONFIG['bindir'] = dir 199 | CONFIG['ruby_install_name'] = name 200 | CONFIG['RUBY_INSTALL_NAME'] = name 201 | Gem::ConfigMap[:bindir] = dir if defined?(Gem) 202 | end 203 | end 204 | 205 | -------------------------------------------------------------------------------- /test/openssl/utils.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require "openssl" 3 | rescue LoadError 4 | end 5 | require "test/unit" 6 | require "digest/md5" 7 | require 'tempfile' 8 | require "rbconfig" 9 | require "socket" 10 | require "openssl/envutil" 11 | 12 | module OpenSSL::TestUtils 13 | TEST_KEY_RSA1024 = OpenSSL::PKey::RSA.new <<-_end_of_pem_ 14 | -----BEGIN RSA PRIVATE KEY----- 15 | MIICXgIBAAKBgQDLwsSw1ECnPtT+PkOgHhcGA71nwC2/nL85VBGnRqDxOqjVh7Cx 16 | aKPERYHsk4BPCkE3brtThPWc9kjHEQQ7uf9Y1rbCz0layNqHyywQEVLFmp1cpIt/ 17 | Q3geLv8ZD9pihowKJDyMDiN6ArYUmZczvW4976MU3+l54E6lF/JfFEU5hwIDAQAB 18 | AoGBAKSl/MQarye1yOysqX6P8fDFQt68VvtXkNmlSiKOGuzyho0M+UVSFcs6k1L0 19 | maDE25AMZUiGzuWHyaU55d7RXDgeskDMakD1v6ZejYtxJkSXbETOTLDwUWTn618T 20 | gnb17tU1jktUtU67xK/08i/XodlgnQhs6VoHTuCh3Hu77O6RAkEA7+gxqBuZR572 21 | 74/akiW/SuXm0SXPEviyO1MuSRwtI87B02D0qgV8D1UHRm4AhMnJ8MCs1809kMQE 22 | JiQUCrp9mQJBANlt2ngBO14us6NnhuAseFDTBzCHXwUUu1YKHpMMmxpnGqaldGgX 23 | sOZB3lgJsT9VlGf3YGYdkLTNVbogQKlKpB8CQQDiSwkb4vyQfDe8/NpU5Not0fII 24 | 8jsDUCb+opWUTMmfbxWRR3FBNu8wnym/m19N4fFj8LqYzHX4KY0oVPu6qvJxAkEA 25 | wa5snNekFcqONLIE4G5cosrIrb74sqL8GbGb+KuTAprzj5z1K8Bm0UW9lTjVDjDi 26 | qRYgZfZSL+x1P/54+xTFSwJAY1FxA/N3QPCXCjPh5YqFxAMQs2VVYTfg+t0MEcJD 27 | dPMQD5JX6g5HKnHFg2mZtoXQrWmJSn7p8GJK8yNTopEErA== 28 | -----END RSA PRIVATE KEY----- 29 | _end_of_pem_ 30 | 31 | TEST_KEY_RSA2048 = OpenSSL::PKey::RSA.new <<-_end_of_pem_ 32 | -----BEGIN RSA PRIVATE KEY----- 33 | MIIEpAIBAAKCAQEAuV9ht9J7k4NBs38jOXvvTKY9gW8nLICSno5EETR1cuF7i4pN 34 | s9I1QJGAFAX0BEO4KbzXmuOvfCpD3CU+Slp1enenfzq/t/e/1IRW0wkJUJUFQign 35 | 4CtrkJL+P07yx18UjyPlBXb81ApEmAB5mrJVSrWmqbjs07JbuS4QQGGXLc+Su96D 36 | kYKmSNVjBiLxVVSpyZfAY3hD37d60uG+X8xdW5v68JkRFIhdGlb6JL8fllf/A/bl 37 | NwdJOhVr9mESHhwGjwfSeTDPfd8ZLE027E5lyAVX9KZYcU00mOX+fdxOSnGqS/8J 38 | DRh0EPHDL15RcJjV2J6vZjPb0rOYGDoMcH+94wIDAQABAoIBAAzsamqfYQAqwXTb 39 | I0CJtGg6msUgU7HVkOM+9d3hM2L791oGHV6xBAdpXW2H8LgvZHJ8eOeSghR8+dgq 40 | PIqAffo4x1Oma+FOg3A0fb0evyiACyrOk+EcBdbBeLo/LcvahBtqnDfiUMQTpy6V 41 | seSoFCwuN91TSCeGIsDpRjbG1vxZgtx+uI+oH5+ytqJOmfCksRDCkMglGkzyfcl0 42 | Xc5CUhIJ0my53xijEUQl19rtWdMnNnnkdbG8PT3LZlOta5Do86BElzUYka0C6dUc 43 | VsBDQ0Nup0P6rEQgy7tephHoRlUGTYamsajGJaAo1F3IQVIrRSuagi7+YpSpCqsW 44 | wORqorkCgYEA7RdX6MDVrbw7LePnhyuaqTiMK+055/R1TqhB1JvvxJ1CXk2rDL6G 45 | 0TLHQ7oGofd5LYiemg4ZVtWdJe43BPZlVgT6lvL/iGo8JnrncB9Da6L7nrq/+Rvj 46 | XGjf1qODCK+LmreZWEsaLPURIoR/Ewwxb9J2zd0CaMjeTwafJo1CZvcCgYEAyCgb 47 | aqoWvUecX8VvARfuA593Lsi50t4MEArnOXXcd1RnXoZWhbx5rgO8/ATKfXr0BK/n 48 | h2GF9PfKzHFm/4V6e82OL7gu/kLy2u9bXN74vOvWFL5NOrOKPM7Kg+9I131kNYOw 49 | Ivnr/VtHE5s0dY7JChYWE1F3vArrOw3T00a4CXUCgYEA0SqY+dS2LvIzW4cHCe9k 50 | IQqsT0yYm5TFsUEr4sA3xcPfe4cV8sZb9k/QEGYb1+SWWZ+AHPV3UW5fl8kTbSNb 51 | v4ng8i8rVVQ0ANbJO9e5CUrepein2MPL0AkOATR8M7t7dGGpvYV0cFk8ZrFx0oId 52 | U0PgYDotF/iueBWlbsOM430CgYEAqYI95dFyPI5/AiSkY5queeb8+mQH62sdcCCr 53 | vd/w/CZA/K5sbAo4SoTj8dLk4evU6HtIa0DOP63y071eaxvRpTNqLUOgmLh+D6gS 54 | Cc7TfLuFrD+WDBatBd5jZ+SoHccVrLR/4L8jeodo5FPW05A+9gnKXEXsTxY4LOUC 55 | 9bS4e1kCgYAqVXZh63JsMwoaxCYmQ66eJojKa47VNrOeIZDZvd2BPVf30glBOT41 56 | gBoDG3WMPZoQj9pb7uMcrnvs4APj2FIhMU8U15LcPAj59cD6S6rWnAxO8NFK7HQG 57 | 4Jxg3JNNf8ErQoCHb1B3oVdXJkmbJkARoDpBKmTCgKtP8ADYLmVPQw== 58 | -----END RSA PRIVATE KEY----- 59 | _end_of_pem_ 60 | 61 | TEST_KEY_DSA256 = OpenSSL::PKey::DSA.new <<-_end_of_pem_ 62 | -----BEGIN DSA PRIVATE KEY----- 63 | MIH3AgEAAkEAhk2libbY2a8y2Pt21+YPYGZeW6wzaW2yfj5oiClXro9XMR7XWLkE 64 | 9B7XxLNFCS2gmCCdMsMW1HulaHtLFQmB2wIVAM43JZrcgpu6ajZ01VkLc93gu/Ed 65 | AkAOhujZrrKV5CzBKutKLb0GVyVWmdC7InoNSMZEeGU72rT96IjM59YzoqmD0pGM 66 | 3I1o4cGqg1D1DfM1rQlnN1eSAkBq6xXfEDwJ1mLNxF6q8Zm/ugFYWR5xcX/3wFiT 67 | b4+EjHP/DbNh9Vm5wcfnDBJ1zKvrMEf2xqngYdrV/3CiGJeKAhRvL57QvJZcQGvn 68 | ISNX5cMzFHRW3Q== 69 | -----END DSA PRIVATE KEY----- 70 | _end_of_pem_ 71 | 72 | TEST_KEY_DSA512 = OpenSSL::PKey::DSA.new <<-_end_of_pem_ 73 | -----BEGIN DSA PRIVATE KEY----- 74 | MIH4AgEAAkEA5lB4GvEwjrsMlGDqGsxrbqeFRh6o9OWt6FgTYiEEHaOYhkIxv0Ok 75 | RZPDNwOG997mDjBnvDJ1i56OmS3MbTnovwIVAJgub/aDrSDB4DZGH7UyarcaGy6D 76 | AkB9HdFw/3td8K4l1FZHv7TCZeJ3ZLb7dF3TWoGUP003RCqoji3/lHdKoVdTQNuR 77 | S/m6DlCwhjRjiQ/lBRgCLCcaAkEAjN891JBjzpMj4bWgsACmMggFf57DS0Ti+5++ 78 | Q1VB8qkJN7rA7/2HrCR3gTsWNb1YhAsnFsoeRscC+LxXoXi9OAIUBG98h4tilg6S 79 | 55jreJD3Se3slps= 80 | -----END DSA PRIVATE KEY----- 81 | _end_of_pem_ 82 | 83 | module_function 84 | 85 | def issue_cert(dn, key, serial, not_before, not_after, extensions, 86 | issuer, issuer_key, digest) 87 | cert = OpenSSL::X509::Certificate.new 88 | issuer = cert unless issuer 89 | issuer_key = key unless issuer_key 90 | cert.version = 2 91 | cert.serial = serial 92 | cert.subject = dn 93 | cert.issuer = issuer.subject 94 | cert.public_key = key.public_key 95 | cert.not_before = not_before 96 | cert.not_after = not_after 97 | ef = OpenSSL::X509::ExtensionFactory.new 98 | ef.subject_certificate = cert 99 | ef.issuer_certificate = issuer 100 | extensions.each{|oid, value, critical| 101 | cert.add_extension(ef.create_extension(oid, value, critical)) 102 | } 103 | cert.sign(issuer_key, digest) 104 | cert 105 | end 106 | 107 | def issue_crl(revoke_info, serial, lastup, nextup, extensions, 108 | issuer, issuer_key, digest) 109 | crl = OpenSSL::X509::CRL.new 110 | crl.issuer = issuer.subject 111 | crl.version = 1 112 | crl.last_update = lastup 113 | crl.next_update = nextup 114 | revoke_info.each{|rserial, time, reason_code| 115 | revoked = OpenSSL::X509::Revoked.new 116 | revoked.serial = rserial 117 | revoked.time = time 118 | enum = OpenSSL::ASN1::Enumerated(reason_code) 119 | ext = OpenSSL::X509::Extension.new("CRLReason", enum) 120 | revoked.add_extension(ext) 121 | crl.add_revoked(revoked) 122 | } 123 | ef = OpenSSL::X509::ExtensionFactory.new 124 | ef.issuer_certificate = issuer 125 | ef.crl = crl 126 | crlnum = OpenSSL::ASN1::Integer(serial) 127 | crl.add_extension(OpenSSL::X509::Extension.new("crlNumber", crlnum)) 128 | extensions.each{|oid, value, critical| 129 | crl.add_extension(ef.create_extension(oid, value, critical)) 130 | } 131 | crl.sign(issuer_key, digest) 132 | crl 133 | end 134 | 135 | def get_subject_key_id(cert) 136 | asn1_cert = OpenSSL::ASN1.decode(cert) 137 | tbscert = asn1_cert.value[0] 138 | pkinfo = tbscert.value[6] 139 | publickey = pkinfo.value[1] 140 | pkvalue = publickey.value 141 | OpenSSL::Digest::SHA1.hexdigest(pkvalue).scan(/../).join(":").upcase 142 | end 143 | 144 | def silent 145 | begin 146 | back, $VERBOSE = $VERBOSE, nil 147 | yield 148 | ensure 149 | $VERBOSE = back 150 | end 151 | end 152 | end if defined?(OpenSSL) 153 | 154 | -------------------------------------------------------------------------------- /test/test_buffered_io.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require 'stringio' 3 | 4 | require 'utils' 5 | 6 | module Net 7 | class TestBufferedIO < Test::Unit::TestCase 8 | def test_eof? 9 | s = StringIO.new 10 | assert s.eof? 11 | bio = BufferedIO.new(s) 12 | assert_equal s, bio.io 13 | assert_equal s.eof?, bio.eof? 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/test_http.rb: -------------------------------------------------------------------------------- 1 | # $Id$ 2 | 3 | require 'test/unit' 4 | require 'stringio' 5 | require 'utils' 6 | require 'http_test_base' 7 | 8 | class TestNetHTTP_v1_2 < Test::Unit::TestCase 9 | CONFIG = { 10 | 'host' => '127.0.0.1', 11 | 'port' => 10081, 12 | 'proxy_host' => nil, 13 | 'proxy_port' => nil, 14 | } 15 | 16 | include TestNetHTTPUtils 17 | include TestNetHTTP_version_1_1_methods 18 | include TestNetHTTP_version_1_2_methods 19 | 20 | def new 21 | Net2::HTTP.version_1_2 22 | super 23 | end 24 | end 25 | 26 | -------------------------------------------------------------------------------- /test/test_http_chunked.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require 'stringio' 3 | require 'utils' 4 | require 'http_test_base' 5 | 6 | class TestNetHTTP_v1_2_chunked < Test::Unit::TestCase 7 | CONFIG = { 8 | 'host' => '127.0.0.1', 9 | 'port' => 10081, 10 | 'proxy_host' => nil, 11 | 'proxy_port' => nil, 12 | 'chunked' => true, 13 | } 14 | 15 | include TestNetHTTPUtils 16 | include TestNetHTTP_version_1_1_methods 17 | include TestNetHTTP_version_1_2_methods 18 | 19 | def new 20 | Net::HTTP.version_1_2 21 | super 22 | end 23 | 24 | def test_chunked_break 25 | i = 0 26 | assert_nothing_raised("[ruby-core:29229]") { 27 | start {|http| 28 | http.request_get('/') {|res| 29 | res.read_body {|chunk| 30 | break 31 | } 32 | } 33 | } 34 | } 35 | end 36 | end 37 | 38 | 39 | -------------------------------------------------------------------------------- /test/test_http_gzip.rb: -------------------------------------------------------------------------------- 1 | require "utils" 2 | require "http_test_base" 3 | 4 | class TestNetHTTP_v1_2_gzip < Test::Unit::TestCase 5 | CONFIG = { 6 | 'host' => '127.0.0.1', 7 | 'port' => 10081, 8 | 'proxy_host' => nil, 9 | 'proxy_port' => nil, 10 | 'gzip' => true 11 | } 12 | 13 | include TestNetHTTPUtils 14 | include TestNetHTTP_version_1_1_methods 15 | include TestNetHTTP_version_1_2_methods 16 | 17 | def new 18 | Net::HTTP.version_1_2 19 | super 20 | end 21 | end 22 | 23 | class TestNetHTTP_v1_2_gzip_chunked < Test::Unit::TestCase 24 | CONFIG = { 25 | 'host' => '127.0.0.1', 26 | 'port' => 10081, 27 | 'proxy_host' => nil, 28 | 'proxy_port' => nil, 29 | 'gzip' => true, 30 | 'chunked' => true 31 | } 32 | 33 | include TestNetHTTPUtils 34 | include TestNetHTTP_version_1_1_methods 35 | include TestNetHTTP_version_1_2_methods 36 | 37 | def new 38 | Net::HTTP.version_1_2 39 | super 40 | end 41 | end 42 | 43 | 44 | -------------------------------------------------------------------------------- /test/test_httpheader.rb: -------------------------------------------------------------------------------- 1 | require 'utils' 2 | require 'test/unit' 3 | 4 | class HTTPHeaderTest < Test::Unit::TestCase 5 | 6 | class C 7 | include Net::HTTPHeader 8 | def initialize 9 | initialize_http_header({}) 10 | end 11 | attr_accessor :body 12 | end 13 | 14 | def setup 15 | @c = C.new 16 | end 17 | 18 | def test_size 19 | assert_equal 0, @c.size 20 | @c['a'] = 'a' 21 | assert_equal 1, @c.size 22 | @c['b'] = 'b' 23 | assert_equal 2, @c.size 24 | @c['b'] = 'b' 25 | assert_equal 2, @c.size 26 | @c['c'] = 'c' 27 | assert_equal 3, @c.size 28 | end 29 | 30 | def test_ASET 31 | @c['My-Header'] = 'test string' 32 | @c['my-Header'] = 'test string' 33 | @c['My-header'] = 'test string' 34 | @c['my-header'] = 'test string' 35 | @c['MY-HEADER'] = 'test string' 36 | assert_equal 1, @c.size 37 | 38 | @c['AaA'] = 'aaa' 39 | @c['aaA'] = 'aaa' 40 | @c['AAa'] = 'aaa' 41 | assert_equal 2, @c.length 42 | end 43 | 44 | def test_AREF 45 | @c['My-Header'] = 'test string' 46 | assert_equal 'test string', @c['my-header'] 47 | assert_equal 'test string', @c['MY-header'] 48 | assert_equal 'test string', @c['my-HEADER'] 49 | 50 | @c['Next-Header'] = 'next string' 51 | assert_equal 'next string', @c['next-header'] 52 | end 53 | 54 | def test_add_field 55 | @c.add_field 'My-Header', 'a' 56 | assert_equal 'a', @c['My-Header'] 57 | assert_equal ['a'], @c.get_fields('My-Header') 58 | @c.add_field 'My-Header', 'b' 59 | assert_equal 'a, b', @c['My-Header'] 60 | assert_equal ['a', 'b'], @c.get_fields('My-Header') 61 | @c.add_field 'My-Header', 'c' 62 | assert_equal 'a, b, c', @c['My-Header'] 63 | assert_equal ['a', 'b', 'c'], @c.get_fields('My-Header') 64 | @c.add_field 'My-Header', 'd, d' 65 | assert_equal 'a, b, c, d, d', @c['My-Header'] 66 | assert_equal ['a', 'b', 'c', 'd, d'], @c.get_fields('My-Header') 67 | end 68 | 69 | def test_get_fields 70 | @c['My-Header'] = 'test string' 71 | assert_equal ['test string'], @c.get_fields('my-header') 72 | assert_equal ['test string'], @c.get_fields('My-header') 73 | assert_equal ['test string'], @c.get_fields('my-Header') 74 | 75 | assert_nil @c.get_fields('not-found') 76 | assert_nil @c.get_fields('Not-Found') 77 | 78 | @c.get_fields('my-header').push 'junk' 79 | assert_equal ['test string'], @c.get_fields('my-header') 80 | @c.get_fields('my-header').clear 81 | assert_equal ['test string'], @c.get_fields('my-header') 82 | end 83 | 84 | def test_delete 85 | @c['My-Header'] = 'test' 86 | assert_equal 'test', @c['My-Header'] 87 | assert_nil @c['not-found'] 88 | @c.delete 'My-Header' 89 | assert_nil @c['My-Header'] 90 | assert_nil @c['not-found'] 91 | @c.delete 'My-Header' 92 | @c.delete 'My-Header' 93 | assert_nil @c['My-Header'] 94 | assert_nil @c['not-found'] 95 | end 96 | 97 | def test_each 98 | @c['My-Header'] = 'test' 99 | @c.each do |k, v| 100 | assert_equal 'my-header', k 101 | assert_equal 'test', v 102 | end 103 | @c.each do |k, v| 104 | assert_equal 'my-header', k 105 | assert_equal 'test', v 106 | end 107 | end 108 | 109 | def test_each_key 110 | @c['My-Header'] = 'test' 111 | @c.each_key do |k| 112 | assert_equal 'my-header', k 113 | end 114 | @c.each_key do |k| 115 | assert_equal 'my-header', k 116 | end 117 | end 118 | 119 | def test_each_value 120 | @c['My-Header'] = 'test' 121 | @c.each_value do |v| 122 | assert_equal 'test', v 123 | end 124 | @c.each_value do |v| 125 | assert_equal 'test', v 126 | end 127 | end 128 | 129 | def test_canonical_each 130 | @c['my-header'] = ['a', 'b'] 131 | @c.canonical_each do |k,v| 132 | assert_equal 'My-Header', k 133 | assert_equal 'a, b', v 134 | end 135 | end 136 | 137 | def test_each_capitalized 138 | @c['my-header'] = ['a', 'b'] 139 | @c.each_capitalized do |k,v| 140 | assert_equal 'My-Header', k 141 | assert_equal 'a, b', v 142 | end 143 | end 144 | 145 | def test_key? 146 | @c['My-Header'] = 'test' 147 | assert_equal true, @c.key?('My-Header') 148 | assert_equal true, @c.key?('my-header') 149 | assert_equal false, @c.key?('Not-Found') 150 | assert_equal false, @c.key?('not-found') 151 | assert_equal false, @c.key?('') 152 | assert_equal false, @c.key?('x' * 1024) 153 | end 154 | 155 | def test_to_hash 156 | end 157 | 158 | def test_range 159 | try_range(1..5, '1-5') 160 | try_range(234..567, '234-567') 161 | try_range(-5..-1, '-5') 162 | try_range(1..-1, '1-') 163 | end 164 | 165 | def try_range(r, s) 166 | @c['range'] = "bytes=#{s}" 167 | assert_equal r, Array(@c.range)[0] 168 | end 169 | 170 | def test_range= 171 | @c.range = 0..499 172 | assert_equal 'bytes=0-499', @c['range'] 173 | @c.range = 0...500 174 | assert_equal 'bytes=0-499', @c['range'] 175 | @c.range = 300 176 | assert_equal 'bytes=0-299', @c['range'] 177 | @c.range = -400 178 | assert_equal 'bytes=-400', @c['range'] 179 | @c.set_range 0, 500 180 | assert_equal 'bytes=0-499', @c['range'] 181 | end 182 | 183 | def test_content_range 184 | end 185 | 186 | def test_range_length 187 | @c['Content-Range'] = "bytes 0-499/1000" 188 | assert_equal 500, @c.range_length 189 | @c['Content-Range'] = "bytes 1-500/1000" 190 | assert_equal 500, @c.range_length 191 | @c['Content-Range'] = "bytes 1-1/1000" 192 | assert_equal 1, @c.range_length 193 | end 194 | 195 | def test_chunked? 196 | try_chunked true, 'chunked' 197 | try_chunked true, ' chunked ' 198 | try_chunked true, '(OK)chunked' 199 | 200 | try_chunked false, 'not-chunked' 201 | try_chunked false, 'chunked-but-not-chunked' 202 | end 203 | 204 | def try_chunked(bool, str) 205 | @c['transfer-encoding'] = str 206 | assert_equal bool, @c.chunked? 207 | end 208 | 209 | def test_content_length 210 | @c.delete('content-length') 211 | assert_nil @c['content-length'] 212 | 213 | try_content_length 500, '500' 214 | try_content_length 10000_0000_0000, '1000000000000' 215 | try_content_length 123, ' 123' 216 | try_content_length 1, '1 23' 217 | try_content_length 500, '(OK)500' 218 | assert_raise(Net::HTTPHeaderSyntaxError, 'here is no digit, but') { 219 | @c['content-length'] = 'no digit' 220 | @c.content_length 221 | } 222 | end 223 | 224 | def try_content_length(len, str) 225 | @c['content-length'] = str 226 | assert_equal len, @c.content_length 227 | end 228 | 229 | def test_content_length= 230 | @c.content_length = 0 231 | assert_equal 0, @c.content_length 232 | @c.content_length = 1 233 | assert_equal 1, @c.content_length 234 | @c.content_length = 999 235 | assert_equal 999, @c.content_length 236 | @c.content_length = 10000000000000 237 | assert_equal 10000000000000, @c.content_length 238 | end 239 | 240 | def test_content_type 241 | assert_nil @c.content_type 242 | @c.content_type = 'text/html' 243 | assert_equal 'text/html', @c.content_type 244 | @c.content_type = 'application/pdf' 245 | assert_equal 'application/pdf', @c.content_type 246 | @c.set_content_type 'text/html', {'charset' => 'iso-2022-jp'} 247 | assert_equal 'text/html', @c.content_type 248 | @c.content_type = 'text' 249 | assert_equal 'text', @c.content_type 250 | end 251 | 252 | def test_main_type 253 | assert_nil @c.main_type 254 | @c.content_type = 'text/html' 255 | assert_equal 'text', @c.main_type 256 | @c.content_type = 'application/pdf' 257 | assert_equal 'application', @c.main_type 258 | @c.set_content_type 'text/html', {'charset' => 'iso-2022-jp'} 259 | assert_equal 'text', @c.main_type 260 | @c.content_type = 'text' 261 | assert_equal 'text', @c.main_type 262 | end 263 | 264 | def test_sub_type 265 | assert_nil @c.sub_type 266 | @c.content_type = 'text/html' 267 | assert_equal 'html', @c.sub_type 268 | @c.content_type = 'application/pdf' 269 | assert_equal 'pdf', @c.sub_type 270 | @c.set_content_type 'text/html', {'charset' => 'iso-2022-jp'} 271 | assert_equal 'html', @c.sub_type 272 | @c.content_type = 'text' 273 | assert_nil @c.sub_type 274 | end 275 | 276 | def test_type_params 277 | assert_equal({}, @c.type_params) 278 | @c.content_type = 'text/html' 279 | assert_equal({}, @c.type_params) 280 | @c.content_type = 'application/pdf' 281 | assert_equal({}, @c.type_params) 282 | @c.set_content_type 'text/html', {'charset' => 'iso-2022-jp'} 283 | assert_equal({'charset' => 'iso-2022-jp'}, @c.type_params) 284 | @c.content_type = 'text' 285 | assert_equal({}, @c.type_params) 286 | end 287 | 288 | def test_set_content_type 289 | end 290 | 291 | def test_form_data= 292 | @c.form_data = {"cmd"=>"search", "q"=>"ruby", "max"=>"50"} 293 | assert_equal 'application/x-www-form-urlencoded', @c.content_type 294 | assert_equal %w( cmd=search max=50 q=ruby ), @c.body.split('&').sort 295 | end 296 | 297 | def test_set_form_data 298 | @c.set_form_data "cmd"=>"search", "q"=>"ruby", "max"=>"50" 299 | assert_equal 'application/x-www-form-urlencoded', @c.content_type 300 | assert_equal %w( cmd=search max=50 q=ruby ), @c.body.split('&').sort 301 | 302 | @c.set_form_data "cmd"=>"search", "q"=>"ruby", "max"=>50 303 | assert_equal 'application/x-www-form-urlencoded', @c.content_type 304 | assert_equal %w( cmd=search max=50 q=ruby ), @c.body.split('&').sort 305 | 306 | @c.set_form_data({"cmd"=>"search", "q"=>"ruby", "max"=>"50"}, ';') 307 | assert_equal 'application/x-www-form-urlencoded', @c.content_type 308 | assert_equal %w( cmd=search max=50 q=ruby ), @c.body.split(';').sort 309 | end 310 | 311 | def test_basic_auth 312 | end 313 | 314 | def test_proxy_basic_auth 315 | end 316 | 317 | end 318 | -------------------------------------------------------------------------------- /test/test_httpresponse.rb: -------------------------------------------------------------------------------- 1 | require 'utils' 2 | require 'test/unit' 3 | require 'stringio' 4 | 5 | class HTTPResponseTest < Test::Unit::TestCase 6 | def test_singleline_header 7 | io = dummy_io(< '127.0.0.1', 27 | 'port' => 10081, 28 | 'proxy_host' => nil, 29 | 'proxy_port' => nil, 30 | 'ssl_enable' => true, 31 | 'ssl_certificate' => cert, 32 | 'ssl_private_key' => key, 33 | } 34 | 35 | def test_get 36 | http = Net::HTTP.new("localhost", config("port")) 37 | http.use_ssl = true 38 | http.verify_callback = Proc.new do |preverify_ok, store_ctx| 39 | store_ctx.current_cert.to_der == config('ssl_certificate').to_der 40 | end 41 | http.request_get("/") {|res| 42 | assert_equal($test_net_http_data, res.body) 43 | } 44 | rescue SystemCallError 45 | skip $! 46 | end 47 | 48 | def test_post 49 | http = Net::HTTP.new("localhost", config("port")) 50 | http.use_ssl = true 51 | http.verify_callback = Proc.new do |preverify_ok, store_ctx| 52 | store_ctx.current_cert.to_der == config('ssl_certificate').to_der 53 | end 54 | data = config('ssl_private_key').to_der 55 | http.request_post("/", data) {|res| 56 | assert_equal(data, res.body) 57 | } 58 | rescue SystemCallError 59 | skip $! 60 | end 61 | 62 | if ENV["RUBY_OPENSSL_TEST_ALL"] 63 | def test_verify 64 | http = Net::HTTP.new("ssl.netlab.jp", 443) 65 | http.use_ssl = true 66 | assert( 67 | (http.request_head("/"){|res| } rescue false), 68 | "The system may not have default CA certificate store." 69 | ) 70 | end 71 | end 72 | 73 | def test_verify_none 74 | http = Net::HTTP.new("localhost", config("port")) 75 | http.use_ssl = true 76 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE 77 | http.request_get("/") {|res| 78 | assert_equal($test_net_http_data, res.body) 79 | } 80 | rescue SystemCallError 81 | skip $! 82 | end 83 | 84 | def test_certificate_verify_failure 85 | http = Net::HTTP.new("localhost", config("port")) 86 | http.use_ssl = true 87 | ex = assert_raise(OpenSSL::SSL::SSLError){ 88 | begin 89 | http.request_get("/") {|res| } 90 | rescue SystemCallError 91 | skip $! 92 | end 93 | } 94 | assert_match(/certificate verify failed/, ex.message) 95 | end 96 | 97 | def test_identity_verify_failure 98 | http = Net::HTTP.new("127.0.0.1", config("port")) 99 | http.use_ssl = true 100 | http.verify_callback = Proc.new do |preverify_ok, store_ctx| 101 | store_ctx.current_cert.to_der == config('ssl_certificate').to_der 102 | end 103 | ex = assert_raise(OpenSSL::SSL::SSLError){ 104 | http.request_get("/") {|res| } 105 | } 106 | assert_match(/hostname was not match|certificate verify failed/, ex.message) 107 | end 108 | 109 | def test_timeout_during_SSL_handshake 110 | bug4246 = "expected the SSL connection to have timed out but have not. [ruby-core:34203]" 111 | 112 | # listen for connections... but deliberately do not complete SSL handshake 113 | TCPServer.open(0) {|server| 114 | port = server.addr[1] 115 | 116 | conn = Net::HTTP.new('localhost', port) 117 | conn.use_ssl = true 118 | conn.read_timeout = 1 119 | conn.open_timeout = 1 120 | 121 | th = Thread.new do 122 | assert_raise(Timeout::Error) { 123 | conn.get('/') 124 | } 125 | end 126 | assert th.join(10), bug4246 127 | } 128 | end 129 | end if defined?(OpenSSL) 130 | -------------------------------------------------------------------------------- /test/test_https_proxy.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require 'net2/https' 3 | rescue LoadError 4 | end 5 | require 'test/unit' 6 | 7 | class HTTPSProxyTest < Test::Unit::TestCase 8 | def test_https_proxy_authentication 9 | t = nil 10 | TCPServer.open("127.0.0.1", 0) {|serv| 11 | _, port, _, _ = serv.addr 12 | t = Thread.new { 13 | proxy = Net::HTTP.Proxy("127.0.0.1", port, 'user', 'password') 14 | http = proxy.new("foo.example.org", 8000) 15 | http.use_ssl = true 16 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE 17 | begin 18 | http.start 19 | rescue EOFError 20 | end 21 | } 22 | sock = serv.accept 23 | proxy_request = sock.gets("\r\n\r\n") 24 | assert_equal( 25 | "CONNECT foo.example.org:8000 HTTP/1.1\r\n" + 26 | "Host: foo.example.org:8000\r\n" + 27 | "Proxy-Authorization: Basic dXNlcjpwYXNzd29yZA==\r\n" + 28 | "\r\n", 29 | proxy_request, 30 | "[ruby-dev:25673]") 31 | sock.close 32 | } 33 | ensure 34 | t.join if t 35 | end 36 | end if defined?(OpenSSL) 37 | 38 | -------------------------------------------------------------------------------- /test/test_reader.rb: -------------------------------------------------------------------------------- 1 | require "test/unit" 2 | require "utils" 3 | require "net2/http/readers" 4 | require "zlib" 5 | require "stringio" 6 | 7 | module Net2 8 | class TestBodyReader < Test::Unit::TestCase 9 | def setup 10 | @body = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." 11 | 12 | @read, @write = IO.pipe 13 | @buf = "" 14 | @reader = Net2::HTTP::BodyReader.new(@read, @buf, @body.bytesize) 15 | end 16 | 17 | def teardown 18 | @read.close 19 | @write.close 20 | end 21 | 22 | def test_simple_read 23 | @write.write @body 24 | @reader.read_to_endpoint 25 | assert_equal @body, @buf 26 | end 27 | 28 | def test_read_chunks 29 | @write.write @body 30 | @reader.read_to_endpoint 50 31 | assert_equal @body.slice(0,50), @buf 32 | end 33 | 34 | def test_read_over 35 | @write.write @body 36 | @reader.read_to_endpoint 50 37 | assert_equal @body.slice(0,50), @buf 38 | 39 | @reader.read_to_endpoint @body.size 40 | assert_equal @body, @buf 41 | 42 | assert_raises EOFError do 43 | @reader.read_to_endpoint 10 44 | end 45 | end 46 | 47 | def test_blocking 48 | @write.write @body.slice(0,50) 49 | @reader.read_to_endpoint 100 50 | assert_equal @body.slice(0,50), @buf 51 | 52 | @reader.read_to_endpoint 100 53 | assert_equal @body.slice(0,50), @buf 54 | 55 | @write.write @body.slice(50..-1) 56 | @reader.read_to_endpoint 57 | assert_equal @body, @buf 58 | 59 | assert_raises EOFError do 60 | @reader.read_to_endpoint 10 61 | end 62 | end 63 | 64 | class TestBuffer 65 | def initialize(queue) 66 | @queue = queue 67 | @string = "" 68 | end 69 | 70 | def <<(str) 71 | @string << str 72 | @queue.push :continue 73 | end 74 | 75 | def to_str 76 | @string 77 | end 78 | end 79 | 80 | def test_read_entire_body 81 | read_queue = Queue.new 82 | write_queue = Queue.new 83 | 84 | Thread.new do 85 | @write.write @body.slice(0,50) 86 | 87 | read_queue.push :continue 88 | write_queue.pop 89 | 90 | @write.write @body[50..-2] 91 | 92 | write_queue.pop 93 | 94 | @write.write @body[-1..-1] 95 | end 96 | 97 | read_queue.pop 98 | 99 | buffer = TestBuffer.new(write_queue) 100 | @reader = Net2::HTTP::BodyReader.new(@read, buffer, @body.bytesize) 101 | out = @reader.read 102 | 103 | assert_equal @body, out.to_str 104 | end 105 | 106 | def test_read_nonblock 107 | buf = "" 108 | @reader = Net2::HTTP::BodyReader.new(@read, buf, @body.bytesize) 109 | 110 | @write.write @body.slice(0,50) 111 | 112 | @reader.read_nonblock(20) 113 | @reader.read_nonblock(35) 114 | 115 | assert_raises Errno::EWOULDBLOCK do 116 | @reader.read_nonblock(10) 117 | end 118 | 119 | @write.write @body[50..-2] 120 | 121 | @reader.read_nonblock(1000) 122 | 123 | assert_raises Errno::EWOULDBLOCK do 124 | @reader.read_nonblock(10) 125 | end 126 | 127 | @write.write @body[-1..-1] 128 | 129 | @reader.read_nonblock(100) 130 | 131 | assert_raises EOFError do 132 | @reader.read_nonblock(10) 133 | end 134 | 135 | assert_raises EOFError do 136 | @reader.read_nonblock(10) 137 | end 138 | 139 | assert_equal @body, buf 140 | end 141 | 142 | def test_read_nonblock_gzip 143 | inflated_body = @body 144 | 145 | io = StringIO.new 146 | 147 | gzip = Zlib::GzipWriter.new(io) 148 | gzip.write @body 149 | gzip.close 150 | 151 | @body = io.string 152 | 153 | buf = "" 154 | endpoint = Net2::HTTP::Response::StringAdapter.new(buf) 155 | endpoint = Net2::HTTP::Response::DecompressionMiddleware.new(endpoint, { "Content-Encoding" => "gzip"}) 156 | @reader = Net2::HTTP::BodyReader.new(@read, endpoint, @body.bytesize) 157 | 158 | @write.write @body.slice(0,50) 159 | 160 | @reader.read_nonblock(20) 161 | @reader.read_nonblock(35) 162 | 163 | assert_raises Errno::EWOULDBLOCK do 164 | @reader.read_nonblock(10) 165 | end 166 | 167 | @write.write @body[50..-2] 168 | 169 | @reader.read_nonblock(1000) 170 | 171 | assert_raises Errno::EWOULDBLOCK do 172 | @reader.read_nonblock(10) 173 | end 174 | 175 | @write.write @body[-1..-1] 176 | 177 | @reader.read_nonblock(100) 178 | 179 | assert_raises EOFError do 180 | @reader.read_nonblock(10) 181 | end 182 | 183 | assert_raises EOFError do 184 | @reader.read_nonblock(10) 185 | end 186 | 187 | assert_equal inflated_body, buf 188 | end 189 | end 190 | 191 | class TestChunkedBodyReader < Test::Unit::TestCase 192 | def setup 193 | @body = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." 194 | 195 | @read, @write = IO.pipe 196 | @buf = "" 197 | @reader = Net2::HTTP::ChunkedBodyReader.new(@read, @buf) 198 | end 199 | 200 | def teardown 201 | @read.close 202 | @write.close 203 | end 204 | 205 | def test_simple_read 206 | @write.write "#{@body.size.to_s(16)}\r\n#{@body}\r\n0\r\n" 207 | @reader.read_to_endpoint 208 | assert_equal @body, @buf 209 | end 210 | 211 | def test_read_chunks 212 | @write.write "#{@body.size.to_s(16)}\r\n#{@body}\r\n0\r\n" 213 | @reader.read_to_endpoint 50 214 | assert_equal @body.slice(0,50), @buf 215 | end 216 | 217 | def test_read_over 218 | @write.write "#{@body.size.to_s(16)}\r\n#{@body}\r\n0\r\n\r\n" 219 | @reader.read_to_endpoint 50 220 | assert_equal @body.slice(0,50), @buf 221 | 222 | @reader.read_to_endpoint @body.size 223 | assert_equal @body, @buf 224 | 225 | assert_raises EOFError do 226 | @reader.read_to_endpoint 10 227 | end 228 | end 229 | 230 | def test_blocking 231 | size = @body.size.to_s(16) 232 | body = "#{size}\r\n#{@body}\r\n0\r\n\r\n" 233 | 234 | @write.write body.slice(0,50 + size.size + 2) 235 | @reader.read_to_endpoint 100 236 | assert_equal @body.slice(0,50), @buf 237 | 238 | @reader.read_to_endpoint 100 239 | assert_equal @body.slice(0,50), @buf 240 | 241 | @write.write body.slice((50 + size.size + 2)..-1) 242 | @reader.read_to_endpoint 243 | assert_equal @body, @buf 244 | 245 | assert_raises EOFError do 246 | @reader.read_to_endpoint 10 247 | end 248 | end 249 | 250 | def test_multi_chunks 251 | @write.write 50.to_s(16) 252 | @write.write "\r\n" 253 | @write.write @body.slice(0,50) 254 | 255 | @reader.read_to_endpoint 100 256 | assert_equal @body.slice(0,50), @buf 257 | 258 | @write.write "\r\n" 259 | rest = @body[50..-1] 260 | @write.write rest.size.to_s(16) 261 | @write.write "\r\n" 262 | @write.write rest 263 | 264 | @reader.read_to_endpoint 265 | assert_equal @body, @buf 266 | 267 | @write.write "\r\n0\r\n\r\n" 268 | @reader.read_to_endpoint 269 | assert_equal @body, @buf 270 | 271 | assert_raises EOFError do 272 | @reader.read_to_endpoint 273 | end 274 | end 275 | 276 | def test_read_nonblock 277 | @write.write 50.to_s(16) 278 | @write.write "\r\n" 279 | @write.write @body.slice(0,50) 280 | 281 | @reader.read_nonblock(20) 282 | @reader.read_nonblock(35) 283 | 284 | assert_raises Errno::EWOULDBLOCK do 285 | @reader.read_nonblock 10 286 | end 287 | 288 | @write.write "\r\n" 289 | rest = @body[50..-1] 290 | @write.write rest.size.to_s(16) 291 | @write.write "\r\n" 292 | @write.write rest 293 | 294 | @reader.read_nonblock(1000) 295 | 296 | assert_raises Errno::EWOULDBLOCK do 297 | @reader.read_nonblock 10 298 | end 299 | 300 | @write.write "\r\n0\r\n\r\n" 301 | @reader.read_nonblock 10 302 | 303 | assert_raises EOFError do 304 | @reader.read_nonblock(100) 305 | end 306 | 307 | assert_equal @body, @buf 308 | 309 | assert_raises EOFError do 310 | @reader.read_nonblock(100) 311 | end 312 | end 313 | 314 | class TestBuffer 315 | def initialize(queue) 316 | @write_queue = queue 317 | @string = "" 318 | end 319 | 320 | def <<(str) 321 | @string << str 322 | @write_queue.push :continue 323 | end 324 | 325 | def to_str 326 | @string 327 | end 328 | end 329 | 330 | def test_read_entire_body 331 | write_queue = Queue.new 332 | read_queue = Queue.new 333 | 334 | Thread.new do 335 | @write.write 50.to_s(16) 336 | @write.write "\r\n" 337 | @write.write @body.slice(0,50) 338 | 339 | read_queue.push :continue 340 | write_queue.pop 341 | 342 | @write.write "\r\n" 343 | rest = @body[50..-1] 344 | @write.write rest.size.to_s(16) 345 | @write.write "\r\n" 346 | @write.write rest 347 | 348 | write_queue.pop 349 | 350 | @write.write "\r\n0\r\n\r\n" 351 | 352 | end 353 | 354 | read_queue.pop 355 | 356 | buffer = TestBuffer.new(write_queue) 357 | @reader = Net2::HTTP::ChunkedBodyReader.new(@read, buffer) 358 | out = @reader.read 359 | 360 | assert_equal @body, out.to_str 361 | end 362 | end 363 | end 364 | 365 | -------------------------------------------------------------------------------- /test/utils.rb: -------------------------------------------------------------------------------- 1 | require 'webrick' 2 | begin 3 | require "webrick/https" 4 | rescue LoadError 5 | # SSL features cannot be tested 6 | end 7 | require 'webrick/httpservlet/abstract' 8 | require 'zlib' 9 | require "test/unit" 10 | 11 | require 'net2/http' 12 | Net = Net2 13 | 14 | class String 15 | def encoding_aware? 16 | respond_to? :encoding 17 | end 18 | end 19 | 20 | module TestNetHTTPUtils 21 | def self.force_encoding(string, encoding) 22 | if string.respond_to?(:force_encoding) 23 | string.force_encoding(encoding) 24 | end 25 | end 26 | 27 | def start(&block) 28 | new().start(&block) 29 | end 30 | 31 | def new 32 | klass = Net2::HTTP::Proxy(config('proxy_host'), config('proxy_port')) 33 | http = klass.new(config('host'), config('port')) 34 | http.set_debug_output logfile() 35 | http 36 | end 37 | 38 | def config(key) 39 | self.class::CONFIG[key] 40 | end 41 | 42 | def chunked? 43 | config("chunked") 44 | end 45 | 46 | def gzip? 47 | config("gzip") 48 | end 49 | 50 | def logfile 51 | $DEBUG ? $stderr : NullWriter.new 52 | end 53 | 54 | def setup 55 | spawn_server 56 | end 57 | 58 | def teardown 59 | @server.shutdown 60 | until @server.status == :Stop 61 | sleep 0.1 62 | end 63 | # resume global state 64 | Net::HTTP.version_1_2 65 | end 66 | 67 | def spawn_server 68 | server_config = { 69 | :BindAddress => config('host'), 70 | :Port => config('port'), 71 | :Logger => WEBrick::Log.new(NullWriter.new), 72 | :AccessLog => [], 73 | :ShutdownSocketWithoutClose => true, 74 | :ServerType => Thread, 75 | } 76 | server_config[:OutputBufferSize] = 4 if config('chunked') 77 | if defined?(OpenSSL) and config('ssl_enable') 78 | server_config.update({ 79 | :SSLEnable => true, 80 | :SSLCertificate => config('ssl_certificate'), 81 | :SSLPrivateKey => config('ssl_private_key'), 82 | }) 83 | end 84 | @server = WEBrick::HTTPServer.new(server_config) 85 | @server.mount('/', Servlet, config('chunked'), config("gzip")) 86 | @server.start 87 | n_try_max = 5 88 | begin 89 | TCPSocket.open(config('host'), config('port')).close 90 | rescue Errno::ECONNREFUSED 91 | sleep 0.2 92 | n_try_max -= 1 93 | raise 'cannot spawn server; give up' if n_try_max < 0 94 | retry 95 | end 96 | end 97 | 98 | $test_net_http = nil 99 | $test_net_http_data = (0...256).to_a.map {|i| i.chr }.join('') * 64 100 | force_encoding($test_net_http_data, "ASCII-8BIT") 101 | $test_net_http_data_type = 'application/octet-stream' 102 | 103 | class Servlet < WEBrick::HTTPServlet::AbstractServlet 104 | def initialize(this, chunked = false, gzip = false) 105 | @chunked = chunked 106 | @gzip = gzip 107 | end 108 | 109 | def do_GET(req, res) 110 | res['Content-Type'] = $test_net_http_data_type 111 | res.chunked = @chunked 112 | 113 | raw_body = $test_net_http_data 114 | 115 | if @gzip 116 | res['Content-Encoding'] = 'gzip' 117 | io = StringIO.new 118 | io.set_encoding "BINARY" if io.respond_to?(:set_encoding) 119 | gz = Zlib::GzipWriter.new(io) 120 | gz.write raw_body 121 | gz.close 122 | 123 | body = io.string 124 | end 125 | 126 | res.body = body || raw_body 127 | end 128 | 129 | # echo server 130 | def do_POST(req, res) 131 | res['Content-Type'] = req['Content-Type'] 132 | res.body = req.body 133 | res.chunked = @chunked 134 | end 135 | 136 | def do_PATCH(req, res) 137 | res['Content-Type'] = req['Content-Type'] 138 | res.body = req.body 139 | res.chunked = @chunked 140 | end 141 | end 142 | 143 | class NullWriter 144 | def <<(s) end 145 | def puts(*args) end 146 | def print(*args) end 147 | def printf(*args) end 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /test/webrick/utils.rb: -------------------------------------------------------------------------------- 1 | # 2 | # utils.rb -- Miscellaneous utilities 3 | # 4 | # Author: IPR -- Internet Programming with Ruby -- writers 5 | # Copyright (c) 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou 6 | # Copyright (c) 2002 Internet Programming with Ruby writers. All rights 7 | # reserved. 8 | # 9 | # $IPR: utils.rb,v 1.10 2003/02/16 22:22:54 gotoyuzo Exp $ 10 | 11 | require 'socket' 12 | require 'fcntl' 13 | begin 14 | require 'etc' 15 | rescue LoadError 16 | nil 17 | end 18 | 19 | module WEBrick 20 | module Utils 21 | def set_non_blocking(io) 22 | flag = File::NONBLOCK 23 | if defined?(Fcntl::F_GETFL) 24 | flag |= io.fcntl(Fcntl::F_GETFL) 25 | end 26 | io.fcntl(Fcntl::F_SETFL, flag) 27 | end 28 | module_function :set_non_blocking 29 | 30 | def set_close_on_exec(io) 31 | if defined?(Fcntl::FD_CLOEXEC) 32 | io.fcntl(Fcntl::FD_CLOEXEC, 1) 33 | end 34 | end 35 | module_function :set_close_on_exec 36 | 37 | def su(user) 38 | if defined?(Etc) 39 | pw = Etc.getpwnam(user) 40 | Process::initgroups(user, pw.gid) 41 | Process::Sys::setgid(pw.gid) 42 | Process::Sys::setuid(pw.uid) 43 | else 44 | warn("WEBrick::Utils::su doesn't work on this platform") 45 | end 46 | end 47 | module_function :su 48 | 49 | # the original version of this doesn't work on airplanes 50 | def getservername 51 | Socket::gethostname 52 | end 53 | module_function :getservername 54 | 55 | def create_listeners(address, port, logger=nil) 56 | unless port 57 | raise ArgumentError, "must specify port" 58 | end 59 | res = Socket::getaddrinfo(address, port, 60 | Socket::AF_UNSPEC, # address family 61 | Socket::SOCK_STREAM, # socket type 62 | 0, # protocol 63 | Socket::AI_PASSIVE) # flag 64 | last_error = nil 65 | sockets = [] 66 | res.each{|ai| 67 | begin 68 | logger.debug("TCPServer.new(#{ai[3]}, #{port})") if logger 69 | sock = TCPServer.new(ai[3], port) 70 | port = sock.addr[1] if port == 0 71 | Utils::set_close_on_exec(sock) 72 | sockets << sock 73 | rescue => ex 74 | logger.warn("TCPServer Error: #{ex}") if logger 75 | last_error = ex 76 | end 77 | } 78 | raise last_error if sockets.empty? 79 | return sockets 80 | end 81 | module_function :create_listeners 82 | 83 | RAND_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + 84 | "0123456789" + 85 | "abcdefghijklmnopqrstuvwxyz" 86 | 87 | def random_string(len) 88 | rand_max = RAND_CHARS.bytesize 89 | ret = "" 90 | len.times{ ret << RAND_CHARS[rand(rand_max)] } 91 | ret 92 | end 93 | module_function :random_string 94 | 95 | ########### 96 | 97 | require "thread" 98 | require "timeout" 99 | require "singleton" 100 | 101 | class TimeoutHandler 102 | include Singleton 103 | TimeoutMutex = Mutex.new 104 | 105 | def TimeoutHandler.register(seconds, exception) 106 | TimeoutMutex.synchronize{ 107 | instance.register(Thread.current, Time.now + seconds, exception) 108 | } 109 | end 110 | 111 | def TimeoutHandler.cancel(id) 112 | TimeoutMutex.synchronize{ 113 | instance.cancel(Thread.current, id) 114 | } 115 | end 116 | 117 | def initialize 118 | @timeout_info = Hash.new 119 | Thread.start{ 120 | while true 121 | now = Time.now 122 | @timeout_info.each{|thread, ary| 123 | ary.dup.each{|info| 124 | time, exception = *info 125 | interrupt(thread, info.object_id, exception) if time < now 126 | } 127 | } 128 | sleep 0.5 129 | end 130 | } 131 | end 132 | 133 | def interrupt(thread, id, exception) 134 | TimeoutMutex.synchronize{ 135 | if cancel(thread, id) && thread.alive? 136 | thread.raise(exception, "execution timeout") 137 | end 138 | } 139 | end 140 | 141 | def register(thread, time, exception) 142 | @timeout_info[thread] ||= Array.new 143 | @timeout_info[thread] << [time, exception] 144 | return @timeout_info[thread].last.object_id 145 | end 146 | 147 | def cancel(thread, id) 148 | if ary = @timeout_info[thread] 149 | ary.delete_if{|info| info.object_id == id } 150 | if ary.empty? 151 | @timeout_info.delete(thread) 152 | end 153 | return true 154 | end 155 | return false 156 | end 157 | end 158 | 159 | def timeout(seconds, exception=Timeout::Error) 160 | return yield if seconds.nil? or seconds.zero? 161 | # raise ThreadError, "timeout within critical session" if Thread.critical 162 | id = TimeoutHandler.register(seconds, exception) 163 | begin 164 | yield(seconds) 165 | ensure 166 | TimeoutHandler.cancel(id) 167 | end 168 | end 169 | module_function :timeout 170 | end 171 | end 172 | 173 | --------------------------------------------------------------------------------