dot_net_services 0.0.1

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