diff --git a/Gemfile b/Gemfile index df419cb2..d42e0bd7 100644 --- a/Gemfile +++ b/Gemfile @@ -13,4 +13,6 @@ group :development, :test do gem "pry-byebug", platform: :mri, require: false gem 'rubocop' gem 'yard' + gem 'hrr_rb_ssh', git: "https://github.com/adfoster-r7/hrr_rb_ssh.git", branch: "investigate-openssl3-support" + gem 'hrr_rb_sftp' end diff --git a/Gemfile.lock b/Gemfile.lock index 4a70de83..d42959e6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,10 @@ +GIT + remote: https://github.com/adfoster-r7/hrr_rb_ssh.git + revision: c804e69c4e364ce7fc2092a3e654c4d920c8c54f + branch: investigate-openssl3-support + specs: + hrr_rb_ssh (0.4.2) + PATH remote: . specs: @@ -5,12 +12,14 @@ PATH addressable (>= 2.8.0) bunny (>= 2.14) deep_merge (~> 1.2.0) - dry-configurable (~> 0.12) + dry-configurable (~> 0.12.0) + dry-container (~> 0.8.0) + dry-core (< 0.8.0) dry-events (~> 0.3) - dry-inflector (~> 0.2) - dry-initializer (~> 3.0) + dry-inflector (< 0.3.0) + dry-initializer (~> 3.0.0) dry-monads (~> 1.3) - dry-schema (~> 1.6) + dry-schema (~> 1.6.0) dry-struct (~> 1.4) dry-types (~> 1.5) dry-validation (~> 1.6) @@ -18,9 +27,11 @@ PATH faraday_middleware (~> 1.0) logging (~> 2.3.0) mime-types + net-sftp nokogiri (>= 1.13.0) - oj (~> 3.11) - ox (~> 2.14) + oj (< 3.14) + ox (< 2.14.15) + set (< 1.0.4) typhoeus (~> 1.4.0) GEM @@ -91,7 +102,7 @@ GEM ast (2.4.2) bson (4.12.1) builder (3.2.4) - bunny (2.19.0) + bunny (2.22.0) amq-protocol (~> 2.3, >= 2.3.1) sorted_set (~> 1, >= 1.0.2) byebug (11.1.3) @@ -106,7 +117,7 @@ GEM activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - deep_merge (1.2.1) + deep_merge (1.2.2) diff-lcs (1.4.4) dry-configurable (0.12.1) concurrent-ruby (~> 1.0) @@ -153,7 +164,7 @@ GEM dry-initializer (~> 3.0) dry-schema (~> 1.5, >= 1.5.2) erubi (1.10.0) - ethon (0.15.0) + ethon (0.16.0) ffi (>= 1.15.0) faker (2.18.0) i18n (>= 1.6, < 2) @@ -172,15 +183,16 @@ GEM faraday-net_http_persistent (1.2.0) faraday_middleware (1.2.0) faraday (~> 1.0) - ffi (1.15.4) + ffi (1.16.3) globalid (0.5.2) activesupport (>= 5.0) hashdiff (1.0.1) + hrr_rb_sftp (0.2.0) i18n (1.8.10) concurrent-ruby (~> 1.0) ice_nine (0.11.2) little-plugger (1.1.4) - logging (2.3.0) + logging (2.3.1) little-plugger (~> 1.1) multi_json (~> 1.14) loofah (2.12.0) @@ -190,11 +202,11 @@ GEM mini_mime (>= 0.1.1) marcel (1.0.2) method_source (1.0.0) - mime-types (3.3.1) + mime-types (3.5.2) mime-types-data (~> 3.2015) - mime-types-data (3.2021.0901) + mime-types-data (3.2024.0206) mini_mime (1.1.2) - mini_portile2 (2.8.1) + mini_portile2 (2.8.5) minitest (5.14.4) mongo (2.14.0) bson (>= 4.8.2, < 5.0.0) @@ -202,12 +214,15 @@ GEM activemodel (>= 5.1, < 6.2) mongo (>= 2.10.5, < 3.0.0) multi_json (1.15.0) - multipart-post (2.1.1) + multipart-post (2.4.0) mustermann (1.1.1) ruby2_keywords (~> 0.0.1) + net-sftp (4.0.0) + net-ssh (>= 5.0.0, < 8.0.0) + net-ssh (7.2.1) nio4r (2.5.8) - oj (3.13.9) - ox (2.14.5) + oj (3.13.23) + ox (2.14.14) parallel (1.20.1) parser (3.0.1.1) ast (~> 2.4.1) @@ -218,7 +233,7 @@ GEM byebug (~> 11.0) pry (~> 0.10) public_suffix (4.0.6) - racc (1.6.2) + racc (1.7.3) rack (2.2.3) rack-protection (2.1.0) rack @@ -285,7 +300,7 @@ GEM parser (>= 3.0.1.1) ruby-progressbar (1.11.0) ruby2_keywords (0.0.4) - set (1.0.2) + set (1.0.3) sinatra (2.1.0) mustermann (~> 1.0) rack (~> 2.2) @@ -303,7 +318,7 @@ GEM sprockets (>= 3.0.0) thor (1.1.0) tilt (2.0.10) - typhoeus (1.4.0) + typhoeus (1.4.1) ethon (>= 0.9.0) tzinfo (2.0.4) concurrent-ruby (~> 1.0) @@ -325,6 +340,8 @@ DEPENDENCIES database_cleaner event_source! faker + hrr_rb_sftp + hrr_rb_ssh! mongoid pry pry-byebug diff --git a/event_source.gemspec b/event_source.gemspec index f5bdc6a5..94a60975 100644 --- a/event_source.gemspec +++ b/event_source.gemspec @@ -4,6 +4,7 @@ lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'event_source/version' +require 'event_source/ruby_versions' Gem::Specification.new do |spec| spec.name = 'event_source' @@ -38,23 +39,31 @@ Gem::Specification.new do |spec| spec.add_dependency 'addressable', '>= 2.8.0' spec.add_dependency 'bunny', '>= 2.14' spec.add_dependency 'deep_merge', '~> 1.2.0' - spec.add_dependency 'dry-configurable', '~> 0.12' + spec.add_dependency 'dry-core', '< 0.8.0' + spec.add_dependency 'dry-configurable', '~> 0.12.0' + spec.add_dependency 'dry-container', '~> 0.8.0' spec.add_dependency 'dry-events', '~> 0.3' - spec.add_dependency 'dry-inflector', '~> 0.2' - spec.add_dependency 'dry-initializer', '~> 3.0' + spec.add_dependency 'dry-inflector', '< 0.3.0' + spec.add_dependency 'dry-initializer', '~> 3.0.0' spec.add_dependency 'dry-monads', '~> 1.3' spec.add_dependency 'dry-struct', '~> 1.4' spec.add_dependency 'dry-types', '~> 1.5' spec.add_dependency 'dry-validation', '~> 1.6' - spec.add_dependency 'dry-schema', '~> 1.6' + spec.add_dependency 'dry-schema', '~> 1.6.0' spec.add_dependency 'faraday', '~> 1.4.1' spec.add_dependency 'faraday_middleware', '~> 1.0' spec.add_dependency 'logging', '~> 2.3.0' - spec.add_dependency 'nokogiri', '>= 1.13.0' + if EventSource::RubyVersions::GREATER_THAN_TWO_SIX + spec.add_dependency 'nokogiri', '>= 1.13.0' + else + spec.add_dependency 'nokogiri', '< 1.14.0' + end + spec.add_dependency 'set', '< 1.0.4' spec.add_dependency 'mime-types' - spec.add_dependency 'oj', '~> 3.11' - spec.add_dependency 'ox', '~> 2.14' + spec.add_dependency 'oj', '< 3.14' + spec.add_dependency 'ox', '< 2.14.15' spec.add_dependency 'typhoeus', '~> 1.4.0' + spec.add_dependency 'net-sftp' # TODO: Change to development dependency spec.add_development_dependency 'database_cleaner' diff --git a/lib/event_source/async_api/contracts/publish_operation_bindings_contract.rb b/lib/event_source/async_api/contracts/publish_operation_bindings_contract.rb index 878fd34c..6ae21c46 100644 --- a/lib/event_source/async_api/contracts/publish_operation_bindings_contract.rb +++ b/lib/event_source/async_api/contracts/publish_operation_bindings_contract.rb @@ -11,6 +11,7 @@ class PublishOperationBindingsContract < Contract params do optional(:http).hash optional(:amqp).hash + optional(:sftp).hash end rule(:http) do diff --git a/lib/event_source/async_api/publish_bindings.rb b/lib/event_source/async_api/publish_bindings.rb index 3ae45f3d..5faa040a 100644 --- a/lib/event_source/async_api/publish_bindings.rb +++ b/lib/event_source/async_api/publish_bindings.rb @@ -12,6 +12,7 @@ class PublishBindings < Dry::Struct transform_keys(&:to_sym) attribute :http, ::EventSource::Protocols::Http::PublishBindings.meta(omittable: true) attribute :amqp, Types::Hash.meta(omittable: true) + attribute :sftp, Types::Hash.meta(omittable: true) attribute :x_amqp_exchange_to_exchanges, Types::Hash.meta(omittable: true) end end diff --git a/lib/event_source/configure/contracts.rb b/lib/event_source/configure/contracts.rb index 73c8bb94..5b1891c3 100644 --- a/lib/event_source/configure/contracts.rb +++ b/lib/event_source/configure/contracts.rb @@ -3,6 +3,7 @@ require_relative "contracts/client_certificate_settings_contract" require_relative "contracts/soap_settings_contract" require_relative "contracts/http_configuration_contract" +require_relative "contracts/sftp_configuration_contract" module EventSource module Configure diff --git a/lib/event_source/configure/contracts/sftp_configuration_contract.rb b/lib/event_source/configure/contracts/sftp_configuration_contract.rb new file mode 100644 index 00000000..96100595 --- /dev/null +++ b/lib/event_source/configure/contracts/sftp_configuration_contract.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module EventSource + module Configure + module Contracts + # Contract for Sftp configuration. + class SftpConfigurationContract < Dry::Validation::Contract + params do + optional(:ref).value(:string) + optional(:url).value(:string) + optional(:host).value(:string) + optional(:port).value(::EventSource::Configure::Types::SftpPortSettingType) + optional(:path).value(:string) + required(:user_name).value(:string) + optional(:password).value(:string) + optional(:private_key).value(::EventSource::Configure::Types::SftpPrivateKeySettingType) + required(:call_location).array(:string) + end + + rule(:url, :host) do + key.failure("either :url or :host must be specified") if values[:url].blank? && values[:host].blank? + end + end + end + end +end diff --git a/lib/event_source/configure/operations/validate_server_configurations.rb b/lib/event_source/configure/operations/validate_server_configurations.rb index 4dd53332..ff8f8796 100644 --- a/lib/event_source/configure/operations/validate_server_configurations.rb +++ b/lib/event_source/configure/operations/validate_server_configurations.rb @@ -35,6 +35,10 @@ def validate_configuration(sc) result = ::EventSource::Configure::Contracts::HttpConfigurationContract.new.call(params_as_hash) return Success(sc) if result.success? Failure([sc, result.errors]) + when ::EventSource::Configure::SftpConfiguration + result = ::EventSource::Configure::Contracts::SftpConfigurationContract.new.call(params_as_hash) + return Success(sc) if result.success? + Failure([sc, result.errors]) else Success(sc) end diff --git a/lib/event_source/configure/servers.rb b/lib/event_source/configure/servers.rb index 0f14a865..eb2a5cd7 100644 --- a/lib/event_source/configure/servers.rb +++ b/lib/event_source/configure/servers.rb @@ -63,6 +63,24 @@ def to_h end end + SftpConfiguration = Struct.new( + :protocol, + :ref, + :host, + :port, + :url, + :user_name, + :password, + :path, + :private_key, + :call_location + ) do + def to_h + attribute_hash = super() + attribute_hash.compact + end + end + # Represents a server configuration. class Servers attr_reader :default_content_type, :configurations @@ -84,6 +102,13 @@ def amqp yield(amqp_conf) @configurations.push(amqp_conf) end + + def sftp + sftp_conf = SftpConfiguration.new(:sftp) + sftp_conf.call_location = caller(1) + yield(sftp_conf) + @configurations.push(sftp_conf) + end end end end \ No newline at end of file diff --git a/lib/event_source/configure/types.rb b/lib/event_source/configure/types.rb index 55e809f5..0326e2a6 100644 --- a/lib/event_source/configure/types.rb +++ b/lib/event_source/configure/types.rb @@ -6,6 +6,9 @@ module Configure module Types send(:include, Dry.Types) + SftpPortSettingType = Types::Params::Integer.default(22) + SftpPrivateKeySettingType = Types::String + SoapSecurityTimestampUseSettingType = Types::Bool.default(false) SoapPasswordDigestSettingType = Types::Symbol.default(:digest).enum(:digest, :plain) SoapTimestampTtlSettingType = Types::Params::Integer.default(60) diff --git a/lib/event_source/connection_manager.rb b/lib/event_source/connection_manager.rb index 58df84ab..f3f5f31f 100644 --- a/lib/event_source/connection_manager.rb +++ b/lib/event_source/connection_manager.rb @@ -41,7 +41,6 @@ def add_connection(async_api_server) # an {EventSource::AsyncApi::Server} configuration # @return [EventSource::Connection] Connection def fetch_connection(async_api_server) - # raise connections.keys.inspect client_klass = protocol_klass_for(async_api_server.protocol) connection_uri = client_klass.connection_uri_for(async_api_server) connections[connection_uri] @@ -171,6 +170,8 @@ def protocol_klass_for(protocol) EventSource::Protocols::Amqp::BunnyConnectionProxy when :http, :https, 'http', 'https' EventSource::Protocols::Http::FaradayConnectionProxy + when :sftp, 'sftp' + EventSource::Protocols::Sftp::SftpConnectionProxy else raise EventSource::Protocols::Amqp::Error::UnknownConnectionProtocolError, "unknown protocol: #{protocol}" diff --git a/lib/event_source/protocols/sftp/sftp_channel_proxy.rb b/lib/event_source/protocols/sftp/sftp_channel_proxy.rb new file mode 100644 index 00000000..97ec23e3 --- /dev/null +++ b/lib/event_source/protocols/sftp/sftp_channel_proxy.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module EventSource + module Protocols + module Sftp + class SftpChannelProxy + include EventSource::Logging + + # @param sftp_connection_proxy [EventSource::Protocols::Sftp::SftpConnectionProxy] The Connection proxy instance + # @param channel_item_key [EventSource::AsyncApi::ChannelItem] unique name for the channel + # @param async_api_channel_item [EventSource::AsyncApi::ChannelItem] configuration settings for the Channel + # @return [SftpChannelProxy] Channel proxy instance + def initialize( + sftp_connection_proxy, + channel_item_key, + async_api_channel_item + ) + @sftp_connection_proxy = sftp_connection_proxy + @channel_item_key = channel_item_key + @async_api_channel_item = async_api_channel_item + end + + # Create and register an operation to broadcast messages + # @param async_api_channel_item [Hash] configuration values in the form of + # an {EventSource::AsyncApi::ChannelItem} + # @return [SftpPublishProxy] + def add_publish_operation(async_api_channel_item) + SftpPublishProxy.new(self, async_api_channel_item) + end + + def execute(&blk) + @sftp_connection_proxy.execute(&blk) + end + end + end + end +end \ No newline at end of file diff --git a/lib/event_source/protocols/sftp/sftp_connection_proxy.rb b/lib/event_source/protocols/sftp/sftp_connection_proxy.rb new file mode 100644 index 00000000..e0d9f706 --- /dev/null +++ b/lib/event_source/protocols/sftp/sftp_connection_proxy.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +require "net/sftp" + +module EventSource + module Protocols + module Sftp + class SftpConnectionProxy + include EventSource::Logging + + attr_reader :connection_uri + + # @param [EventSource::AsyncApi::Server] server + def initialize(server) + @server = server + @connection_uri = self.class.connection_uri_for(server) + @settings = parse_server_settings + end + + def protocol + :sftp + end + + # Return the connection uri, based on server settings, under which an + # SFTP connection will be registered in event_source. + # @param async_api_server [EventSource::AsyncApi::Server] + def self.connection_uri_for(async_api_server) + params = parse_url(async_api_server) + scheme = 'sftp' + + host = params[:host] + path = params[:path] + if path == '/' || path.blank? + "#{scheme}://#{host}/" + else + "#{scheme}://#{host}/#{path}" + end + end + + def self.parse_url(server) + url = server[:url] + if URI(url) + sftp_url = URI.parse(url) + host = sftp_url.host + path = sftp_url.path.blank? ? sftp_url.path : "/" + else + host = url || ConnectDefaults[:host] + path = "/" + end + { host: host, path: path } + end + + # @param [String] channel_item_key a unique name for the channel + # @param [Hash] async_api_channel_item configuration values for the new channel + # @return [SftpChannelProxy] + def add_channel(channel_item_key, async_api_channel_item) + SftpChannelProxy.new(self, channel_item_key, async_api_channel_item) + end + + def active? + true + end + + def close + end + + def start + end + + def execute + logger.info "Connecting to SFTP: #{@settings[:connection_parameters].first}" + Net::SFTP.start(*@settings[:connection_parameters]) do |sftp| + logger.info " Uploading to SFTP directory: #{@settings[:path]}" + yield sftp, @settings[:path] + end + end + + protected + + def parse_server_settings + port = nil + host = if URI(@server[:url]) + uri = URI(@server[:url]) + port = uri.port if uri.port.present? + host = uri.host + else + @server[:host] + end + host ||= @server[:port] + port = @server[:port] if @server[:port] + user_name = @server[:user_name] + credentials = Hash.new + credentials[:password] = @server[:password] if @server[:password] + credentials[:key_data] = @server[:private_key] if @server[:private_key] + auth_methods = [] + auth_methods << "password" if credentials[:password] + auth_methods << "publickey" if credentials[:key_data] + { + :connection_parameters => [ + host, + user_name, + { + :port => port, + :config => false, + :use_agent => false, + :auth_methods => auth_methods + }.merge(credentials) + ], + :path => @server[:path] + } + end + end + end + end +end \ No newline at end of file diff --git a/lib/event_source/protocols/sftp/sftp_publish_proxy.rb b/lib/event_source/protocols/sftp/sftp_publish_proxy.rb new file mode 100644 index 00000000..ebe1aaef --- /dev/null +++ b/lib/event_source/protocols/sftp/sftp_publish_proxy.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module EventSource + module Protocols + module Sftp + class SftpPublishProxy + include EventSource::Logging + + attr_reader :channel_proxy + + # @param [EventSource::AsyncApi::Channel] channel_proxy instance on which to open this Exchange + # @param [Hash] async_api_channel_item configuration values in the form of + # an {EventSource::AsyncApi::ChannelItem} + def initialize(channel_proxy, async_api_channel_item) + @channel_proxy = channel_proxy + @async_api_channel_item = async_api_channel_item + end + + def publish(payload:, publish_bindings:, headers: {}) + data = payload[:data] + f_name = payload[:filename] + io = StringIO.new(data) + channel_proxy.execute do |sftp, path| + upload_path = File.join(path, f_name) + sftp.upload!(io, upload_path) + end + end + end + end + end +end \ No newline at end of file diff --git a/lib/event_source/protocols/sftp/sftp_uri.rb b/lib/event_source/protocols/sftp/sftp_uri.rb new file mode 100644 index 00000000..72625767 --- /dev/null +++ b/lib/event_source/protocols/sftp/sftp_uri.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'uri' + +# Include URI::SFTP +module URI + class SFTP < Generic + DEFAULT_PORT = 22 + end + + if EventSource::RubyVersions::LESS_THAN_THREE_ONE + @@schemes['SFTP'] = SFTP + else + register_scheme 'SFTP', SFTP + end +end \ No newline at end of file diff --git a/lib/event_source/protocols/sftp_protocol.rb b/lib/event_source/protocols/sftp_protocol.rb new file mode 100644 index 00000000..88742361 --- /dev/null +++ b/lib/event_source/protocols/sftp_protocol.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require_relative 'sftp/sftp_uri' +require_relative 'sftp/sftp_connection_proxy' +require_relative 'sftp/sftp_channel_proxy' +require_relative 'sftp/sftp_publish_proxy' + +module EventSource + module Protocols + # Namespace for classes and modules that use AsyncAPI to manage message + # exchange using the SFTP protocol + module Sftp + end + end +end diff --git a/lib/event_source/ruby_versions.rb b/lib/event_source/ruby_versions.rb index 80df5eea..e3dddd5a 100644 --- a/lib/event_source/ruby_versions.rb +++ b/lib/event_source/ruby_versions.rb @@ -5,10 +5,13 @@ module EventSource class RubyVersions CURRENT_VERSION = Gem::Version.new(RUBY_VERSION) + VERSION_TWO_SEVEN = Gem::Version.new("2.7.0") VERSION_THREE = Gem::Version.new("3.0.0") VERSION_THREE_ONE = Gem::Version.new("3.1.0") LESS_THAN_THREE = CURRENT_VERSION < VERSION_THREE LESS_THAN_THREE_ONE = CURRENT_VERSION < VERSION_THREE_ONE + + GREATER_THAN_TWO_SIX = CURRENT_VERSION >= VERSION_TWO_SEVEN end end \ No newline at end of file diff --git a/spec/config_helper.rb b/spec/config_helper.rb index d3d11654..1c5d3b93 100644 --- a/spec/config_helper.rb +++ b/spec/config_helper.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true EventSource.configure do |config| - config.protocols = %w[amqp http] + config.protocols = %w[amqp http sftp] config.log_level = :warn end diff --git a/spec/event_source/connection_manager_spec.rb b/spec/event_source/connection_manager_spec.rb index da9d30cf..9f209e3c 100644 --- a/spec/event_source/connection_manager_spec.rb +++ b/spec/event_source/connection_manager_spec.rb @@ -8,6 +8,7 @@ before(:all) do described_class.instance.drop_connections_for(:amqp) described_class.instance.drop_connections_for(:http) + described_class.instance.drop_connections_for(:sftp) end context 'A ConnectionManager Singleton instance' do diff --git a/spec/event_source/protocols/sftp/sftp_async_api_spec.rb b/spec/event_source/protocols/sftp/sftp_async_api_spec.rb new file mode 100644 index 00000000..381887ab --- /dev/null +++ b/spec/event_source/protocols/sftp/sftp_async_api_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe EventSource::Protocols::Sftp, "able to load a simple publisher definition" do + let(:async_api_file) do + Pathname.pwd.join( + 'spec', + 'support', + 'asyncapi', + 'sftp_example_publish.yml' + ) + end + let(:config) do + EventSource::AsyncApi::Operations::AsyncApiConf::LoadPath + .new + .call(path: async_api_file) + .value! + end + + before(:each) do + EventSource::ConnectionManager.instance.drop_connections_for(:http) + EventSource::ConnectionManager.instance.drop_connections_for(:amqp) + EventSource::ConnectionManager.instance.drop_connections_for(:sftp) + EventSource.create_connections + EventSource.config.async_api_schemas = [config] + EventSource.config.load_async_api_resources + end + + let(:connection) do + EventSource::ConnectionManager.instance.fetch_connection(config.servers.first) + end + + it "has a connection" do + expect(connection).not_to be_nil + end + + it "has a publish operation" do + expect(EventSource::ConnectionManager.instance.find_publish_operation({:protocol => :sftp, :publish_operation_name => config.channels.first.publish.operationId})).not_to be_nil + end + + it "has registered the SFTP protocol" do + conn = EventSource::ConnectionManager.instance.connections_for(:sftp).first + expect(URI.scheme_list.keys).to include "SFTP" + end +end \ No newline at end of file diff --git a/spec/event_source/protocols/sftp/sftp_message_publish_spec.rb b/spec/event_source/protocols/sftp/sftp_message_publish_spec.rb new file mode 100644 index 00000000..b6508f4d --- /dev/null +++ b/spec/event_source/protocols/sftp/sftp_message_publish_spec.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require 'rails_helper' + +class SftpProtocolExamplePublisher < ::EventSource::Publisher + include ::EventSource::Publisher[sftp: 'some.file'] + + register_event "upload_to_cms" +end + +class UploadToCms < ::EventSource::Event + publisher_path("sftp_protocol_example_publisher") +end + +class SftpProtocolExamplePublishingContext + include ::EventSource::Command +end + +RSpec.describe EventSource::Protocols::Sftp, "with a publisher definition loaded" do + let(:async_api_file) do + Pathname.pwd.join( + 'spec', + 'support', + 'asyncapi', + 'sftp_example_publish.yml' + ) + end + let(:config) do + EventSource::AsyncApi::Operations::AsyncApiConf::LoadPath + .new + .call(path: async_api_file) + .value! + end + + let(:test_file_upload_path) do + File.expand_path( + File.join( + File.dirname(__FILE__), + "..", + "..", + "..", + "mock_services", + "sftp_server_root", + "some_Crazy_GeneratedFilename.zip" + ) + ) + end + + let(:server_run_path) do + File.expand_path( + File.join( + File.dirname(__FILE__), + "..", + "..", + "..", + ".." + ) + ) + end + + before(:each) do + FileUtils.rm_f(test_file_upload_path) + @sftp_server_pid = spawn( + "bundle exec ruby spec/mock_services/sftp_server.rb", + { + :chdir => server_run_path + } + ) + sleep(1) + EventSource::ConnectionManager.instance.drop_connections_for(:http) + EventSource::ConnectionManager.instance.drop_connections_for(:amqp) + EventSource::ConnectionManager.instance.drop_connections_for(:sftp) + EventSource.create_connections + EventSource.config.async_api_schemas = [config] + EventSource.config.load_async_api_resources + end + + after(:each) do + Process.kill("INT", @sftp_server_pid) + end + + it "can publish a message" do + pub_context = SftpProtocolExamplePublishingContext.new + event = pub_context.event( + "upload_to_cms", + attributes: { + data: "SOME RAW DATA", + filename: "some_Crazy_GeneratedFilename.zip" + } + ) + event.value!.publish + expect(File.exist?(test_file_upload_path)).to be_truthy + end +end \ No newline at end of file diff --git a/spec/mock_services/.gitignore b/spec/mock_services/.gitignore new file mode 100644 index 00000000..f1f51718 --- /dev/null +++ b/spec/mock_services/.gitignore @@ -0,0 +1 @@ +sftp_server_root/* \ No newline at end of file diff --git a/spec/mock_services/sftp_server.rb b/spec/mock_services/sftp_server.rb new file mode 100644 index 00000000..6d247938 --- /dev/null +++ b/spec/mock_services/sftp_server.rb @@ -0,0 +1,57 @@ +require 'hrr_rb_ssh' +require 'hrr_rb_sftp' +require 'logger' + +options = Hash.new + +logger = Logger.new(STDOUT) +logger.level = Logger::INFO + +auth_password = HrrRbSsh::Authentication::Authenticator.new { |context| + user_and_pass = [ + ['user1', 'password1'] + ] + user_and_pass.any? { |user, pass| + context.verify user, pass + } +} +options['authentication_password_authenticator'] = auth_password + +subsys = HrrRbSsh::Connection::RequestHandler.new { |ctx| + ctx.chain_proc { |chain| + case ctx.subsystem_name + when 'sftp' + begin + sftp_server = HrrRbSftp::Server.new(logger: logger) + sftp_server.start(ctx.io[0], ctx.io[1], ctx.io[2]) + exitstatus = 0 + rescue + exitstatus = 1 + end + else + # Do something for other subsystem, or just return exitstatus + exitstatus = 0 + end + exitstatus + } +} + +options['connection_channel_request_subsystem'] = subsys + +server = TCPServer.new 31337 +loop do + Thread.new(server.accept) do |io| + pid = fork do + begin + server = HrrRbSsh::Server.new options + server.start io + ensure + io.close + end + end + io.close + Process.waitpid pid + end +end + +HrrRbSsh::Server.new options, logger: logger \ No newline at end of file diff --git a/spec/mock_services/sftp_server_root/.gitkeep b/spec/mock_services/sftp_server_root/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/spec/rails_app/config/initializers/event_source.rb b/spec/rails_app/config/initializers/event_source.rb index d192c56d..1c209e2b 100644 --- a/spec/rails_app/config/initializers/event_source.rb +++ b/spec/rails_app/config/initializers/event_source.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true EventSource.configure do |config| - config.protocols = %w[amqp http] + config.protocols = %w[amqp http sftp] config.pub_sub_root = Pathname.pwd.join('spec', 'rails_app', 'app', 'event_source') config.server_key = Rails.env.to_sym config.app_name = :enroll @@ -96,6 +96,24 @@ soap.use_timestamp = true soap.timestamp_ttl = 60.seconds end + + server.sftp do |sftp| + sftp.ref = "sftp://sftp/" + sftp.url = "sftp://localhost" + sftp.user_name = "user1" + sftp.password = "password1" + sftp.path = File.expand_path( + File.join( + File.dirname(__FILE__), + "..", + "..", + "..", + "mock_services", + "sftp_server_root" + ) + ) + sftp.port = 31337 + end end # server.amqp do |amqp| diff --git a/spec/support/asyncapi/sftp_example_publish.yml b/spec/support/asyncapi/sftp_example_publish.yml new file mode 100644 index 00000000..f3817568 --- /dev/null +++ b/spec/support/asyncapi/sftp_example_publish.yml @@ -0,0 +1,21 @@ +--- +asyncapi: 2.0.0 +info: + title: SFTP Example + version: 0.1.0 + description: AMQP Publish configuration for the Fdsh services + +servers: + test: + url: "sftp://sftp/" + protocol: sftp + description: Sftp Server + +channels: + some.file.upload_to_cms: + publish: + operationId: some.file.upload_to_cms + description: Upload some file + bindings: + sftp: + path: /home/user/outbox