dot_net_services 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ Copyright (c) 2008, ThoughtWorks
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+ * Redistributions of source code must retain the above copyright
7
+ notice, this list of conditions and the following disclaimer.
8
+ * Redistributions in binary form must reproduce the above copyright
9
+ notice, this list of conditions and the following disclaimer in the
10
+ documentation and/or other materials provided with the distribution.
11
+ * Neither the name of the ThoughtWorks nor the
12
+ names of its contributors may be used to endorse or promote products
13
+ derived from this software without specific prior written permission.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY ThoughtWorks ''AS IS'' AND ANY
16
+ EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL THOUGHTWORKS BE LIABLE FOR ANY
19
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README ADDED
@@ -0,0 +1,52 @@
1
+ = .NET Services for Ruby
2
+
3
+ * Project homepage: http://dotnetservicesruby.com
4
+ * Download: http://rubyforge.org/frs/?group_id=7155
5
+ * Demo application: http://dotnetservicesruby.com/billboard
6
+ * Source code: http://rubyforge.org/frs/?group_id=7155
7
+ * Documentation: http://dotnetservicesruby.com/documentation/index.html
8
+
9
+ == What's this?
10
+
11
+ .NET Services for Ruby is an open source library that helps Ruby programs communicate with Microsoft's .NET Services
12
+ using plain HTTP. It was developed by a small team in ThoughtWorks, while Microsoft provided funding, management and
13
+ technical guidance for the project.
14
+
15
+ == Installation
16
+
17
+ The library can be installed as a 'dot_net_services' gem, from RubyForge gem repository:
18
+
19
+ $ gem install dot_net_services
20
+
21
+ or downloaded as an archive from RubyForge [http://rubyforge.org/frs/?group_id=7155].
22
+
23
+ <i>NOTE: Version number 0.1.0 tells you that the API will have backwards-incompatible changes in future, so
24
+ Vendor Everything! [http://errtheblog.com/posts/50-vendor-everything]</i>
25
+
26
+ == Documentation
27
+
28
+ API: http://dotnetservicesruby.com/documentation/classes/DotNetServices.html
29
+
30
+ .NET Services: http://go.microsoft.com/fwlink/?LinkID=129428
31
+
32
+ == Demo application
33
+
34
+ To provide an example of our interop API in action, we have implemented a small Rails application called BillBoard.
35
+ We were working on the API while building the app, which helped us study the technology and discover the right
36
+ abstractions.
37
+
38
+ You can see BillBoard in action at http://dotnetservicesruby.com/billboard and download BillBoard source code from
39
+ RubyForge [http://rubyforge.org/frs/?group_id=7155].
40
+
41
+ == Contacts
42
+
43
+ Users maillist: http://rubyforge.org/mailman/listinfo/dotnetsrv-ruby-users
44
+ ThoughtWorks: info-us@thoughtworks.com
45
+
46
+ == License
47
+
48
+ BSD license. See [LICENSE].
49
+
50
+ == Copyright
51
+
52
+ (c) ThoughtWorks, Inc 2008
@@ -0,0 +1,168 @@
1
+ require 'cgi'
2
+ require 'net/http'
3
+ require 'net/https'
4
+
5
+ module DotNetServices
6
+ # This stores the token and expiration time. The default expiration
7
+ # time is 1 day.
8
+ module Authentication # :nodoc:
9
+ @cache = {}
10
+
11
+ # Authentication token.
12
+ class Token # :nodoc:
13
+
14
+ attr_reader :value, :expiry
15
+
16
+ # Create a new authentication token; defaults to expire in one day.
17
+ def initialize(value, expiry = Time.now + 24 *60 * 60)
18
+ # workaround for a known bug
19
+ match = value.match(/^([^=]+==).*/)
20
+ if match
21
+ @value = match[1]
22
+ @expiry = expiry
23
+ else
24
+ raise AuthenticationError,
25
+ "Response from access control service doesn't seem to contain a valid authentication token:\n" +
26
+ value.inspect
27
+ end
28
+ end
29
+
30
+ def expired?
31
+ @expiry < Time.now
32
+ end
33
+
34
+ def to_s
35
+ @value
36
+ end
37
+ end
38
+
39
+ # Standard Username and Password authenticator.
40
+ class UsernamePassword # :nodoc:
41
+
42
+ attr_reader :token, :username, :password
43
+
44
+ def initialize(username, password, token = nil)
45
+ @username, @password = username, password
46
+ @token = token
47
+ end
48
+
49
+ def authenticate
50
+ return if @token and not @token.expired?
51
+ @token = acquire_token
52
+ end
53
+
54
+ # Enhance the request with the identity token provided by the identity service.
55
+ def enhance(request)
56
+ authenticate
57
+ request['X-MS-Identity-Token'] = token.value
58
+ end
59
+
60
+ def ==(other)
61
+ other.is_a?(Authentication::UsernamePassword) && @username == other.username && @password == other.password
62
+ end
63
+ alias :eql? :==
64
+
65
+ def hash
66
+ @hash ||= @username.hash & @password.hash
67
+ end
68
+
69
+ # Retrieve a token from the DotNetServices token issuing service.
70
+ def acquire_token
71
+ http = Net::HTTP.new(DotNetServices.identity_host, 443)
72
+ http.use_ssl = true
73
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
74
+
75
+ escaped_username = CGI.escape(@username)
76
+ escaped_password = CGI.escape(@password)
77
+ begin
78
+ response = http.get("/issuetoken.aspx?u=#{escaped_username}&p=#{escaped_password}")
79
+ rescue => e
80
+ raise AuthenticationError, "Failed to obtain authentication token. Original error of type #{e.class} " +
81
+ "was overridden to prevent logging security-sensitive data"
82
+ end
83
+
84
+ unless response.is_a?(Net::HTTPOK)
85
+ raise AuthenticationError, "Failed to obtain a security token from the identity service. HTTP response was #{response.class.name}"
86
+ end
87
+
88
+ Token.new(response.body)
89
+ end
90
+ end
91
+
92
+ # Certificate authenticator. NOT YET IMPLEMENTED
93
+ class Certificate # :nodoc:
94
+ def authenticate
95
+ raise "not implemented"
96
+ end
97
+
98
+ def enhance(request)
99
+ raise "not implemented"
100
+ end
101
+
102
+ def hash
103
+ 1
104
+ end
105
+
106
+ def ==(other)
107
+ other.is_a?(Certificate)
108
+ end
109
+ alias :eql? :==
110
+ end
111
+
112
+ # An anonymous authenticator. It is used as a stand-in for
113
+ # services which do not require authentication.
114
+ class Anonymous # :nodoc:
115
+ def authenticate() end
116
+ def enhance(request) end
117
+ def ==(another) another.is_a?(Anonymous) end
118
+ alias :eql? :==
119
+ def hash() -1 end
120
+ end
121
+
122
+ class << self
123
+ def setup(auth_data)
124
+ authenticator = create_authenticator(auth_data)
125
+ @cache[authenticator] ||= authenticator
126
+ end
127
+
128
+ # Create an authenticator based on the data provided.
129
+ def create_authenticator(auth_data)
130
+ if auth_data.nil?
131
+ Authentication::Anonymous.new
132
+ elsif !auth_data.is_a? Hash
133
+ auth_data
134
+ elsif auth_data.empty?
135
+ Authentication::Anonymous.new
136
+ else
137
+ auth_data_copy = auth_data.dup
138
+ username = auth_data_copy.delete(:username)
139
+ password = auth_data_copy.delete(:password)
140
+ certificate = auth_data_copy.delete(:certificate)
141
+
142
+ unless auth_data_copy.empty?
143
+ raise ArgumentError, "Auth data contains unknown options: #{auth_data.keys.inspect}"
144
+ end
145
+
146
+ if username && !password
147
+ raise ArgumentError, "Auth data specifies username, but no password."
148
+ elsif password && !username
149
+ raise ArgumentError, "Auth data specifies password, but no username."
150
+ elsif (username || password) && certificate
151
+ raise ArgumentError, "Cannot determine authentication type from auth data."
152
+ elsif username && password
153
+ Authentication::UsernamePassword.new(username, password)
154
+ elsif certificate
155
+ Authentication::Certificate.new
156
+ else
157
+ raise "Internal error. Unable to setup authenticator from #{auth_data.inspect}"
158
+ end
159
+ end
160
+ end
161
+
162
+ def clear_cache!
163
+ @cache.clear
164
+ end
165
+ end
166
+
167
+ end
168
+ end
@@ -0,0 +1,4 @@
1
+ module DotNetServices
2
+ class AuthenticationError < StandardError # :nodoc:
3
+ end
4
+ end
@@ -0,0 +1,269 @@
1
+ require 'date'
2
+
3
+ module DotNetServices
4
+
5
+ # The MessageBuffer class is used for creating Volatile Message Buffer (VMB) endpoints on the .NET Services bus, and
6
+ # retrieving messages from them.
7
+ #
8
+ # A VMB is a temporary message buffer that can be used for asynchronous communications between senders and receivers
9
+ # of messages. A process may request creation of a VMB on any URL within a solution namespace. Any HTTP requests
10
+ # (other than GETs) to that URL are stored in the buffer, until some process retrieves them by sending an HTTP
11
+ # X-RETRIEVE request to the management endpoint.
12
+ #
13
+ # VMB can exist by itself, not tied to the lifecycle of a process that originally created it. Further information
14
+ # about VMBs can be found in .NET Services portal [http://go.microsoft.com/fwlink/?LinkID=129428].
15
+ #
16
+ # == Usage examples
17
+ #
18
+ # MessageBuffer instance represents a VMB endpoint. Typical usage looks as follows:
19
+ #
20
+ # error_handler = lambda { |error| logger.error(error) }
21
+ #
22
+ # buffer = DotNetServices::MessageBuffer.open_and_poll(
23
+ # "MySolution/MyVMB",
24
+ # {:username => 'MySolution', :password => 'my_password'},
25
+ # error_handler) do
26
+ # |message|
27
+ # ... process the message
28
+ # end
29
+ #
30
+ # This invocation does all of the following:
31
+ #
32
+ # * Creates a MessageBuffer instance associated with a specified endpoint (/MySolution/MyVMB)
33
+ # * obains a security token from trhe Identity service (see Session for details on that).
34
+ # * Creates a VMB on the .NET Services bus if it doesn't exist yet
35
+ # * immediately starts polling it
36
+ # * if it retrieves a message, it passes it to the block. The message is an instance of Net::HTTP::Request that
37
+ # looks exactly the same as what the sender originally sent to the bus.
38
+ # * if an error occurs while polling, the error is passed to the error_handler block. Polling then continues.
39
+ #
40
+ # == Guidelines
41
+ #
42
+ # In cases where an application (which in this case means a process) needs to process multiple types of messages,
43
+ # Microsoft recommends to create a single VMB per application, route all message types through the same VMB, and
44
+ # route messages to appropriate processors within the application itself.
45
+ #
46
+ # In the current version of the API, we provide no explicit support for clustering message processors. If you use
47
+ # MessageBuffer#open_and_poll(), it's recommended that you only run one cipy of a message processor. If clustering
48
+ # is required (for high availability reasons), you can use lower-level MessageBuffer methods to do it.
49
+ #
50
+ # .NET Services VMBs provide pub-sub and multicast functionality which can be used even over plain HTTP. Which is
51
+ # amazing. However, we don't support this functionality in the current version of our library.
52
+ class MessageBuffer
53
+
54
+ attr_reader :session
55
+
56
+ class << self
57
+
58
+ # Creates a MessageBuffer instance, acquires a security token, registers a VMB if necessary.
59
+ # Unlike Session#open, +auth_data+ is mandatory here.
60
+ # If given a block, passes the MessageBuffer instance to the block, and closes it at the end of the block
61
+ # returns the result of the block (if called with a block), or the buffer instance opened
62
+ def open(name, auth_data, &block)
63
+ buffer = MessageBuffer.new(name, auth_data).open
64
+ if block_given?
65
+ begin
66
+ return yield(buffer)
67
+ ensure
68
+ # TODO gracefully close the buffer
69
+ end
70
+ else
71
+ return buffer
72
+ end
73
+ end
74
+
75
+ # Invoke MessageBuffer#open to create a MessageBuffer instance, subscribe the buffer to receive messages
76
+ # posted to the +subscription_endpoint+, then poll it until the containing thread or process is terminated.
77
+ #
78
+ # Whenever a message is retrieved from the buffer, this method passes it to the block. When an error occurs
79
+ # (while polling, or raised by the block), it is passed to +error_handler+, which should be a lambda, or an
80
+ # object with handle(error) public method. If no +error_handler+ is provided, the error is printed out
81
+ # to STDERR and then ignored.
82
+ def open_and_poll(name, subscription_endpoint, auth_data, error_handler=nil, &block)
83
+ MessageBuffer.new(name, auth_data).open_and_poll(subscription_endpoint, error_handler, &block)
84
+ end
85
+ end
86
+
87
+ # Initializes a new MessageBuffer instance.
88
+ # Does not create a VMB on the .NET Services bus (use #open for that)
89
+ def initialize(name, auth_data)
90
+ @name = name
91
+ @session = Session.new(name, auth_data)
92
+ end
93
+
94
+ # Register the message buffer (by sending X-CREATEMB to the management endpoint).
95
+ #
96
+ # Returns the buffer. Raises an exception if the creation is not successful.
97
+ def register
98
+ createmb_response = @session.createmb
99
+
100
+ unless createmb_response.is_a?(Net::HTTPCreated)
101
+ # the buffer may already be there
102
+ unless @session.get_from_relay.is_a?(Net::HTTPSuccess)
103
+ raise "Creating VMB failed. Service responded with #{createmb_response.class.name}"
104
+ end
105
+ end
106
+ self
107
+ end
108
+
109
+ # Initiates a VMB session by:
110
+ # * acquiring a security token
111
+ # * checks if there is already a VMB at the endpoint
112
+ # * creating a VMB if necessary
113
+ def open
114
+ register unless @session.get_from_relay.is_a?(Net::HTTPSuccess)
115
+ self
116
+ end
117
+
118
+ # Open the buffer (see #open), subscribe the buffer to receive messages
119
+ # posted to the +subscription_endpoint+, then poll it until the containing thread or
120
+ # process is terminated.
121
+ #
122
+ # Whenever a message is retrieved from the buffer, this method passes it to the block. When an error occurs
123
+ # (while polling, or raised by the block), it is passed to +error_handler+, which should be a lambda, or an
124
+ # object with handle(error) public method. If no +error_handler+ is provided, the error is printed out
125
+ # to STDERR and then ignored.
126
+ def open_and_poll(subscription_endpoint, error_handler=nil, &block)
127
+ raise "MessageBuffer#open_and_poll requires a block" unless block_given?
128
+
129
+ @subscription_endpoint = subscription_endpoint
130
+
131
+ open
132
+ subscribe(@subscription_endpoint)
133
+
134
+ begin
135
+ loop do
136
+ begin
137
+ message = poll
138
+ block.call(message) if message
139
+ rescue Object => error
140
+ if error_handler
141
+ if error_handler.is_a?(Proc)
142
+ error_handler.call(error)
143
+ else
144
+ error_handler.handle(error)
145
+ end
146
+ else
147
+ STDERR.puts
148
+ STDERR.puts "Message Buffer Error"
149
+ STDERR.puts error.message
150
+ STDERR.puts error.backtrace.map { |line| " #{line}"}
151
+ STDERR.puts
152
+ end
153
+ end
154
+ end
155
+ ensure
156
+ @subscription_endpoint = nil
157
+ end
158
+ end
159
+
160
+ # Poll VMB endpoint for new messages; return the message if one was retrieved, or nil if it wasn't.
161
+ #
162
+ # Raises an exception if the response from the bus contains an error code (which usually means that the VMB is
163
+ # not registered, but may mean other things, for example if the bus itself is not accessible for some reason).
164
+ #
165
+ # +timeout+ parameter regulates how long a polling request will be held by the bus if there are no messages.
166
+ #
167
+ # An HTTP server that holds HTTP connections because it has nothing to respond with is somewhat uncommon, so a
168
+ # more detailed explanation is due.
169
+ #
170
+ # Normally, any HTTP interaction begins by opening of a TCP socket from the client to the server. The client
171
+ # then writes the HTTP request into that socket and waits until the server responds back and closes the socket.
172
+ # If the server doesn't respond for a long time (a minute, usually), the client drops the socket
173
+ # and declares timeout.
174
+ #
175
+ # Many times, when you poll a VMB endpoint, it will have no messages. If the bus simply came back immediately with
176
+ # "No Content" response, this would cause a VMB subscriber needs to constantly poll the buffer, abusing the bus
177
+ # infrastructure.
178
+ #
179
+ # .NET Services gets around this problem by making the client connection hang for some time, either until there
180
+ # is a message, or a certain number of seconds has passed, and there is still no message. That number of seconds is
181
+ # what the +timeout+ parameter specifies. Microsoft suggested that 10-20 seconds is reasonable for
182
+ # production purposes. #open_and_poll() loop uses 15.
183
+ def poll(timeout=nil)
184
+ response = @session.retrieve(:encoding => 'asreply', :timeout => timeout)
185
+ case response
186
+ when Net::HTTPNoContent
187
+ return nil
188
+ when Net::HTTPNotFound
189
+ register
190
+ subscribe(@subscription_endpoint) if @subscription_endpoint
191
+ return nil
192
+ when Net::HTTPSuccess
193
+ return response
194
+ else
195
+ raise "Retrieving messages failed. Response was #{response.class.name}" unless response.is_a?(Net::HTTPSuccess)
196
+ end
197
+ end
198
+
199
+ # Delete the message buffer. This removes the buffer entirely
200
+ # from the bus, not just the local reference to it.
201
+ def delete
202
+ response = @session.delete
203
+
204
+ unless response.is_a?(Net::HTTPNoContent)
205
+ raise "Deleting VMB failed. Response was #{response.class.name}"
206
+ end
207
+ self
208
+ end
209
+
210
+ # Queries the VMB management endpoint for the VMB expiry time. With every #poll (HTTP X-RETRIEVE to the VMB management
211
+ # endpoint) the bus sets the expiry time of this VMB to 30 minutes later. If 30 minutes pass and there are no
212
+ # further polls, the bus automatically delets the buffer.
213
+ def expires
214
+ response = @session.get_from_relay
215
+
216
+ unless response.is_a?(Net::HTTPOK)
217
+ raise "Querying expiry status of VMB failed. Response was #{response.class}"
218
+ end
219
+
220
+ expires_header = response["expires"]
221
+ raise "Querying expiry status of VMB failed. Response doesn't have expires: header" unless expires_header
222
+ DateTime.parse(expires_header)
223
+ end
224
+
225
+ # Performs an empty POST to the VMB management endpoint, which extends the VMB expiry time without retrieving any
226
+ # messages.
227
+ def keep_alive
228
+ # if we don't pass a linefeed as a body to the VMB management endpoint, it responds with HTTP 411 Length Required
229
+ response = @session.post_to_relay "\n"
230
+ case response
231
+ when Net::HTTPSuccess
232
+ self
233
+ else
234
+ raise "POST to the VMB management endpoint failed. Response was #{response.class}"
235
+ end
236
+ end
237
+
238
+ # Subscribe to an endpoint.
239
+ #
240
+ # HTTP messages sent to the +endpoint+ will be routed to this message buffer
241
+ def subscribe(target_path)
242
+ subscription_endpoint_url = DotNetServices.root_url + "/" + target_path
243
+ subscribe_response = @session.subscribe(:target => subscription_endpoint_url)
244
+ case subscribe_response
245
+ when Net::HTTPSuccess
246
+ return self
247
+ when Net::HTTPConflict
248
+ unsubscribe(target_path)
249
+ resubscribe_response = @session.subscribe(:target => subscription_endpoint_url)
250
+ unless resubscribe_response.is_a?(Net::HTTPSuccess)
251
+ raise "Second X-SUBSCRIBE to VMB management endpoint failed. Response was #{resubscribe_response.class}"
252
+ end
253
+ else
254
+ raise "X-SUBSCRIBE to VMB management endpoint failed. Response was #{subscribe_response.class}"
255
+ end
256
+ end
257
+
258
+ # Unsubscribe from an endpoint.
259
+ def unsubscribe(target_path)
260
+ subscription_endpoint_url = DotNetServices.root_url + "/" + target_path
261
+ unsubscribe_response = @session.unsubscribe(:target => subscription_endpoint_url)
262
+ unless unsubscribe_response.is_a?(Net::HTTPSuccess)
263
+ raise "X-UNSUBSCRIBE to VMB management endpoint failed. Response was #{subscribe_response.class}"
264
+ end
265
+ self
266
+ end
267
+
268
+ end
269
+ end