drbservice 1.0.4

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.
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'drbservice'
4
+ require 'drbservice/passwordauth'
5
+
6
+ # An example service that just returns the version of Ruby running on the
7
+ # current machine.
8
+ class RubyVersionService < DRbService
9
+ include DRbService::PasswordAuthentication
10
+
11
+ service_password '6d4bf8ac6490219f4f8807dad066d742f39a2d25501ae66d650cb647cd758979'
12
+
13
+ ### Fetch the version of Ruby running this service as a vector of
14
+ ### three network-byte-order shorts.
15
+ def ruby_version
16
+ return RUBY_VERSION.split( /\./, 3 ).map( &:to_i ).pack( 'n*' )
17
+ end
18
+
19
+ end
20
+
21
+ RubyVersionService.start(
22
+ :ip => '127.0.0.1',
23
+ :port => 4848,
24
+ :certfile => 'service.pem',
25
+ :keyfile => 'service.pem' )
26
+
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'uri'
4
+
5
+ require 'drb'
6
+ require 'drb/ssl'
7
+
8
+ module DRb
9
+
10
+ # A DRb protocol implementation that provides an authenticated, encrypted channel
11
+ # for DRb services.
12
+ class DRbAuthenticatedSSLSocket < DRbSSLSocket
13
+
14
+ # The scheme of URIs which specify this protocol
15
+ SCHEME = 'drbauthssl'
16
+
17
+
18
+ ### Parse a drbauthssl:// URI. Accepts a String, a URI, or any object that responds to
19
+ ### #host, #port, and #query.
20
+ ### Return the values from the URI as an Array of the form: [ host, port, optionhash ].
21
+ ### Raises DRbBadScheme if the +uri+ is not a +drbauthssl+ URI.
22
+ ### Raises DRbBadURI if the +uri+ is missing the port number.
23
+ def self::parse_uri( uri )
24
+ uri = URI( uri ) unless uri.respond_to?( :host )
25
+ raise DRbBadScheme, "not a #{SCHEME} URI: %p" % [ uri ] unless
26
+ uri.scheme == SCHEME
27
+ raise DRbBadURI, "missing the port number" unless
28
+ uri.port && uri.port.to_i.nonzero?
29
+
30
+ return [ uri.host, uri.port.to_i, uri.query ]
31
+ end
32
+
33
+
34
+ ### Open a client connection to the server at +uri+, using configuration
35
+ ### +config+. Return a protocol instance for this connection.
36
+ def self::open( uri, config )
37
+ host, port, option = self.parse_uri( uri )
38
+ host.untaint
39
+ port.untaint
40
+ soc = TCPSocket.open( host, port )
41
+ ssl_conf = DRb::DRbSSLSocket::SSLConfig.new( config )
42
+ ssl_conf.setup_ssl_context
43
+ ssl = ssl_conf.connect( soc )
44
+ self.new( uri, ssl, ssl_conf, true )
45
+ end
46
+
47
+
48
+ end # class DRbAuthenticatedSSLSocket
49
+
50
+
51
+ DRbProtocol.add_protocol( DRbAuthenticatedSSLSocket )
52
+
53
+ end # module DRb
54
+
55
+
data/lib/drbservice.rb ADDED
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'drb'
4
+ require 'drb/ssl'
5
+
6
+
7
+ # A base class for DRb-based services. Concrete subclasses must define the service API by
8
+ # declaring public methods. By default, any public methods are hidden until the client
9
+ # authenticates. You can optionally declare a subset of its API that is # accessible before
10
+ # authentication by wrapping them in an 'unguarded' block. # See DRbService::unguarded for
11
+ # more details.
12
+ class DRbService
13
+ require 'drbservice/utils'
14
+ include DRbUndumped,
15
+ DRbService::Logging
16
+
17
+ # Library version
18
+ VERSION = '1.0.4'
19
+
20
+ # Version-control revision
21
+ REVISION = %$Revision: 59c8e5acd8bb $
22
+
23
+ # The default IP address to listen on
24
+ DEFAULT_IP = '127.0.0.1'
25
+
26
+ # The default port to listen on
27
+ DEFAULT_PORT = 4848
28
+
29
+ # The default path to the service cert, relative to the current directory
30
+ DEFAULT_CERTNAME = 'service-cert.pem'
31
+
32
+ # The default path to the service key, relative to the current directory
33
+ DEFAULT_KEYNAME = 'service-key.pem'
34
+
35
+ # The default values for the drbservice config hash
36
+ DEFAULT_CONFIG = {
37
+ :ip => DEFAULT_IP,
38
+ :port => DEFAULT_PORT,
39
+ :certfile => DEFAULT_CERTNAME,
40
+ :keyfile => DEFAULT_KEYNAME,
41
+ }
42
+
43
+ # The container for obscured methods
44
+ class << self
45
+ attr_reader :real_methods
46
+ end
47
+
48
+
49
+ #################################################################
50
+ ### C L A S S M E T H O D S
51
+ #################################################################
52
+
53
+ ### Start the DRbService, using the ip, port, and cert information from the given +config+
54
+ ### hash.
55
+ ###
56
+ ### [:ip] the ip to bind to
57
+ ### [:port] the port to listen on
58
+ ### [:certfile] the name of the server's SSL certificate file
59
+ ### [:keyfile] the name of the server's SSL key file
60
+ ###
61
+ def self::start( config={} )
62
+ config = DEFAULT_CONFIG.merge( config )
63
+
64
+ frontobj = self.new( config )
65
+ uri = "drbssl://%s:%d" % config.values_at( :ip, :port )
66
+
67
+ cert = OpenSSL::X509::Certificate.new( File.read(config[:certfile]) )
68
+ key = OpenSSL::PKey::RSA.new( File.read(config[:keyfile]) )
69
+
70
+ config = {
71
+ :safe_level => 1,
72
+ :verbose => true,
73
+ :SSLCertificate => cert,
74
+ :SSLPrivateKey => key,
75
+ }
76
+
77
+ DRbService.log.info "Starting %p as a DRbService at %s" % [ self, uri ]
78
+ server = DRb::DRbServer.new( uri, frontobj, config )
79
+ DRbService.log.debug " started. Joining the DRb thread."
80
+ $0 = "%s %s" % [ self.name, uri ]
81
+ server.thread.join
82
+ end
83
+
84
+
85
+ ### Method-addition callback: Obscure the method +meth+ unless unguarded mode is enabled.
86
+ def self::method_added( meth )
87
+ super
88
+
89
+ unless self == ::DRbService || meth.to_sym == :initialize
90
+ if !self.public_instance_methods.collect( &:to_sym ).include?( meth )
91
+ DRbService.log.debug "Not obsuring %p#%s: not a public method" % [ self, meth ]
92
+ elsif self.unguarded_mode
93
+ DRbService.log.debug "Not obscuring %p#%s: unguarded mode." % [ self, meth ]
94
+ else
95
+ DRbService.log.debug "Obscuring %p#%s." % [ self, meth ]
96
+ @real_methods ||= {}
97
+ @real_methods[ meth.to_sym ] = self.instance_method( meth )
98
+ remove_method( meth )
99
+ end
100
+ end
101
+ end
102
+
103
+
104
+ ### Inheritance callback: Add a per-class 'unguarded mode' flag to subclasses.
105
+ def self::inherited( subclass )
106
+ self.log.debug "Setting @unguarded_mode in %p" % [ subclass ]
107
+ subclass.instance_variable_set( :@unguarded_mode, false )
108
+ super
109
+ end
110
+
111
+
112
+ ### Declare some service methods that can be called without authentication in
113
+ ### the provided block.
114
+ def self::unguarded
115
+ self.unguarded_mode = true
116
+ yield
117
+ ensure
118
+ self.unguarded_mode = false
119
+ end
120
+
121
+
122
+ ### Return the library's version string
123
+ def self::version_string( include_buildnum=false )
124
+ vstring = "%s %s" % [ self.name, VERSION ]
125
+ vstring << " (build %s)" % [ REVISION[/.*: ([[:xdigit:]]+)/, 1] || '0' ] if include_buildnum
126
+ return vstring
127
+ end
128
+
129
+
130
+ ### Class accessors
131
+ class << self
132
+
133
+ # The unguarded mode flag -- instance methods defined while this flag is set
134
+ # will not be hidden
135
+ attr_accessor :unguarded_mode
136
+
137
+ end
138
+
139
+
140
+ #################################################################
141
+ ### I N S T A N C E M E T H O D S
142
+ #################################################################
143
+
144
+ ### Create a new instance of the service.
145
+ ### Raises a ScriptError if DRbService is instantiated directly.
146
+ def initialize( config={} )
147
+ raise ScriptError,
148
+ "can't instantiate #{self.class} directly: please subclass it instead" if
149
+ self.class == DRbService
150
+ @authenticated = false
151
+ end
152
+
153
+
154
+ ######
155
+ public
156
+ ######
157
+
158
+ ### Return a human-readable representation of the object.
159
+ def inspect
160
+ return "#<%s:0x%0x>" % [ self.class, self.__id__ * 2 ]
161
+ end
162
+
163
+
164
+ ### Returns +true+ if the client has successfully authenticated.
165
+ def authenticated?
166
+ return @authenticated ? true : false
167
+ end
168
+
169
+
170
+ ### Returns +true+ if the client has successfully authenticated and is authorized
171
+ ### to use the service. By default, authentication is sufficient for authorization;
172
+ ### to specify otherwise, override this method in your service's subclass or
173
+ ### include an auth mixin that provides one.
174
+ def authorized?
175
+ return self.authenticated?
176
+ end
177
+
178
+
179
+ ### Default authentication implementation -- always fails. You'll need to include
180
+ ### one of the authentication modules or provide your own #authenticate method in
181
+ ### your subclass.
182
+ def authenticate( *args )
183
+ self.log.error "authentication failure (fallback method)"
184
+ raise SecurityError, "authentication failure"
185
+ end
186
+
187
+
188
+ #########
189
+ protected
190
+ #########
191
+
192
+ ### Handle calls to guarded methods by requiring the authentication flag be
193
+ ### set if there is a password set.
194
+ def method_missing( sym, *args )
195
+ return super unless body = self.class.real_methods[ sym ]
196
+
197
+ if self.authorized?
198
+ return body.clone.bind( self ).call( *args )
199
+ else
200
+ self.log.error "Guarded method %p called without authentication!" % [ sym ]
201
+ raise SecurityError, "not authenticated"
202
+ end
203
+ end
204
+
205
+
206
+ end # class DRbService
207
+
208
+
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env ruby
2
+ # coding: utf-8
3
+
4
+ require 'treequel'
5
+ require 'drbservice'
6
+
7
+ # An authentication strategy for DRbService -- set a password via a
8
+ # class method.
9
+ module DRbService::LDAPAuthentication
10
+
11
+ ### Methods added to including classes when LDAPAuthentication is
12
+ ### mixed in.
13
+ module ClassMethods
14
+
15
+ # The default attributes of a search
16
+ DEFAULT_SEARCH = {
17
+ :filter => 'uid=%s',
18
+ :base => nil,
19
+ :scope => :sub,
20
+ }.freeze
21
+
22
+
23
+ ### Extension callback -- add the necessary class instance variables
24
+ ### to extended modules.
25
+ def self::extended( mod )
26
+ super
27
+ mod.instance_variable_set( :@ldap_uri, nil )
28
+ mod.instance_variable_set( :@ldap_dn, nil )
29
+ mod.instance_variable_set( :@ldap_dn_search, DEFAULT_SEARCH.dup )
30
+ mod.instance_variable_set( :@ldap_authz_callback, nil )
31
+ end
32
+
33
+
34
+ ### Set the URI of the LDAP server to bind to for authentication
35
+ def ldap_uri( uri=nil )
36
+ @ldap_uri = uri if uri
37
+ return @ldap_uri
38
+ end
39
+
40
+
41
+ ### Set the pattern to use when creating the DN to use when binding.
42
+ def ldap_dn( pattern=nil )
43
+ @ldap_dn = pattern if pattern
44
+ return @ldap_dn
45
+ end
46
+
47
+
48
+ ### Set a filter that is used when searching for an account to bind
49
+ ### as.
50
+ def ldap_dn_search( filter=nil, options={} )
51
+ if filter
52
+ @ldap_dn_search ||= {}
53
+ @ldap_dn_search[:filter] = filter
54
+ @ldap_dn_search[:base] = options[:base] if options[:base]
55
+ @ldap_dn_search[:scope] = options[:scope] if options[:scope]
56
+ end
57
+
58
+ return @ldap_dn_search
59
+ end
60
+
61
+
62
+ ### Register a function to call when the user successfully binds to the
63
+ ### directory to check for authorization. It will be called with the
64
+ ### Treequel::Branch of the bound user and the Treequel::Directory they
65
+ ### are bound to. Returning +true+ from this function will cause
66
+ ### authorization to succeed, while returning a false value causes it to
67
+ ### fail.
68
+ def ldap_authz_callback( callable=nil, &block )
69
+ if callable
70
+ @ldap_authz_callback = callable
71
+ elsif block
72
+ @ldap_authz_callback = block
73
+ end
74
+
75
+ return @ldap_authz_callback
76
+ end
77
+
78
+ end # module ClassMethods
79
+
80
+
81
+ ### Overridden mixin callback -- add the ClassMethods to the including class
82
+ def self::included( klass )
83
+ super
84
+ klass.extend( ClassMethods )
85
+ end
86
+
87
+
88
+ ### Set up some instance variables used by the mixin.
89
+ def initialize( *args )
90
+ super
91
+ @authenticated = false
92
+ @authuser = nil
93
+ @authuser_branch = nil
94
+ end
95
+
96
+
97
+ # the username of the authenticated user
98
+ attr_reader :authuser
99
+
100
+ # the Treequel::Branch of the authenticated user
101
+ attr_reader :authuser_branch
102
+
103
+
104
+ ### Authenticate using the specified +password+, calling the provided block if
105
+ ### authentication succeeds. Raises a SecurityError if authentication fails. If
106
+ ### no password is set, the block is called regardless of what the +password+ is.
107
+ def authenticate( user, password )
108
+ uri = self.class.ldap_uri
109
+ self.log.debug "Connecting to %p for authentication" % [ uri ]
110
+ directory = Treequel.directory( uri )
111
+ self.log.debug " finding LDAP record for: %p" % [ user ]
112
+ user_branch = self.find_auth_user( directory, user ) or
113
+ return super
114
+
115
+ self.log.debug " binding as %p (%p)" % [ user, user_branch ]
116
+ directory.bind_as( user_branch, password )
117
+ self.log.debug " bound successfully..."
118
+
119
+ @authenticated = true
120
+
121
+ if cb = self.class.ldap_authz_callback
122
+ self.log.debug " calling authorization callback..."
123
+
124
+ unless self.call_authz_callback( cb, user_branch, directory )
125
+ msg = " authorization failed for: %s" % [ user_branch ]
126
+ self.log.debug( msg )
127
+ raise SecurityError, msg
128
+ end
129
+
130
+ self.log.debug " authorization succeeded."
131
+ end
132
+
133
+ @authuser = user
134
+ @authuser_branch = user
135
+ yield
136
+
137
+ rescue LDAP::ResultError => err
138
+ self.log.error " authentication failed for %p" % [ user_branch || user ]
139
+ raise SecurityError, "authentication failure"
140
+
141
+ ensure
142
+ @authuser = nil
143
+ @authuser_branch = nil
144
+ @authenticated = false
145
+ end
146
+
147
+
148
+ #########
149
+ protected
150
+ #########
151
+
152
+ ### Find the specified +username+ entry in the given +directory+, which should be a
153
+ ### Treequel::Directory. Returns the Treequel::Branch for the first found user, if one
154
+ ### was found, or +nil+ if no such user was found.
155
+ def find_auth_user( directory, username )
156
+ self.log.debug "Finding the user to bind as."
157
+
158
+ if dnpattern = self.class.ldap_dn
159
+ self.log.debug " using DN pattern %p" % [ dnpattern ]
160
+ dn = dnpattern % [ username ]
161
+ user = Treequel::Branch.new( directory, dn )
162
+ return user.exists? ? user : nil
163
+
164
+ else
165
+ dnsearch = self.class.ldap_dn_search
166
+ usersearch = dnsearch[:base] ?
167
+ Treequel::Branch.new( directory, dnsearch[:base] ) :
168
+ directory.base
169
+ usersearch = usersearch.scope( dnsearch[:scope] ) if dnsearch[:scope]
170
+ usersearch = usersearch.filter( dnsearch[:filter] % [username] )
171
+
172
+ self.log.debug " using filter: %s" % [ usersearch ]
173
+ if user = usersearch.first
174
+ self.log.debug " search found: %s" % [ user ]
175
+ return user
176
+ else
177
+ self.log.error " search returned no entries" % [ usersearch ]
178
+ return nil
179
+ end
180
+ end
181
+
182
+ end
183
+
184
+
185
+ ### Call the authorization callback with the given +user+ and +directory+ and
186
+ ### return true if it indicates authorization was successful.
187
+ def call_authz_callback( callback, user, directory )
188
+
189
+ if callback.respond_to?( :call )
190
+ return true if callback.call( user, directory )
191
+
192
+ else callback = self.method( callback )
193
+ return true if callback.call( user, directory )
194
+ end
195
+
196
+ end
197
+
198
+ end # DRbService::LDAPAuthentication
199
+
200
+