t2-server 0.9.3 → 1.0.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/CHANGES.rdoc +79 -0
- data/LICENCE.rdoc +1 -1
- data/README.rdoc +94 -26
- data/Rakefile +6 -5
- data/bin/t2-delete-runs +25 -23
- data/bin/t2-get-output +11 -13
- data/bin/t2-run-workflow +91 -29
- data/bin/t2-server-info +29 -12
- data/extras/t2-server-stress +184 -0
- data/lib/t2-server-cli.rb +48 -23
- data/lib/t2-server.rb +2 -1
- data/lib/t2-server/admin.rb +7 -4
- data/lib/t2-server/exceptions.rb +23 -4
- data/lib/t2-server/interaction.rb +241 -0
- data/lib/t2-server/net/connection.rb +90 -60
- data/lib/t2-server/net/credentials.rb +25 -9
- data/lib/t2-server/net/parameters.rb +21 -6
- data/lib/t2-server/port.rb +229 -140
- data/lib/t2-server/run-cache.rb +99 -0
- data/lib/t2-server/run.rb +349 -332
- data/lib/t2-server/server.rb +115 -164
- data/lib/t2-server/util.rb +11 -9
- data/lib/t2-server/xml/libxml.rb +3 -2
- data/lib/t2-server/xml/nokogiri.rb +4 -3
- data/lib/t2-server/xml/rexml.rb +3 -2
- data/lib/t2-server/xml/xml.rb +47 -36
- data/lib/{t2server.rb → t2-server/xml/xpath_cache.rb} +29 -7
- data/t2-server.gemspec +16 -5
- data/test/tc_misc.rb +61 -0
- data/test/tc_perms.rb +17 -1
- data/test/tc_run.rb +164 -34
- data/test/tc_secure.rb +11 -2
- data/test/tc_server.rb +23 -2
- data/test/ts_t2server.rb +10 -8
- data/test/workflows/missing_outputs.t2flow +440 -0
- data/version.yml +3 -3
- metadata +42 -4
data/lib/t2-server-cli.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2010-
|
1
|
+
# Copyright (c) 2010-2013 The University of Manchester, UK.
|
2
2
|
#
|
3
3
|
# All rights reserved.
|
4
4
|
#
|
@@ -50,27 +50,8 @@ module T2Server
|
|
50
50
|
end
|
51
51
|
|
52
52
|
# SSL options
|
53
|
-
opt
|
54
|
-
|
55
|
-
"optional password is not provided it will be asked for on the " +
|
56
|
-
"command line. Must be in PEM format.") do |val|
|
57
|
-
cert, cpass = val.chomp.split(":", 2)
|
58
|
-
conn_params[:client_certificate] = cert
|
59
|
-
conn_params[:client_password] = cpass if cpass
|
60
|
-
end
|
61
|
-
opt.on("--cacert=CERT_FILE", "Use the specified certificate file to " +
|
62
|
-
"verify the peer. Must be in PEM format.") do |val|
|
63
|
-
conn_params[:ca_file] = val.chomp
|
64
|
-
end
|
65
|
-
opt.on("--capath=CERTS_PATH", "Use the specified certificate " +
|
66
|
-
"directory to verify the peer. Certificates must be in PEM " +
|
67
|
-
"format") do |val|
|
68
|
-
conn_params[:ca_path] = val.chomp
|
69
|
-
end
|
70
|
-
opt.on("-k", "--insecure", "Allow insecure connections: no peer " +
|
71
|
-
"verification.") do
|
72
|
-
conn_params[:verify_peer] = false
|
73
|
-
end
|
53
|
+
ssl_auth_opts(opt, conn_params)
|
54
|
+
ssl_transport_opts(opt, conn_params)
|
74
55
|
|
75
56
|
# common options
|
76
57
|
opt.on_tail("-u", "--username=USERNAME", "The username to use for " +
|
@@ -100,7 +81,7 @@ module T2Server
|
|
100
81
|
|
101
82
|
# separate the creds if they are supplied in the uri
|
102
83
|
def parse_address(address, creds)
|
103
|
-
if address == nil
|
84
|
+
if address == nil || address == ""
|
104
85
|
puts @opts
|
105
86
|
exit 1
|
106
87
|
end
|
@@ -112,5 +93,49 @@ module T2Server
|
|
112
93
|
def opts
|
113
94
|
@opts
|
114
95
|
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
# The SSL authentication and peer verification options.
|
100
|
+
def ssl_auth_opts(opt, conn_params)
|
101
|
+
opt.on("-E CERT_FILE:PASSWORD", "--cert=CERT_FILE:PASSWORD", "Use " +
|
102
|
+
"the specified certificate file for client authentication. If the " +
|
103
|
+
"optional password is not provided it will be asked for on the " +
|
104
|
+
"command line. Must be in PEM format.") do |val|
|
105
|
+
cert, cpass = val.chomp.split(":", 2)
|
106
|
+
conn_params[:client_certificate] = cert
|
107
|
+
conn_params[:client_password] = cpass if cpass
|
108
|
+
end
|
109
|
+
opt.on("--cacert=CERT_FILE", "Use the specified certificate file to " +
|
110
|
+
"verify the peer. Must be in PEM format.") do |val|
|
111
|
+
conn_params[:ca_file] = val.chomp
|
112
|
+
end
|
113
|
+
opt.on("--capath=CERTS_PATH", "Use the specified certificate " +
|
114
|
+
"directory to verify the peer. Certificates must be in PEM " +
|
115
|
+
"format") do |val|
|
116
|
+
conn_params[:ca_path] = val.chomp
|
117
|
+
end
|
118
|
+
opt.on("-k", "--insecure", "Allow insecure connections: no peer " +
|
119
|
+
"verification.") do
|
120
|
+
conn_params[:verify_peer] = false
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# The SSL transport options.
|
125
|
+
def ssl_transport_opts(opt, conn_params)
|
126
|
+
opt.on("-1", "--tlsv1", "Use TLS version 1 when negotiating with " +
|
127
|
+
"the remote Taverna Server server.") do
|
128
|
+
conn_params[:ssl_version] = :TLSv1
|
129
|
+
end
|
130
|
+
opt.on("-2", "--sslv2", "Use SSL version 2 when negotiating with " +
|
131
|
+
"the remote Taverna Server server.") do
|
132
|
+
conn_params[:ssl_version] = :SSLv23
|
133
|
+
end
|
134
|
+
opt.on("-3", "--sslv3", "Use SSL version 3 when negotiating with " +
|
135
|
+
"the remote Taverna Server server.") do
|
136
|
+
conn_params[:ssl_version] = :SSLv3
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
115
140
|
end
|
116
141
|
end
|
data/lib/t2-server.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2010-
|
1
|
+
# Copyright (c) 2010-2013 The University of Manchester, UK.
|
2
2
|
#
|
3
3
|
# All rights reserved.
|
4
4
|
#
|
@@ -38,6 +38,7 @@ require 't2-server/net/credentials'
|
|
38
38
|
require 't2-server/net/connection'
|
39
39
|
require 't2-server/net/parameters'
|
40
40
|
require 't2-server/port'
|
41
|
+
require 't2-server/interaction'
|
41
42
|
require 't2-server/server'
|
42
43
|
require 't2-server/run'
|
43
44
|
require 't2-server/admin'
|
data/lib/t2-server/admin.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2010-
|
1
|
+
# Copyright (c) 2010-2013 The University of Manchester, UK.
|
2
2
|
#
|
3
3
|
# All rights reserved.
|
4
4
|
#
|
@@ -30,7 +30,11 @@
|
|
30
30
|
#
|
31
31
|
# Author: Robert Haines
|
32
32
|
|
33
|
+
# :stopdoc:
|
34
|
+
# This comment is needed to stop the above licence from being included in the
|
35
|
+
# documentation multiple times. Sigh.
|
33
36
|
module T2Server
|
37
|
+
# :startdoc:
|
34
38
|
|
35
39
|
# This call provides access to the administrative interface of a Taverna
|
36
40
|
# Server instance.
|
@@ -56,7 +60,6 @@ module T2Server
|
|
56
60
|
admin_description = xml_document(@server.read(@uri, "application/xml",
|
57
61
|
@credentials))
|
58
62
|
@resources = get_resources(admin_description)
|
59
|
-
#@resources.each {|key, value| puts "#{key}: #{value}"}
|
60
63
|
|
61
64
|
yield(self) if block_given?
|
62
65
|
end
|
@@ -117,7 +120,7 @@ module T2Server
|
|
117
120
|
# :startdoc:
|
118
121
|
|
119
122
|
# :call-seq:
|
120
|
-
# value ->
|
123
|
+
# value -> string
|
121
124
|
# value=
|
122
125
|
#
|
123
126
|
# Get or set the value held by this resource. This call always queries
|
@@ -129,7 +132,7 @@ module T2Server
|
|
129
132
|
end
|
130
133
|
|
131
134
|
# :call-seq:
|
132
|
-
# writable? ->
|
135
|
+
# writable? -> true or false
|
133
136
|
#
|
134
137
|
# Is this resource writable?
|
135
138
|
def writable?
|
data/lib/t2-server/exceptions.rb
CHANGED
@@ -83,12 +83,31 @@ module T2Server
|
|
83
83
|
# not necessarily indicate a problem with the server.
|
84
84
|
class UnexpectedServerResponse < T2ServerError
|
85
85
|
|
86
|
-
#
|
86
|
+
# The method that was called to produce this error.
|
87
|
+
attr_reader :method
|
88
|
+
|
89
|
+
# The path of the URI that returned this error.
|
90
|
+
attr_reader :path
|
91
|
+
|
92
|
+
# The HTTP error code of this error.
|
93
|
+
attr_reader :code
|
94
|
+
|
95
|
+
# The response body of this error. If the server did not supply one then
|
96
|
+
# this will be "<none>".
|
97
|
+
attr_reader :body
|
98
|
+
|
99
|
+
# Create a new UnexpectedServerResponse with details of which HTTP method
|
100
|
+
# was called, the path that it was called on and the specified unexpected
|
87
101
|
# response. The response to be passed in is that which was returned by a
|
88
102
|
# call to Net::HTTP#request.
|
89
|
-
def initialize(response)
|
90
|
-
|
91
|
-
|
103
|
+
def initialize(method, path, response)
|
104
|
+
@method = method
|
105
|
+
@path = path
|
106
|
+
@code = response.code
|
107
|
+
@body = response.body.empty? ? "<none>" : "#{response.body}"
|
108
|
+
message = "Unexpected server response:\n Method: #{@method}\n Path: "\
|
109
|
+
"#{@path}\n Code: #{@code}\n Body: #{@body}"
|
110
|
+
super message
|
92
111
|
end
|
93
112
|
end
|
94
113
|
|
@@ -0,0 +1,241 @@
|
|
1
|
+
# Copyright (c) 2010-2013 The University of Manchester, UK.
|
2
|
+
#
|
3
|
+
# All rights reserved.
|
4
|
+
#
|
5
|
+
# Redistribution and use in source and binary forms, with or without
|
6
|
+
# modification, are permitted provided that the following conditions are met:
|
7
|
+
#
|
8
|
+
# * Redistributions of source code must retain the above copyright notice,
|
9
|
+
# this list of conditions and the following disclaimer.
|
10
|
+
#
|
11
|
+
# * Redistributions in binary form must reproduce the above copyright notice,
|
12
|
+
# this list of conditions and the following disclaimer in the documentation
|
13
|
+
# and/or other materials provided with the distribution.
|
14
|
+
#
|
15
|
+
# * Neither the names of The University of Manchester nor the names of its
|
16
|
+
# contributors may be used to endorse or promote products derived from this
|
17
|
+
# software without specific prior written permission.
|
18
|
+
#
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
20
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
21
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
22
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
23
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
24
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
25
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
26
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
27
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
28
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
29
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
30
|
+
#
|
31
|
+
# Author: Robert Haines
|
32
|
+
|
33
|
+
require 'rubygems'
|
34
|
+
require 'atom'
|
35
|
+
require 'uri'
|
36
|
+
|
37
|
+
module T2Server
|
38
|
+
|
39
|
+
# The Interaction module provides access to Taverna workflow notifications
|
40
|
+
# as supplied by the Interaction Service. These are read from an Atom feed
|
41
|
+
# and returned as Notification objects. For more information about the
|
42
|
+
# Interaction Service, see
|
43
|
+
# http://dev.mygrid.org.uk/wiki/display/taverna/Interaction+service
|
44
|
+
module Interaction
|
45
|
+
|
46
|
+
# :stopdoc:
|
47
|
+
FEED_NS = "http://ns.taverna.org.uk/2012/interaction"
|
48
|
+
|
49
|
+
class Feed
|
50
|
+
def initialize(run)
|
51
|
+
@run = run
|
52
|
+
@cache = {:requests => {}, :replies => {}}
|
53
|
+
end
|
54
|
+
|
55
|
+
# Get all new notification requests since they were last checked.
|
56
|
+
#
|
57
|
+
# Here we really only want new unanswered notifications, but polling
|
58
|
+
# returns all requests new to *us*, even those that have been replied to
|
59
|
+
# elsewhere. Filter out answered requests here.
|
60
|
+
def new_requests
|
61
|
+
poll(:requests).select { |i| !i.has_reply? }
|
62
|
+
end
|
63
|
+
|
64
|
+
# Get all notifications, or all of a particular type.
|
65
|
+
def notifications(type = :all)
|
66
|
+
poll
|
67
|
+
|
68
|
+
case type
|
69
|
+
when :requests, :replies
|
70
|
+
@cache[type].values
|
71
|
+
else
|
72
|
+
@cache[:requests].values + @cache[:replies].values
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def entries(&block)
|
79
|
+
feed = Atom::Feed.load_feed(@run.read_notification_feed)
|
80
|
+
feed.each_entry(:paginate => true, &block)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Poll for all notification types and update the caches.
|
84
|
+
#
|
85
|
+
# Returns any new notifications, [] otherwise. If you are only
|
86
|
+
# interested in knowing about new notifications of a specific type you
|
87
|
+
# can use the type parameter to specify this. Use :requests, :replies or
|
88
|
+
# :all (default).
|
89
|
+
def poll(type = :all)
|
90
|
+
updates = []
|
91
|
+
requests = @cache[:requests]
|
92
|
+
replies = @cache[:replies]
|
93
|
+
|
94
|
+
entries do |entry|
|
95
|
+
# It's worth noting what happens here.
|
96
|
+
#
|
97
|
+
# This connection to a run's notification feed may not be the only
|
98
|
+
# one, or it might be a reconnection. As a result we might see a
|
99
|
+
# reply before we see the original request; atom feeds are last in
|
100
|
+
# first out.
|
101
|
+
#
|
102
|
+
# So if we see a reply, we should check to see if we have the
|
103
|
+
# request before setting the request to "replied". And if we see a
|
104
|
+
# request we should check to see if we have a reply for it already;
|
105
|
+
# we may have seen the reply the previous time through the loop.
|
106
|
+
note = Notification.new(entry, @run)
|
107
|
+
if note.is_reply?
|
108
|
+
next if replies.has_key? note.reply_to
|
109
|
+
requests[note.reply_to].has_reply unless requests[note.reply_to].nil?
|
110
|
+
replies[note.reply_to] = note
|
111
|
+
updates << note if type == :replies || type == :all
|
112
|
+
else
|
113
|
+
next if requests.has_key? note.id
|
114
|
+
note.has_reply unless replies[note.id].nil?
|
115
|
+
requests[note.id] = note
|
116
|
+
updates << note if type == :requests || type == :all
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
updates
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
# :startdoc:
|
125
|
+
|
126
|
+
# This class represents a Taverna notification.
|
127
|
+
class Notification
|
128
|
+
|
129
|
+
# The identifier of this notification.
|
130
|
+
attr_reader :id
|
131
|
+
|
132
|
+
# If this notification is a reply then this is the identifier of the
|
133
|
+
# notification that it is a reply to.
|
134
|
+
attr_reader :reply_to
|
135
|
+
|
136
|
+
# The URI of the notification page to show.
|
137
|
+
attr_reader :uri
|
138
|
+
|
139
|
+
# The serial number of a notification. This identifies a notification
|
140
|
+
# within a workflow.
|
141
|
+
attr_reader :serial
|
142
|
+
|
143
|
+
# :stopdoc:
|
144
|
+
def initialize(entry, run)
|
145
|
+
@run = run
|
146
|
+
reply_to = entry[FEED_NS, "in-reply-to"]
|
147
|
+
if reply_to.empty?
|
148
|
+
@is_reply = false
|
149
|
+
@has_reply = false
|
150
|
+
@id = entry[FEED_NS, "id"][0]
|
151
|
+
@is_notification = entry[FEED_NS, "progress"].empty? ? false : true
|
152
|
+
@uri = get_link(entry.links)
|
153
|
+
@serial = "#{entry[FEED_NS, 'path'][0]}-#{entry[FEED_NS, 'count'][0]}"
|
154
|
+
else
|
155
|
+
@is_reply = true
|
156
|
+
@is_notification = false
|
157
|
+
@reply_to = reply_to[0]
|
158
|
+
end
|
159
|
+
end
|
160
|
+
# :startdoc:
|
161
|
+
|
162
|
+
# :call-seq:
|
163
|
+
# is_reply? -> true or false
|
164
|
+
#
|
165
|
+
# Is this notification a reply to another notification?
|
166
|
+
def is_reply?
|
167
|
+
@is_reply
|
168
|
+
end
|
169
|
+
|
170
|
+
# :call-seq:
|
171
|
+
# is_notification? -> true or false
|
172
|
+
#
|
173
|
+
# Is this notification a pure notification only? There is no user
|
174
|
+
# response to a pure notification, it is for information only.
|
175
|
+
def is_notification?
|
176
|
+
@is_notification
|
177
|
+
end
|
178
|
+
|
179
|
+
# :stopdoc:
|
180
|
+
def has_reply
|
181
|
+
@has_reply = true
|
182
|
+
end
|
183
|
+
# :startdoc:
|
184
|
+
|
185
|
+
# :call-seq:
|
186
|
+
# has_reply? -> true or false
|
187
|
+
#
|
188
|
+
# Does this notification have a reply? This only makes sense for
|
189
|
+
# notifications that are not replies or pure notifications.
|
190
|
+
def has_reply?
|
191
|
+
@has_reply
|
192
|
+
end
|
193
|
+
|
194
|
+
# :call-seq:
|
195
|
+
# input_data -> data
|
196
|
+
#
|
197
|
+
# Get the input data associated with this notification. Returns an empty
|
198
|
+
# string if this notification is a reply.
|
199
|
+
def input_data
|
200
|
+
return "" if is_reply?
|
201
|
+
|
202
|
+
data_name = "interaction#{@id}InputData.json"
|
203
|
+
@run.read_interaction_data(data_name)
|
204
|
+
rescue AttributeNotFoundError
|
205
|
+
# It does not matter if the file doesn't exist.
|
206
|
+
""
|
207
|
+
end
|
208
|
+
|
209
|
+
# :call-seq:
|
210
|
+
# reply(status, data)
|
211
|
+
#
|
212
|
+
# Given a status and some data this method uploads the data and
|
213
|
+
# publishes an interaction reply on the run's notification feed.
|
214
|
+
def reply(status, data)
|
215
|
+
data_name = "interaction#{@id}OutputData.json"
|
216
|
+
|
217
|
+
notification = Atom::Entry.new do |entry|
|
218
|
+
entry.title = "A reply to #{@id}"
|
219
|
+
entry.id = "#{@id}reply"
|
220
|
+
entry.content = ""
|
221
|
+
entry[FEED_NS, "run-id"] << @run.id
|
222
|
+
entry[FEED_NS, "in-reply-to"] << @id
|
223
|
+
entry[FEED_NS, "result-status"] << status
|
224
|
+
end.to_xml
|
225
|
+
|
226
|
+
@run.write_interaction_data(data_name, data)
|
227
|
+
@run.write_notification(notification)
|
228
|
+
end
|
229
|
+
|
230
|
+
private
|
231
|
+
|
232
|
+
def get_link(links)
|
233
|
+
links.each do |l|
|
234
|
+
return URI.parse(l.to_s) if l.rel == "presentation"
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
end
|
240
|
+
|
241
|
+
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2010-
|
1
|
+
# Copyright (c) 2010-2013 The University of Manchester, UK.
|
2
2
|
#
|
3
3
|
# All rights reserved.
|
4
4
|
#
|
@@ -59,7 +59,7 @@ module T2Server
|
|
59
59
|
end
|
60
60
|
|
61
61
|
# if we're given params they must be of the right type
|
62
|
-
if !params.nil?
|
62
|
+
if !params.nil? && !params.is_a?(ConnectionParameters)
|
63
63
|
raise ArgumentError, "Parameters must be ConnectionParameters", caller
|
64
64
|
end
|
65
65
|
|
@@ -93,23 +93,27 @@ module T2Server
|
|
93
93
|
@uri = uri
|
94
94
|
@params = params || DefaultConnectionParameters.new
|
95
95
|
|
96
|
-
#
|
96
|
+
# Open a persistent HTTP connection.
|
97
97
|
@http = Net::HTTP::Persistent.new("Taverna_Server_Ruby_Client")
|
98
|
+
|
99
|
+
# Set timeouts if specified.
|
100
|
+
@http.open_timeout = @params[:open_timeout] if @params[:open_timeout]
|
101
|
+
@http.read_timeout = @params[:read_timeout] if @params[:read_timeout]
|
98
102
|
end
|
99
103
|
|
100
104
|
# :call-seq:
|
101
|
-
# GET(uri, type, range, credentials) ->
|
105
|
+
# GET(uri, type, range, credentials) -> string
|
102
106
|
#
|
103
107
|
# HTTP GET a resource at _uri_ of _type_ from the server. If successful
|
104
108
|
# the body of the response is returned. A portion of the data can be
|
105
109
|
# retrieved by specifying a byte range, start..end, with the _range_
|
106
110
|
# parameter.
|
107
|
-
def GET(uri, type, range, credentials)
|
111
|
+
def GET(uri, type, range, credentials, &block)
|
108
112
|
get = Net::HTTP::Get.new(uri.path)
|
109
113
|
get["Accept"] = type
|
110
114
|
get["Range"] = "bytes=#{range.min}-#{range.max}" unless range.nil?
|
111
115
|
|
112
|
-
response = submit(get, uri, credentials)
|
116
|
+
response = submit(get, uri, credentials, &block)
|
113
117
|
|
114
118
|
case response
|
115
119
|
when Net::HTTPOK, Net::HTTPPartialContent
|
@@ -119,36 +123,35 @@ module T2Server
|
|
119
123
|
when Net::HTTPMovedTemporarily
|
120
124
|
new_conn = redirect(response["location"])
|
121
125
|
raise ConnectionRedirectError.new(new_conn)
|
122
|
-
when Net::HTTPNotFound
|
123
|
-
raise AttributeNotFoundError.new(uri.path)
|
124
|
-
when Net::HTTPForbidden
|
125
|
-
raise AccessForbiddenError.new("attribute #{uri.path}")
|
126
|
-
when Net::HTTPUnauthorized
|
127
|
-
raise AuthorizationError.new(credentials)
|
128
126
|
else
|
129
|
-
|
127
|
+
report_error("GET", uri.path, response, credentials)
|
130
128
|
end
|
131
129
|
end
|
132
130
|
|
133
131
|
# :call-seq:
|
134
|
-
# PUT(uri, value, type, credentials) ->
|
132
|
+
# PUT(uri, value, type, credentials) -> true or false
|
135
133
|
# PUT(uri, value, type, credentials) -> URI
|
134
|
+
# PUT(uri, stream, type, credentials) -> URI
|
135
|
+
#
|
136
|
+
# Upload data via HTTP PUT. Data may be specified as a value or as a
|
137
|
+
# stream. The stream can be any object that has a read(length) method;
|
138
|
+
# instances of File or IO, for example.
|
136
139
|
#
|
137
|
-
#
|
138
|
-
#
|
139
|
-
#
|
140
|
-
|
141
|
-
def PUT(uri, value, type, credentials)
|
140
|
+
# If successful _true_ or a URI to the uploaded resource is returned
|
141
|
+
# depending on whether the operation has altered a parameter (true) or
|
142
|
+
# uploaded new data (URI).
|
143
|
+
def PUT(uri, data, type, credentials)
|
142
144
|
put = Net::HTTP::Put.new(uri.path)
|
143
145
|
put.content_type = type
|
144
|
-
|
146
|
+
|
147
|
+
set_upload_body(put, data)
|
145
148
|
|
146
149
|
response = submit(put, uri, credentials)
|
147
150
|
|
148
151
|
case response
|
149
|
-
when Net::HTTPOK
|
150
|
-
# We've set a parameter so we get 200
|
151
|
-
# true to indicate success.
|
152
|
+
when Net::HTTPOK, Net::HTTPAccepted
|
153
|
+
# We've either set a parameter or started a run so we get 200 or 202
|
154
|
+
# back from the server, respectively. Return true to indicate success.
|
152
155
|
true
|
153
156
|
when Net::HTTPCreated
|
154
157
|
# We've uploaded data so we get 201 back from the server. Return the
|
@@ -158,26 +161,27 @@ module T2Server
|
|
158
161
|
# We've modified data so we get 204 back from the server. Return the
|
159
162
|
# uri of the modified resource.
|
160
163
|
uri
|
161
|
-
when Net::
|
162
|
-
raise
|
163
|
-
when Net::HTTPForbidden
|
164
|
-
raise AccessForbiddenError.new("attribute #{uri.path}")
|
165
|
-
when Net::HTTPUnauthorized
|
166
|
-
raise AuthorizationError.new(credentials)
|
164
|
+
when Net::HTTPServiceUnavailable
|
165
|
+
raise ServerAtCapacityError.new
|
167
166
|
else
|
168
|
-
|
167
|
+
report_error("PUT", uri.path, response, credentials)
|
169
168
|
end
|
170
169
|
end
|
171
170
|
|
172
171
|
# :call-seq:
|
173
|
-
# POST(uri, value, type, credentials)
|
172
|
+
# POST(uri, value, type, credentials) -> URI
|
173
|
+
# POST(uri, stream, type, credentials) -> URI
|
174
|
+
#
|
175
|
+
# Upload data via HTTP POST. Data may be specified as a value or as a
|
176
|
+
# stream. The stream can be any object that has a read(length) method;
|
177
|
+
# instances of File or IO, for example.
|
174
178
|
#
|
175
|
-
#
|
176
|
-
|
177
|
-
def POST(uri, value, type, credentials)
|
179
|
+
# If successful the URI of the uploaded resource is returned.
|
180
|
+
def POST(uri, data, type, credentials)
|
178
181
|
post = Net::HTTP::Post.new(uri.path)
|
179
182
|
post.content_type = type
|
180
|
-
|
183
|
+
|
184
|
+
set_upload_body(post, data)
|
181
185
|
|
182
186
|
response = submit(post, uri, credentials)
|
183
187
|
|
@@ -185,23 +189,15 @@ module T2Server
|
|
185
189
|
when Net::HTTPCreated
|
186
190
|
# return the URI of the newly created item
|
187
191
|
URI.parse(response['location'])
|
188
|
-
when Net::
|
189
|
-
raise
|
190
|
-
when Net::HTTPForbidden
|
191
|
-
if response.body.chomp.include? "server load exceeded"
|
192
|
-
raise ServerAtCapacityError.new
|
193
|
-
else
|
194
|
-
raise AccessForbiddenError.new("attribute #{uri.path}")
|
195
|
-
end
|
196
|
-
when Net::HTTPUnauthorized
|
197
|
-
raise AuthorizationError.new(credentials)
|
192
|
+
when Net::HTTPServiceUnavailable
|
193
|
+
raise ServerAtCapacityError.new
|
198
194
|
else
|
199
|
-
|
195
|
+
report_error("POST", uri.path, response, credentials)
|
200
196
|
end
|
201
197
|
end
|
202
198
|
|
203
199
|
# :call-seq:
|
204
|
-
# DELETE(uri, credentials) ->
|
200
|
+
# DELETE(uri, credentials) -> true or false
|
205
201
|
#
|
206
202
|
# Perform an HTTP DELETE on a _uri_ on the server. If successful true
|
207
203
|
# is returned.
|
@@ -214,19 +210,13 @@ module T2Server
|
|
214
210
|
when Net::HTTPNoContent
|
215
211
|
# Success, carry on...
|
216
212
|
true
|
217
|
-
when Net::HTTPNotFound
|
218
|
-
false
|
219
|
-
when Net::HTTPForbidden
|
220
|
-
raise AccessForbiddenError.new(uri)
|
221
|
-
when Net::HTTPUnauthorized
|
222
|
-
raise AuthorizationError.new(credentials)
|
223
213
|
else
|
224
|
-
|
214
|
+
report_error("DELETE", uri.path, response, credentials)
|
225
215
|
end
|
226
216
|
end
|
227
217
|
|
228
218
|
# :call-seq:
|
229
|
-
# OPTIONS(uri, credentials) ->
|
219
|
+
# OPTIONS(uri, credentials) -> hash
|
230
220
|
#
|
231
221
|
# Perform the HTTP OPTIONS command on the given _uri_ and return a hash
|
232
222
|
# of the headers returned.
|
@@ -238,23 +228,59 @@ module T2Server
|
|
238
228
|
case response
|
239
229
|
when Net::HTTPOK
|
240
230
|
response.to_hash
|
231
|
+
else
|
232
|
+
report_error("OPTIONS", uri.path, response, credentials)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
private
|
237
|
+
|
238
|
+
# If one of the expected responses for a HTTP method is not received then
|
239
|
+
# handle the error condition here.
|
240
|
+
def report_error(method, path, response, credentials)
|
241
|
+
case response
|
242
|
+
when Net::HTTPNotFound
|
243
|
+
raise AttributeNotFoundError.new(path)
|
241
244
|
when Net::HTTPForbidden
|
242
|
-
raise AccessForbiddenError.new("resource #{
|
245
|
+
raise AccessForbiddenError.new("resource #{path}")
|
243
246
|
when Net::HTTPUnauthorized
|
244
247
|
raise AuthorizationError.new(credentials)
|
245
248
|
else
|
246
|
-
raise UnexpectedServerResponse.new(response)
|
249
|
+
raise UnexpectedServerResponse.new(method, path, response)
|
247
250
|
end
|
248
251
|
end
|
249
252
|
|
250
|
-
|
253
|
+
# If we have a stream then we need to set body_stream and then either
|
254
|
+
# supply a content length or set the transfer encoding to "chunked". A
|
255
|
+
# file object can supply its size, a bare IO object cannot. If we have a
|
256
|
+
# simple value we can set body directly.
|
257
|
+
def set_upload_body(request, data)
|
258
|
+
if data.respond_to? :read
|
259
|
+
request.body_stream = data
|
260
|
+
if data.respond_to? :size
|
261
|
+
request.content_length = data.size
|
262
|
+
else
|
263
|
+
request["Transfer-encoding"] = "chunked"
|
264
|
+
end
|
265
|
+
else
|
266
|
+
request.body = data
|
267
|
+
end
|
268
|
+
end
|
251
269
|
|
252
|
-
|
270
|
+
# If a block is passed in here then the response is returned in chunks
|
271
|
+
# (streamed). If no block is passed in the whole response is read into
|
272
|
+
# memory and returned.
|
273
|
+
def submit(request, uri, credentials, &block)
|
253
274
|
|
254
275
|
credentials.authenticate(request) unless credentials.nil?
|
255
276
|
|
277
|
+
response = nil
|
256
278
|
begin
|
257
|
-
@http.request(uri, request)
|
279
|
+
@http.request(uri, request) do |r|
|
280
|
+
r.read_body(&block)
|
281
|
+
response = r
|
282
|
+
end
|
283
|
+
response
|
258
284
|
rescue InternalHTTPError => e
|
259
285
|
raise ConnectionError.new(e)
|
260
286
|
end
|
@@ -276,6 +302,10 @@ module T2Server
|
|
276
302
|
def initialize(uri, params = nil)
|
277
303
|
super(uri, params)
|
278
304
|
|
305
|
+
if OpenSSL::SSL::SSLContext::METHODS.include? @params[:ssl_version]
|
306
|
+
@http.ssl_version = @params[:ssl_version]
|
307
|
+
end
|
308
|
+
|
279
309
|
# Peer verification
|
280
310
|
if @params[:verify_peer]
|
281
311
|
if @params[:ca_file]
|