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.
- data/README.markdown +105 -0
- data/VERSION.yml +4 -0
- data/generators/handsoap/USAGE +0 -0
- data/generators/handsoap/handsoap_generator.rb +113 -0
- data/generators/handsoap/templates/DUMMY +0 -0
- data/lib/handsoap.rb +4 -0
- data/lib/handsoap/compiler.rb +182 -0
- data/lib/handsoap/deferred.rb +50 -0
- data/lib/handsoap/http.rb +4 -0
- data/lib/handsoap/http/drivers.rb +24 -0
- data/lib/handsoap/http/drivers/abstract_driver.rb +170 -0
- data/lib/handsoap/http/drivers/curb_driver.rb +41 -0
- data/lib/handsoap/http/drivers/event_machine_driver.rb +46 -0
- data/lib/handsoap/http/drivers/http_client_driver.rb +38 -0
- data/lib/handsoap/http/drivers/mock_driver.rb +39 -0
- data/lib/handsoap/http/drivers/net_http_driver.rb +69 -0
- data/lib/handsoap/http/part.rb +78 -0
- data/lib/handsoap/http/request.rb +63 -0
- data/lib/handsoap/http/response.rb +28 -0
- data/lib/handsoap/parser.rb +241 -0
- data/lib/handsoap/service.rb +481 -0
- data/lib/handsoap/xml_mason.rb +221 -0
- data/lib/handsoap/xml_query_front.rb +320 -0
- metadata +81 -0
@@ -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
|