marilyn-rpc 0.0.2 → 0.0.3

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.
Files changed (46) hide show
  1. data/README.md +39 -4
  2. data/examples/async/client.rb +0 -5
  3. data/examples/authentication/client.rb +18 -0
  4. data/examples/authentication/server.rb +30 -0
  5. data/lib/marilyn-rpc.rb +1 -1
  6. data/lib/marilyn-rpc/client.rb +21 -3
  7. data/lib/marilyn-rpc/error.rb +13 -0
  8. data/lib/marilyn-rpc/mails.rb +3 -3
  9. data/lib/marilyn-rpc/server.rb +6 -11
  10. data/lib/marilyn-rpc/service.rb +86 -11
  11. data/lib/marilyn-rpc/service_cache.rb +44 -23
  12. data/lib/marilyn-rpc/version.rb +1 -1
  13. data/spec/mails_spec.rb +2 -1
  14. data/spec/server_spec.rb +1 -1
  15. data/spec/service_cache_spec.rb +140 -0
  16. data/spec/service_spec.rb +56 -5
  17. data/spec/spec_helper.rb +5 -0
  18. metadata +10 -34
  19. data/doc/MarilynRPC.html +0 -108
  20. data/doc/MarilynRPC/CallRequestMail.html +0 -578
  21. data/doc/MarilynRPC/CallResponseMail.html +0 -418
  22. data/doc/MarilynRPC/Envelope.html +0 -705
  23. data/doc/MarilynRPC/ExceptionMail.html +0 -338
  24. data/doc/MarilynRPC/Gentleman.html +0 -658
  25. data/doc/MarilynRPC/MailFactory.html +0 -284
  26. data/doc/MarilynRPC/MailHelper.html +0 -489
  27. data/doc/MarilynRPC/NativeClient.html +0 -579
  28. data/doc/MarilynRPC/NativeClientProxy.html +0 -303
  29. data/doc/MarilynRPC/Server.html +0 -406
  30. data/doc/MarilynRPC/Service.html +0 -599
  31. data/doc/MarilynRPC/ServiceCache.html +0 -481
  32. data/doc/_index.html +0 -219
  33. data/doc/class_list.html +0 -36
  34. data/doc/css/common.css +0 -1
  35. data/doc/css/full_list.css +0 -53
  36. data/doc/css/style.css +0 -318
  37. data/doc/file.README.html +0 -154
  38. data/doc/file_list.html +0 -38
  39. data/doc/frames.html +0 -13
  40. data/doc/index.html +0 -154
  41. data/doc/js/app.js +0 -203
  42. data/doc/js/full_list.js +0 -149
  43. data/doc/js/jquery.js +0 -16
  44. data/doc/method_list.html +0 -467
  45. data/doc/top-level-namespace.html +0 -88
  46. data/spec/service_cache.rb +0 -45
data/README.md CHANGED
@@ -9,10 +9,7 @@ The services are unique per connection, so if you have 50 connections, 50
9
9
  service objects will be used, if (and only if) they are requested by the client.
10
10
 
11
11
  Since this is a session dedicated to one connection, marilyn has support for per
12
- connection caching by using instance variables. Further on, it is planned to
13
- enhance the capabilities of marilyn to allow connection based authentication.
14
- Like in other protocols (e.g. IMAP) where some methods can be called
15
- unauthenticated and some not.
12
+ connection caching by simply using instance variables.
16
13
 
17
14
  Like in IMAP marilyn supports sending of multiple requests to a server over one
18
15
  connection. This feature is called multiplexing supported by the current
@@ -122,6 +119,44 @@ The client also simply has to enable a secure connection:
122
119
 
123
120
  client = MarilynRPC::NativeClient.connect_tcp('localhost', 8008, :secure => true)
124
121
 
122
+ ## Authentication
123
+
124
+ If some remote service methods only should be called using a username/password
125
+ protection, one simply has to define which method and what mechanism:
126
+
127
+ MarilynRPC::Service.authenticate_with do |username, password|
128
+ username == "testuserid" && password == "secret"
129
+ end
130
+
131
+ class TestService < MarilynRPC::Service
132
+ register :test
133
+ authentication_required :add
134
+
135
+ def time # unauthenticated
136
+ puts session_username
137
+ puts session_authenticated?
138
+ Time.now
139
+ end
140
+
141
+ def add(a, b) # authenticated
142
+ puts session_username
143
+ puts session_authenticated?
144
+ a + b
145
+ end
146
+ end
147
+
148
+ The client simple has to do the authentication before start marking calls:
149
+
150
+ client = MarilynRPC::NativeClient.connect_tcp('localhost', 8000)
151
+ TestService = client.for(:test)
152
+
153
+ p TestService.time.to_f
154
+ client.authenticate "testuserid", "secret"
155
+ p TestService.add(1, 2)
156
+
157
+ client.disconnect
158
+
159
+
125
160
  ## Async Server Example & NativeClient
126
161
 
127
162
  As previously said, the server can use the `Gentleman` to issue asynchronous
@@ -19,7 +19,6 @@ done = false
19
19
  t3 = Thread.new do
20
20
  while !done
21
21
  sleep 1
22
- p client
23
22
  STDOUT.print "."
24
23
  STDOUT.flush
25
24
  end
@@ -29,10 +28,6 @@ t1.join
29
28
  t2.join
30
29
 
31
30
  done = true
32
- t3.join
33
-
34
- p client
35
-
36
31
  end_time = Time.now
37
32
 
38
33
  puts "#{start_time - end_time}"
@@ -0,0 +1,18 @@
1
+ $:.push(File.join(File.dirname(__FILE__), "..", "..", "lib"))
2
+ require "marilyn-rpc"
3
+ client = MarilynRPC::NativeClient.connect_tcp('localhost', 8000)
4
+ TestService = client.for(:test)
5
+
6
+ begin
7
+ p TestService.add(1, 2)
8
+ rescue MarilynRPC::MarilynError => ex
9
+ puts "PermissionDenied: #{ex.message}"
10
+ end
11
+
12
+ p TestService.time.to_f
13
+
14
+ client.authenticate "testuserid", "secret"
15
+
16
+ p TestService.add(1, 2)
17
+
18
+ client.disconnect
@@ -0,0 +1,30 @@
1
+ $:.push(File.join(File.dirname(__FILE__), "..", "..", "lib"))
2
+ require "marilyn-rpc"
3
+ require "rubygems"
4
+ require "eventmachine"
5
+
6
+ MarilynRPC::Service.authenticate_with do |username, password|
7
+ username == "testuserid" && password == "secret"
8
+ end
9
+
10
+ class TestService < MarilynRPC::Service
11
+ register :test
12
+ authentication_required :add
13
+
14
+ def time
15
+ puts session_username
16
+ puts session_authenticated?
17
+ Time.now
18
+ end
19
+
20
+ def add(a, b)
21
+ puts session_username
22
+ puts session_authenticated?
23
+ a + b
24
+ end
25
+ end
26
+
27
+
28
+ EM.run {
29
+ EM.start_server "localhost", 8000, MarilynRPC::Server
30
+ }
@@ -1,4 +1,4 @@
1
- %w(version gentleman envelope mails service
1
+ %w(error version gentleman envelope mails service
2
2
  server service_cache client).each do |file|
3
3
  require File.join(File.dirname(__FILE__), "marilyn-rpc", file)
4
4
  end
@@ -2,11 +2,12 @@ require 'socket'
2
2
  require 'thread'
3
3
 
4
4
  module MarilynRPC
5
- class BlankSlate
5
+ # A class with nothing but `__send__` and `__id__`
6
+ class ClientBlankSlate
6
7
  instance_methods.each { |m| undef_method m unless m =~ /^__/ }
7
8
  end
8
9
 
9
- class NativeClientProxy < BlankSlate
10
+ class NativeClientProxy < ClientBlankSlate
10
11
  # Creates a new Native client proxy, were the calls get send to the remote
11
12
  # side.
12
13
  # @param [Object] path the path that is used to identify the service
@@ -62,6 +63,16 @@ module MarilynRPC
62
63
  def disconnect
63
64
  @socket.close
64
65
  end
66
+
67
+ # authenicate the client to call methods that require authentication
68
+ # @param [String] username the username of the client
69
+ # @param [String] password the password of the client
70
+ # @param [Symbol] method the method to use for authentication, currently
71
+ # only plain is supported. So make sure you are using a secure socket.
72
+ def authenticate(username, password, method = :plain)
73
+ execute(MarilynRPC::Service::AUTHENTICATION_PATH,
74
+ "authenticate_#{method}".to_sym, [username, password])
75
+ end
65
76
 
66
77
  # Creates a new Proxy Object for the connection.
67
78
  # @param [Object] path the path were the service is registered on the remote
@@ -139,8 +150,15 @@ module MarilynRPC
139
150
  if mail.is_a? MarilynRPC::CallResponseMail
140
151
  mail.result
141
152
  else
142
- raise mail.exception
153
+ raise MarilynError.new # raise exception to capture the client backtrace
143
154
  end
155
+ rescue MarilynError => exception
156
+ # add local and remote trace together and reraise the original exception
157
+ backtrace = []
158
+ backtrace += exception.backtrace
159
+ backtrace += mail.exception.backtrace
160
+ mail.exception.set_backtrace(backtrace)
161
+ raise mail.exception
144
162
  end
145
163
  end
146
164
  end
@@ -0,0 +1,13 @@
1
+ module MarilynRPC
2
+ class MarilynError < StandardError; end
3
+
4
+ # Error that occurs, when we recieve an broken envelope
5
+ class BrokenEnvelopeError < MarilynError; end
6
+
7
+ # Error that occurs, when the client tries to call an unknown service
8
+ class UnknownServiceError < MarilynError; end
9
+
10
+ # Error that occurs, when the client tries to call an service, that requires
11
+ # authentication. The client must be authenticated before that call.
12
+ class PermissionDeniedError < MarilynError; end
13
+ end
@@ -61,11 +61,11 @@ module MarilynRPC
61
61
  TYPE = MarilynRPC::MailHelper.type(3)
62
62
 
63
63
  def encode
64
- TYPE + serialize(self.exception)
64
+ TYPE + serialize([self.tag, self.exception])
65
65
  end
66
66
 
67
67
  def decode(data)
68
- self.exception = deserialize(only_data(data))
68
+ self.tag, self.exception = deserialize(only_data(data))
69
69
  end
70
70
  end
71
71
 
@@ -86,7 +86,7 @@ module MarilynRPC
86
86
  when MarilynRPC::ExceptionMail::TYPE
87
87
  mail = MarilynRPC::ExceptionMail.new
88
88
  else
89
- raise ArgumentError.new("The passed type #{type.inspect} is unknown!")
89
+ raise MarilynRPC::BrokenEnvelopeError.new("The passed envelope is broken!")
90
90
  end
91
91
  mail.decode(data)
92
92
  mail
@@ -34,17 +34,12 @@ module MarilynRPC::Server
34
34
 
35
35
  # was massage parsed successfully?
36
36
  if @envelope.finished?
37
- begin
38
- # grep the request
39
- mail = MarilynRPC::MailFactory.unpack(@envelope)
40
- answer = @cache.call(mail)
41
- if answer.is_a? MarilynRPC::CallResponseMail
42
- send_mail(answer)
43
- else
44
- answer.connection = self # pass connection for async responses
45
- end
46
- rescue => exception
47
- send_mail(MarilynRPC::ExceptionMail.new(mail.tag, exception))
37
+ # grep the request
38
+ answer = @cache.call(@envelope)
39
+ if answer.is_a? MarilynRPC::Gentleman
40
+ answer.connection = self # pass connection for async responses
41
+ else
42
+ send_mail(answer)
48
43
  end
49
44
 
50
45
  # initialize the next envelope
@@ -1,10 +1,19 @@
1
+ # A class with nothing but `__send__`, `__id__`, `class` and `public_methods`.
2
+ class ServiceBlankSlate
3
+ instance_methods.each { |m| undef_method m unless m =~ /^__|public_methods|class/ }
4
+ end
5
+
1
6
  # This class represents the base for all events, it is used for registering
2
- # services and defining callbacks for certain service events.
7
+ # services and defining callbacks for certain service events. It is also
8
+ # possible to enable an authentication check for methods of the service.
9
+ # @attr [MarilynRPC::ServiceCache] service_cache the service cache where an
10
+ # instance lives in
3
11
  # @example a service that makes use of the available helpers
4
12
  # class EventsService < MarilynRPC::Service
5
13
  # register :events
6
14
  # after_connect :connected
7
15
  # after_disconnect :disconnected
16
+ # authentication_required :notify
8
17
  #
9
18
  # def connected
10
19
  # puts "client connected"
@@ -19,7 +28,9 @@
19
28
  # end
20
29
  # end
21
30
  #
22
- class MarilynRPC::Service
31
+ class MarilynRPC::Service < ServiceBlankSlate
32
+ attr_accessor :service_cache
33
+
23
34
  # registers the class where is was called as a service
24
35
  # @param [String] path the path of the service
25
36
  def self.register(path)
@@ -31,7 +42,7 @@ class MarilynRPC::Service
31
42
  # @api private
32
43
  # @return [Hash<String, Object>] all registered services with path as key and
33
44
  # the registered service as object
34
- def self.registry
45
+ def self.__registry__
35
46
  @@registry || {}
36
47
  end
37
48
 
@@ -39,21 +50,21 @@ class MarilynRPC::Service
39
50
  # defined in the class
40
51
  # @param [Array<Symbol>, Array<String>] callbacks the method names
41
52
  def self.after_connect(*callbacks)
42
- register_callbacks :after_connect, callbacks
53
+ __register_callbacks__ :after_connect, callbacks
43
54
  end
44
55
 
45
56
  # register one or more disconnect callbacks, a callback is simply a method
46
57
  # defined in the class
47
58
  # @param [Array<Symbol>, Array<String>] callbacks the method names
48
59
  def self.after_disconnect(*callbacks)
49
- register_callbacks :after_disconnect, callbacks
60
+ __register_callbacks__ :after_disconnect, callbacks
50
61
  end
51
62
 
52
63
  # registers a callbacks for the service class
53
64
  # @param [Symbol] name the name under which the callbacks should be saved
54
65
  # @param [Array<Symbol>, Array<String>] callbacks the method names
55
66
  # @api private
56
- def self.register_callbacks(name, callbacks)
67
+ def self.__register_callbacks__(name, callbacks)
57
68
  @_callbacks ||= {} # initialize callbacks
58
69
  @_callbacks[name] ||= [] # initialize specific set
59
70
  @_callbacks[name] += callbacks
@@ -64,16 +75,80 @@ class MarilynRPC::Service
64
75
  # @return [Array<String>, Array<Symbol>] an array of callback names, or an
65
76
  # empty array
66
77
  # @api private
67
- def self.registered_callbacks(name)
68
- (@_callbacks || {})[name] || []
78
+ def self.__registered_callbacks__(name)
79
+ @_callbacks ||= {}
80
+ @_callbacks[name] || []
81
+ end
82
+
83
+ # this generator marks the passed method names to require authentication.
84
+ # A Method that requires authentication is only callable if the client was
85
+ # successfully authenticated.
86
+ # @param [Array<String>, Array<Symbol>] methods the methods names
87
+ def self.authentication_required(*methods)
88
+ @_authenticated ||= {} # initalize hash of authenticated methods
89
+ methods.each { |m| @_authenticated[m.to_sym] = true }
90
+ end
91
+
92
+ # returns all methods of the service that require authentication
93
+ # @return [Array<Symbol>] methods that require authentication
94
+ # @api private
95
+ def self.__methods_with_authentication__
96
+ @_authenticated ||= {}
69
97
  end
70
98
 
71
99
  # calls the defined connect callbacks
72
100
  # @param [Symbol] the name of the callbacks to run
73
101
  # @api private
74
- def run_callbacks!(name)
75
- self.class.registered_callbacks(name).each do |callback|
76
- self.send(callback)
102
+ def __run_callbacks__(name)
103
+ self.class.__registered_callbacks__(name).each do |callback|
104
+ self.__send__(callback)
105
+ end
106
+ end
107
+
108
+ # returns the username if a user is authenticated
109
+ # @return [String, nil] the username or nil, if no user is authenticated
110
+ def session_username
111
+ @service_cache.username
112
+ end
113
+
114
+ # checks if a user is authenticated
115
+ # @return [Boolean] `true` if a user is authenticated, `false` otherwise
116
+ def session_authenticated?
117
+ !@service_cache.username.nil?
118
+ end
119
+
120
+ # the name for the service which will be used to do the authentication
121
+ AUTHENTICATION_PATH = :__marilyn_rpc_service_authentication
122
+
123
+ # define an authentication mechanism using a `lambda` or `Proc` object, or
124
+ # something else that respond to `call`. The authentication is available for
125
+ # all serivces of that connection.
126
+ # @param [Proc] &authenticator the authentication mechanism
127
+ # @yieldparam [String] username the username of the client
128
+ # @yieldparam [String] password the password of the client
129
+ # @yieldreturn [Boolean] To authenticate a user, the passed
130
+ # block must return `true` otherwise `false`
131
+ # @example Create a new authentication mechanism for clients using callable
132
+ # MarilynRPC::Service.authenticate_with do |username, password|
133
+ # username == "testuserid" && password == "secret"
134
+ # end
135
+ #
136
+ def self.authenticate_with(&authenticator)
137
+ Class.new(self) do # anonymous class
138
+ @@authenticator = authenticator
139
+ register(AUTHENTICATION_PATH)
140
+
141
+ # authenticate the user using a plain password method
142
+ # @param [String] username the username of the client
143
+ # @param [String] password the password of the client
144
+ def authenticate_plain(username, password)
145
+ if @@authenticator.call(username, password)
146
+ @service_cache.username = username
147
+ else
148
+ raise MarilynRPC::PermissionDeniedError.new \
149
+ "Wrong username or password!"
150
+ end
151
+ end
77
152
  end
78
153
  end
79
154
  end
@@ -1,33 +1,53 @@
1
- class MarilynRPC::ServiceCache
1
+ # # This class represents a per connection cache of the service instances.
2
+ # @attr [String, nil] username the username of a authenticated user oder `nil`
3
+ class MarilynRPC::ServiceCache
4
+ attr_accessor :username
5
+
2
6
  # creates the service cache
3
7
  def initialize
4
8
  @services = {}
5
9
  end
6
10
 
7
11
  # call a service in the service cache
8
- # @param [MarilynRPC::CallRequestMail] mail the mail request object, that
9
- # should be handled
12
+ # @param [MarilynRPC::Envelope] envelope the envelope that contains the
13
+ # request subject (mail), that should be handled
10
14
  # @return [MarilynRPC::CallResponseMail, MarilynRPC::Gentleman] either a
11
15
  # Gentleman if the response is async or an direct response.
12
- def call(mail)
13
- # check if the correct mail object was send
14
- unless mail.is_a?(MarilynRPC::CallRequestMail)
15
- raise ArgumentError.new("Expected CallRequestMail Object!")
16
- end
17
-
18
- # call the service instance using the argument of the mail
19
- # puts "call #{mail.method}@#{mail.path} with #{mail.args.inspect}"
20
- result = lookup(mail.path).send(mail.method, *mail.args)
21
- # puts "result => #{result.inspect}"
16
+ def call(envelope)
17
+ mail = MarilynRPC::MailFactory.unpack(envelope)
18
+ tag = mail.tag
22
19
 
23
- # no direct result, register callback
24
- if result.is_a? MarilynRPC::Gentleman
25
- result.tag = mail.tag # set the correct mail tag for the answer
26
- result
20
+ if mail.is_a?(MarilynRPC::CallRequestMail) # handle a call request
21
+ # fetch the service and check if the user has the permission to access the
22
+ # service
23
+ service = lookup(mail.path)
24
+ method = mail.method.to_sym
25
+ if service.class.__methods_with_authentication__[method] && !@username
26
+ raise MarilynRPC::PermissionDeniedError.new("No permission to access" \
27
+ " the #{service.class.name}##{method}")
28
+ end
29
+
30
+ # call the service instance using the argument of the mail
31
+ #puts "call #{mail.method}@#{mail.path} with #{mail.args.inspect}"
32
+ result = service.__send__(method, *mail.args)
33
+ #puts "result => #{result.inspect}"
34
+
35
+ # no direct result, register callback
36
+ if result.is_a? MarilynRPC::Gentleman
37
+ result.tag = tag # set the correct mail tag for the answer
38
+ result
39
+ else
40
+ MarilynRPC::CallResponseMail.new(tag, result) # direct response
41
+ end
27
42
  else
28
- # make response
29
- MarilynRPC::CallResponseMail.new(mail.tag, result)
43
+ raise MarilynRPC::BrokenEnvelopeError.new("Expected CallRequestMail Object!")
30
44
  end
45
+ rescue MarilynRPC::BrokenEnvelopeError => exception
46
+ MarilynRPC::ExceptionMail.new(nil, exception)
47
+ rescue => exception
48
+ #puts exception
49
+ #puts exception.backtrace.join("\n ")
50
+ MarilynRPC::ExceptionMail.new(tag, exception)
31
51
  end
32
52
 
33
53
  # get the service from the cache or the service registry
@@ -38,12 +58,13 @@ class MarilynRPC::ServiceCache
38
58
  if service = @services[path]
39
59
  return service
40
60
  # it's not in the cache, so try lookup in the service registry
41
- elsif service = MarilynRPC::Service.registry[path]
61
+ elsif service = MarilynRPC::Service.__registry__[path]
42
62
  @services[path] = service.new
43
- @services[path].run_callbacks! :after_connect
63
+ @services[path].service_cache = self
64
+ @services[path].__run_callbacks__(:after_connect)
44
65
  return @services[path]
45
66
  else
46
- raise ArgumentError.new("Service #{path} unknown!")
67
+ raise MarilynRPC::UnknownServiceError.new("Service #{path} unknown!")
47
68
  end
48
69
  end
49
70
 
@@ -52,7 +73,7 @@ class MarilynRPC::ServiceCache
52
73
  # @api private
53
74
  def disconnect!
54
75
  @services.each do |path, service|
55
- service.run_callbacks! :after_disconnect
76
+ service.__run_callbacks__(:after_disconnect)
56
77
  end
57
78
  end
58
79
  end