handsoap 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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