Skip to content

Commit

Permalink
Land #256, Add support for callback hooks in share providers
Browse files Browse the repository at this point in the history
Merge branch 'land-0256' into upstream-master
  • Loading branch information
bwatters-r7 committed Nov 13, 2023
2 parents 83ace4b + 59f6144 commit 7fc546a
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 9 deletions.
4 changes: 2 additions & 2 deletions lib/ruby_smb/server/server_client/share_io.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def proxy_share_io_smb1(request, session)
end

logger.debug("Received #{SMB1::Commands.name(request.smb_header.command)} request for share: #{share_processor.provider.name}")
share_processor.send(__callee__, request)
share_processor.share_io(__callee__, request)
end

alias :do_close_smb1 :proxy_share_io_smb1
Expand All @@ -29,7 +29,7 @@ def proxy_share_io_smb2(request, session)
end

logger.debug("Received #{SMB2::Commands.name(request.smb2_header.command)} request for share: #{share_processor.provider.name}")
share_processor.send(__callee__, request)
share_processor.share_io(__callee__, request)
end

alias :do_close_smb2 :proxy_share_io_smb2
Expand Down
49 changes: 48 additions & 1 deletion lib/ruby_smb/server/share/provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,51 @@ module Provider
# type and name. It is shared across all client connections and
# sessions.
class Base
Hook = Struct.new(:request_class, :location, :callback)

# @param [String] name The name of this share.
def initialize(name)
def initialize(name, hooks: nil)
@name = name
@hooks = hooks || []
end

# Add a hook to be called when the specified request class is processed. Any hook that was previously
# installed for the request class and location will be removed. A hook installed with a location of :before
# will be called with the session and request as the only two arguments. The return value, if provided, will
# replace the request that is to be processed. A hook installed with a location of :after will be called with
# the session, request and response as the only three arguments. The return value, if provided, will replace
# the response that is to be sent to the client.
#
# @param [RubySMB::GenericPacket] request_class The request class to register the hook for.
# @param [Proc] callback The routine to be executed when the request class is being processed.
# @param [Symbol] location When the callback should be invoked. Must be either :before or :after.
def add_hook(request_class, callback: nil, location: :before, &block)
unless %i[ before after ].include?(location)
raise ArgumentError, 'the location argument must be :before or :after'
end

unless callback.nil? ^ block.nil?
raise ArgumentError, 'either a callback or a block must be specified'
end

# Remove any hooks that were previously installed, this enforces that only one hook can be present at a time
# for any particular request class and location combination.
remove_hook(request_class, location: location)
@hooks << Hook.new(request_class, location, callback || block)

nil
end

# Remove a hook for the specified request class.
#
# @param [RubySMB::GenericPacket] request_class The request class to register the hook for.
# @param [Symbol] location When the callback should be invoked.
def remove_hook(request_class, location: :before)
@hooks.filter! do |hook|
hook.request_class == request_class && hook.location == location
end

nil
end

# Create a new, session-specific processor instance for this share.
Expand All @@ -28,6 +70,11 @@ def type
# @!attribute [r] name
# @return [String]
attr_accessor :name

# The hooks installed for this share.
# @!attribute [r] hooks
# @return [Array]
attr_accessor :hooks
end
end
end
Expand Down
4 changes: 2 additions & 2 deletions lib/ruby_smb/server/share/provider/disk.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ class Disk < Base
# @param [String] name The name of this share.
# @param [String, Pathname] path The local file system path to share. This path must be an absolute path to an existing
# directory.
def initialize(name, path)
def initialize(name, path, hooks: nil)
path = Pathname.new(File.expand_path(path)) if path.is_a?(String)
raise ArgumentError.new('path must be a directory') unless path.directory? # it needs to exist
raise ArgumentError.new('path must be absolute') unless path.absolute? # it needs to be absolute so it is independent of the cwd

@path = path
super(name)
super(name, hooks: hooks)
end

attr_accessor :path
Expand Down
25 changes: 25 additions & 0 deletions lib/ruby_smb/server/share/provider/processor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,31 @@ def server
@server_client.server
end

# Forward a share IO method for a particular request. This is a choke point to allow any hooks that were
# registered with the share provider to be executed before and after the specified method is invoked to
# process the request and generate the response. This is used for both SMB1 and SMB2 requests.
#
# @param [Symbol] method_name The method name to forward the request to
# @param [RubySMB::GenericPacket] request The request packet to be processed
# @return [RubySMB::GenericPacket]
def share_io(method_name, request)
@provider.hooks.each do |hook|
next unless hook.request_class == request.class && hook.location == :before

request = hook.callback.call(@session, request) || request
end

response = send(method_name, request)

@provider.hooks.each do |hook|
next unless hook.request_class == request.class && hook.location == :after

response = hook.callback.call(@session, request, response) || response
end

response
end

# The underlying share provider that this is a processor for.
# @!attribute [r] provider
# @return [RubySMB::Server::Share::Provider::Base]
Expand Down
6 changes: 3 additions & 3 deletions lib/ruby_smb/server/share/provider/virtual_disk.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ class VirtualDisk < Disk
private_constant :HASH_METHODS

# @param [String] name The name of this share.
def initialize(name)
def initialize(name, hooks: nil)
@vfs = {}
super(name, add(VirtualPathname.new(self, File::SEPARATOR)))
super(name, add(VirtualPathname.new(self, File::SEPARATOR)), hooks: hooks)
end

# Add a dynamic file to the virtual file system. A dynamic file is one whose contents are generated by the
Expand Down Expand Up @@ -86,7 +86,7 @@ def add(virtual_pathname)
end

def new_processor(server_client, session)
scoped_virtual_disk = self.class.new(@name)
scoped_virtual_disk = self.class.new(@name, hooks: @hooks)
@vfs.each_value do |path|
path = path.dup
path.virtual_disk = scoped_virtual_disk if path.is_a?(VirtualPathname)
Expand Down
2 changes: 1 addition & 1 deletion lib/ruby_smb/smb1/packet/nt_create_andx_request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class ParameterBlock < RubySMB::SMB1::ParameterBlock
end

uint64 :allocation_size, label: 'Allocation Size'
smb_ext_file_attributes :ext_file_attributes, label: 'Extented File Attributes'
smb_ext_file_attributes :ext_file_attributes, label: 'Extended File Attributes'
share_access :share_access, label: 'Share Access'
# The following constants are defined in RubySMB::Dispositions
uint32 :create_disposition, label: 'Create Disposition'
Expand Down

0 comments on commit 7fc546a

Please sign in to comment.