From 2ce7d0b8ee8bfff253fe92dcd2e854daeee0dc88 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 8 Nov 2023 12:10:11 -0500 Subject: [PATCH 1/2] Fix a typo in the label --- lib/ruby_smb/smb1/packet/nt_create_andx_request.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ruby_smb/smb1/packet/nt_create_andx_request.rb b/lib/ruby_smb/smb1/packet/nt_create_andx_request.rb index d30e797b1..6f0bb4cc4 100644 --- a/lib/ruby_smb/smb1/packet/nt_create_andx_request.rb +++ b/lib/ruby_smb/smb1/packet/nt_create_andx_request.rb @@ -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' From 59f6144955abf3d3f026096a4e2b13a9c4e86a33 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 8 Nov 2023 13:01:07 -0500 Subject: [PATCH 2/2] Add hooks for share providers --- lib/ruby_smb/server/server_client/share_io.rb | 4 +- lib/ruby_smb/server/share/provider.rb | 49 ++++++++++++++++++- lib/ruby_smb/server/share/provider/disk.rb | 4 +- .../server/share/provider/processor.rb | 25 ++++++++++ .../server/share/provider/virtual_disk.rb | 6 +-- 5 files changed, 80 insertions(+), 8 deletions(-) diff --git a/lib/ruby_smb/server/server_client/share_io.rb b/lib/ruby_smb/server/server_client/share_io.rb index c312b5246..840e32b28 100644 --- a/lib/ruby_smb/server/server_client/share_io.rb +++ b/lib/ruby_smb/server/server_client/share_io.rb @@ -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 @@ -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 diff --git a/lib/ruby_smb/server/share/provider.rb b/lib/ruby_smb/server/share/provider.rb index 180231cb2..59f3c0b46 100644 --- a/lib/ruby_smb/server/share/provider.rb +++ b/lib/ruby_smb/server/share/provider.rb @@ -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. @@ -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 diff --git a/lib/ruby_smb/server/share/provider/disk.rb b/lib/ruby_smb/server/share/provider/disk.rb index efbaf70f6..760303a89 100644 --- a/lib/ruby_smb/server/share/provider/disk.rb +++ b/lib/ruby_smb/server/share/provider/disk.rb @@ -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 diff --git a/lib/ruby_smb/server/share/provider/processor.rb b/lib/ruby_smb/server/share/provider/processor.rb index 8d63c65d2..d8c0d3f26 100644 --- a/lib/ruby_smb/server/share/provider/processor.rb +++ b/lib/ruby_smb/server/share/provider/processor.rb @@ -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] diff --git a/lib/ruby_smb/server/share/provider/virtual_disk.rb b/lib/ruby_smb/server/share/provider/virtual_disk.rb index 5ae9e70da..478663ee0 100644 --- a/lib/ruby_smb/server/share/provider/virtual_disk.rb +++ b/lib/ruby_smb/server/share/provider/virtual_disk.rb @@ -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 @@ -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)