drbservice 1.0.4

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