marilyn-rpc 0.0.2 → 0.0.3

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