├── .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 |
--------------------------------------------------------------------------------