handsoap 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'handsoap/http/request'
3
+ require 'handsoap/http/response'
4
+ require 'handsoap/http/drivers'
@@ -0,0 +1,24 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'handsoap/http/drivers/abstract_driver'
3
+ require 'handsoap/http/drivers/curb_driver'
4
+ require 'handsoap/http/drivers/event_machine_driver'
5
+ require 'handsoap/http/drivers/http_client_driver'
6
+ require 'handsoap/http/drivers/net_http_driver'
7
+ require 'handsoap/http/drivers/mock_driver'
8
+
9
+ module Handsoap
10
+ module Http
11
+ @@drivers = {
12
+ :curb => Drivers::CurbDriver,
13
+ :em => Drivers::EventMachineDriver,
14
+ :event_machine => Drivers::EventMachineDriver,
15
+ :httpclient => Drivers::HttpClientDriver,
16
+ :http_client => Drivers::HttpClientDriver,
17
+ :net_http => Drivers::NetHttpDriver,
18
+ }
19
+
20
+ def self.drivers
21
+ @@drivers
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,170 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module Handsoap
4
+ module Http
5
+ module Drivers
6
+ class AbstractDriver
7
+ def self.load!
8
+ end
9
+
10
+ def initialize
11
+ self.class.load!
12
+ end
13
+
14
+ # Parses a raw http response into a +Response+ or +Part+ object.
15
+ def parse_http_part(headers, body, status = nil, content_type = nil)
16
+ if headers.kind_of? String
17
+ headers = parse_headers(headers)
18
+ end
19
+ headers = headers.inject({}) {|collect,item| collect[item[0].downcase] = item[1]; collect }
20
+ if content_type.nil? && headers['content-type']
21
+ content_type = headers['content-type'].first
22
+ end
23
+ boundary = parse_multipart_boundary(content_type)
24
+ parts = if boundary
25
+ parse_multipart(boundary, body).map {|raw_part| parse_http_part(raw_part[:head], raw_part[:body]) }
26
+ end
27
+ if status.nil?
28
+ Handsoap::Http::Part.new(headers, body, parts)
29
+ else
30
+ Handsoap::Http::Response.new(status, headers, body, parts)
31
+ end
32
+ end
33
+
34
+ # Content-Type header string -> mime-boundary | nil
35
+ def parse_multipart_boundary(content_type)
36
+ if %r|\Amultipart.*boundary=\"?([^\";,]+)\"?|n.match(content_type)
37
+ $1.dup
38
+ end
39
+ end
40
+
41
+ # Parses a multipart http-response body into parts.
42
+ # +boundary+ is a string of the boundary token.
43
+ # +content_io+ is either a string or an IO. If it's an IO, then content_length must be specified.
44
+ # +content_length+ (optional) is an integer, specifying the length of +content_io+
45
+ #
46
+ # This code is lifted from cgi.rb
47
+ #
48
+ def parse_multipart(boundary, content_io, content_length = nil)
49
+ if content_io.kind_of? String
50
+ content_length = content_io.length
51
+ content_io = StringIO.new(content_io, 'r')
52
+ elsif !(content_io.kind_of? IO) || content_length.nil?
53
+ raise "Second argument must be String or IO with content_length"
54
+ end
55
+
56
+ boundary = "--" + boundary
57
+ quoted_boundary = Regexp.quote(boundary, "n")
58
+ buf = ""
59
+ bufsize = 10 * 1024
60
+ boundary_end = ""
61
+
62
+ # start multipart/form-data
63
+ content_io.binmode if defined? content_io.binmode
64
+ boundary_size = boundary.size + "\r\n".size
65
+ content_length -= boundary_size
66
+ status = content_io.read(boundary_size)
67
+ if nil == status
68
+ raise EOFError, "no content body"
69
+ elsif boundary + "\r\n" != status
70
+ raise EOFError, "bad content body"
71
+ end
72
+
73
+ parts = []
74
+
75
+ loop do
76
+ head = nil
77
+ if 10240 < content_length
78
+ require "tempfile"
79
+ body = Tempfile.new("Handsoap")
80
+ else
81
+ begin
82
+ require "stringio"
83
+ body = StringIO.new
84
+ rescue LoadError
85
+ require "tempfile"
86
+ body = Tempfile.new("Handsoap")
87
+ end
88
+ end
89
+ body.binmode if defined? body.binmode
90
+
91
+ until head and /#{quoted_boundary}(?:\r\n|--)/n.match(buf)
92
+
93
+ if (not head) and /\r\n\r\n/n.match(buf)
94
+ buf = buf.sub(/\A((?:.|\n)*?\r\n)\r\n/n) do
95
+ head = $1.dup
96
+ ""
97
+ end
98
+ next
99
+ end
100
+
101
+ if head and ( ("\r\n" + boundary + "\r\n").size < buf.size )
102
+ body.print buf[0 ... (buf.size - ("\r\n" + boundary + "\r\n").size)]
103
+ buf[0 ... (buf.size - ("\r\n" + boundary + "\r\n").size)] = ""
104
+ end
105
+
106
+ c = if bufsize < content_length
107
+ content_io.read(bufsize)
108
+ else
109
+ content_io.read(content_length)
110
+ end
111
+ if c.nil? || c.empty?
112
+ raise EOFError, "bad content body"
113
+ end
114
+ buf.concat(c)
115
+ content_length -= c.size
116
+ end
117
+
118
+ buf = buf.sub(/\A((?:.|\n)*?)(?:[\r\n]{1,2})?#{quoted_boundary}([\r\n]{1,2}|--)/n) do
119
+ body.print $1
120
+ if "--" == $2
121
+ content_length = -1
122
+ end
123
+ boundary_end = $2.dup
124
+ ""
125
+ end
126
+
127
+ body.rewind
128
+ parts << {:head => head, :body => body.read(body.size)}
129
+
130
+ break if buf.size == 0
131
+ break if content_length == -1
132
+ end
133
+ raise EOFError, "bad boundary end of body part" unless boundary_end =~ /--/
134
+ parts
135
+ end
136
+
137
+ # lifted from webrick/httputils.rb
138
+ def parse_headers(raw)
139
+ header = Hash.new([].freeze)
140
+ field = nil
141
+ tmp = raw.gsub(/^(\r\n)+|(\r\n)+$/, '')
142
+ (tmp.respond_to?(:lines) ? tmp.lines : tmp).each {|line|
143
+ case line
144
+ when /^([A-Za-z0-9!\#$%&'*+\-.^_`|~]+):\s*(.*?)\s*\z/om
145
+ field, value = $1, $2
146
+ field.downcase!
147
+ header[field] = [] unless header.has_key?(field)
148
+ header[field] << value
149
+ when /^\s+(.*?)\s*\z/om
150
+ value = $1
151
+ unless field
152
+ raise "bad header '#{line.inspect}'."
153
+ end
154
+ header[field][-1] << " " << value
155
+ else
156
+ raise "bad header '#{line.inspect}'."
157
+ end
158
+ }
159
+ header.each {|key, values|
160
+ values.each {|value|
161
+ value.strip!
162
+ value.gsub!(/\s+/, " ")
163
+ }
164
+ }
165
+ header
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,41 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'handsoap/http/drivers/abstract_driver'
3
+
4
+ module Handsoap
5
+ module Http
6
+ module Drivers
7
+ class CurbDriver < AbstractDriver
8
+ def self.load!
9
+ require 'curb'
10
+ end
11
+
12
+ def send_http_request(request)
13
+ http_client = Curl::Easy.new(request.url)
14
+ # Set credentials. The driver will negotiate the actual scheme
15
+ if request.username && request.password
16
+ http_client.userpwd = [request.username, ":", request.password].join
17
+ end
18
+ # pack headers
19
+ headers = request.headers.inject([]) do |arr, (k,v)|
20
+ arr + v.map {|x| "#{k}: #{x}" }
21
+ end
22
+ http_client.headers = headers
23
+ # I don't think put/delete is actually supported ..
24
+ case request.http_method
25
+ when :get
26
+ http_client.http_get
27
+ when :post
28
+ http_client.http_post(request.body)
29
+ when :put
30
+ http_client.http_put(request.body)
31
+ when :delete
32
+ http_client.http_delete
33
+ else
34
+ raise "Unsupported request method #{request.http_method}"
35
+ end
36
+ parse_http_part(http_client.header_str.gsub(/^HTTP.*\r\n/, ""), http_client.body_str, http_client.response_code, http_client.content_type)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,46 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module Handsoap
4
+ module Http
5
+ module Drivers
6
+ class EventMachineDriver < AbstractDriver
7
+ def self.load!
8
+ require 'eventmachine'
9
+ require 'em-http'
10
+ end
11
+
12
+ def send_http_request_async(request)
13
+ emr = EventMachine::HttpRequest.new(request.url)
14
+
15
+ if request.username && request.password
16
+ # TODO: Verify that this is actually supported?
17
+ request.headers['authorization'] = [request.username, request.password]
18
+ end
19
+
20
+ case request.http_method
21
+ when :get
22
+ emdef = emr.get(:head => request.headers)
23
+ when :post
24
+ emdef = emr.post(:head => request.headers, :body => request.body)
25
+ when :put
26
+ emdef = emr.put(:head => request.headers, :body => request.body)
27
+ when :delete
28
+ emdef = emr.delete
29
+ else
30
+ raise "Unsupported request method #{request.http_method}"
31
+ end
32
+
33
+ deferred = Handsoap::Deferred.new
34
+ emdef.callback do
35
+ http_response = parse_http_part(emdef.response_header, emdef.response, emdef.response_header.status)
36
+ deferred.trigger_callback http_response
37
+ end
38
+ emdef.errback do
39
+ deferred.trigger_errback emdef
40
+ end
41
+ deferred
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,38 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'handsoap/http/drivers/abstract_driver'
3
+
4
+ module Handsoap
5
+ module Http
6
+ module Drivers
7
+ class HttpClientDriver < AbstractDriver
8
+ def self.load!
9
+ require 'httpclient'
10
+ end
11
+
12
+ def send_http_request(request)
13
+ http_client = HTTPClient.new
14
+ # Set credentials. The driver will negotiate the actual scheme
15
+ if request.username && request.password
16
+ domain = request.url.match(/^(http(s?):\/\/[^\/]+\/)/)[1]
17
+ http_client.set_auth(domain, request.username, request.password)
18
+ end
19
+ # pack headers
20
+ headers = request.headers.inject([]) do |arr, (k,v)|
21
+ arr + v.map {|x| [k,x] }
22
+ end
23
+ response = http_client.request(request.http_method, request.url, nil, request.body, headers)
24
+ response_headers = response.header.all.inject({}) do |h, (k, v)|
25
+ k.downcase!
26
+ if h[k].nil?
27
+ h[k] = [v]
28
+ else
29
+ h[k] << v
30
+ end
31
+ h
32
+ end
33
+ parse_http_part(response_headers, response.content, response.status, response.contenttype)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,39 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'handsoap/http/drivers/abstract_driver'
3
+
4
+ module Handsoap
5
+ module Http
6
+ module Drivers
7
+ # A mock driver for your testing needs.
8
+ #
9
+ # To use it, create a new instance and assign to +Handsoap::Http.drivers+. Then configure +Handsoap::Service+ to use it:
10
+ #
11
+ # Handsoap::Http.drivers[:mock] = Handsoap::Http::Drivers::MockDriver.new :status => 200, :headers => headers, :content => body
12
+ # Handsoap.http_driver = :mock
13
+ #
14
+ # Remember that headers should use \r\n, rather than \n.
15
+ class MockDriver < AbstractDriver
16
+ attr_accessor :mock, :last_request, :is_loaded
17
+
18
+ def initialize(mock)
19
+ @mock = mock
20
+ @is_loaded = false
21
+ end
22
+
23
+ def load!
24
+ is_loaded = true
25
+ end
26
+
27
+ def new
28
+ self
29
+ end
30
+
31
+ def send_http_request(request)
32
+ @last_request = request
33
+ (mock.kind_of? Hash) ?
34
+ parse_http_part(mock[:headers], mock[:content], mock[:status], mock[:content_type]) : mock
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,69 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module Handsoap
4
+ module Http
5
+ module Drivers
6
+ class NetHttpDriver < AbstractDriver
7
+ def self.load!
8
+ require 'net/http'
9
+ require 'uri'
10
+ end
11
+
12
+ def send_http_request(request)
13
+ url = request.url
14
+ unless url.kind_of? ::URI::Generic
15
+ url = ::URI.parse(url)
16
+ end
17
+ ::URI::Generic.send(:public, :path_query) # hackety hack
18
+ path = url.path_query
19
+ http_request = case request.http_method
20
+ when :get
21
+ Net::HTTP::Get.new(path)
22
+ when :post
23
+ Net::HTTP::Post.new(path)
24
+ when :put
25
+ Net::HTTP::Put.new(path)
26
+ when :delete
27
+ Net::HTTP::Delete.new(path)
28
+ else
29
+ raise "Unsupported request method #{request.http_method}"
30
+ end
31
+ http_client = Net::HTTP.new(url.host, url.port)
32
+ http_client.read_timeout = 120
33
+ if request.username && request.password
34
+ # TODO: http://codesnippets.joyent.com/posts/show/1075
35
+ http_request.basic_auth request.username, request.password
36
+ end
37
+ request.headers.each do |k, values|
38
+ values.each do |v|
39
+ http_request.add_field(k, v)
40
+ end
41
+ end
42
+ http_request.body = request.body
43
+ # require 'stringio'
44
+ # debug_output = StringIO.new
45
+ # http_client.set_debug_output debug_output
46
+ http_response = http_client.start do |client|
47
+ client.request(http_request)
48
+ end
49
+ # puts debug_output.string
50
+ # hacky-wacky
51
+ def http_response.get_headers
52
+ @header.inject({}) do |h, (k, v)|
53
+ h[k.downcase] = v
54
+ h
55
+ end
56
+ end
57
+ # net/http only supports basic auth. We raise a warning if the server requires something else.
58
+ if http_response.code == 401 && http_response.get_headers['www-authenticate']
59
+ auth_type = http_response.get_headers['www-authenticate'].chomp.match(/\w+/)[0].downcase
60
+ if auth_type != "basic"
61
+ raise "Authentication type #{auth_type} is unsupported by net/http"
62
+ end
63
+ end
64
+ parse_http_part(http_response.get_headers, http_response.body, http_response.code)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,78 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module Handsoap
4
+ module Http
5
+
6
+ # Represents a HTTP Part.
7
+ # For simple HTTP-requests there is only one part, which is the response.
8
+ class Part
9
+ attr_reader :headers, :body, :parts
10
+
11
+ def initialize(headers, body, parts = nil)
12
+ @headers = headers
13
+ @body = body
14
+ @parts = parts
15
+ end
16
+
17
+ # Returns a header.
18
+ # Returns String | Array | nil
19
+ def [](key)
20
+ key.to_s.downcase!
21
+ (@headers[key] && @headers[key].length == 1) ? @headers[key].first : @headers[key]
22
+ end
23
+
24
+ # Returns the mime-type part of the content-type header
25
+ def mime_type
26
+ @headers['content-type'].first.match(/^[^;]+/).to_s if @headers['content-type']
27
+ end
28
+
29
+ # Returns the charset part of the content-type header
30
+ def charset
31
+ if @headers['content-type']
32
+ match_data = @headers['content-type'].first.match(/^[^;]+; charset=([^;]+)/)
33
+ if match_data
34
+ match_data[1].to_s
35
+ end
36
+ end
37
+ end
38
+
39
+ def multipart?
40
+ !! @parts
41
+ end
42
+
43
+ def inspect(&block)
44
+ str = inspect_head
45
+ if headers.any?
46
+ str << headers.map { |key,values| values.map {|value| normalize_header_key(key) + ": " + value + "\n" }.join("") }.join("")
47
+ end
48
+ if body
49
+ if multipart?
50
+ if block_given?
51
+ str << parts.map{|part| part.inspect(&block) }.join("")
52
+ else
53
+ str << parts.map{|part| part.inspect }.join("")
54
+ end
55
+ elsif body
56
+ str << "---\n"
57
+ if block_given?
58
+ str << yield(body)
59
+ else
60
+ str << body
61
+ end
62
+ str << "\n---"
63
+ end
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def inspect_head
70
+ "--- Part ---\n"
71
+ end
72
+
73
+ def normalize_header_key(key)
74
+ key.split("-").map{|s| s.downcase.capitalize }.join("-")
75
+ end
76
+ end
77
+ end
78
+ end