hrr_rb_sftp 0.1.0

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.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +25 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +7 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +135 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/demo/hrr_rb_sftp_server.rb +23 -0
  13. data/demo/instantiate_hrr_rb_sftp_server.rb +67 -0
  14. data/demo/spawn_hrr_rb_sftp_server.rb +66 -0
  15. data/hrr_rb_sftp.gemspec +26 -0
  16. data/lib/hrr_rb_sftp.rb +13 -0
  17. data/lib/hrr_rb_sftp/loggable.rb +41 -0
  18. data/lib/hrr_rb_sftp/protocol.rb +62 -0
  19. data/lib/hrr_rb_sftp/protocol/common.rb +10 -0
  20. data/lib/hrr_rb_sftp/protocol/common/data_type.rb +15 -0
  21. data/lib/hrr_rb_sftp/protocol/common/data_type/byte.rb +22 -0
  22. data/lib/hrr_rb_sftp/protocol/common/data_type/extension_pair.rb +23 -0
  23. data/lib/hrr_rb_sftp/protocol/common/data_type/extension_pairs.rb +24 -0
  24. data/lib/hrr_rb_sftp/protocol/common/data_type/string.rb +24 -0
  25. data/lib/hrr_rb_sftp/protocol/common/data_type/uint32.rb +22 -0
  26. data/lib/hrr_rb_sftp/protocol/common/data_type/uint64.rb +22 -0
  27. data/lib/hrr_rb_sftp/protocol/common/packet.rb +11 -0
  28. data/lib/hrr_rb_sftp/protocol/common/packet/001_ssh_fxp_init.rb +18 -0
  29. data/lib/hrr_rb_sftp/protocol/common/packet/002_ssh_fxp_version.rb +19 -0
  30. data/lib/hrr_rb_sftp/protocol/common/packetable.rb +72 -0
  31. data/lib/hrr_rb_sftp/protocol/version1.rb +10 -0
  32. data/lib/hrr_rb_sftp/protocol/version1/data_type.rb +11 -0
  33. data/lib/hrr_rb_sftp/protocol/version1/data_type/attrs.rb +54 -0
  34. data/lib/hrr_rb_sftp/protocol/version1/packet.rb +29 -0
  35. data/lib/hrr_rb_sftp/protocol/version1/packet/003_ssh_fxp_open.rb +109 -0
  36. data/lib/hrr_rb_sftp/protocol/version1/packet/004_ssh_fxp_close.rb +44 -0
  37. data/lib/hrr_rb_sftp/protocol/version1/packet/005_ssh_fxp_read.rb +53 -0
  38. data/lib/hrr_rb_sftp/protocol/version1/packet/006_ssh_fxp_write.rb +46 -0
  39. data/lib/hrr_rb_sftp/protocol/version1/packet/007_ssh_fxp_lstat.rb +62 -0
  40. data/lib/hrr_rb_sftp/protocol/version1/packet/008_ssh_fxp_fstat.rb +48 -0
  41. data/lib/hrr_rb_sftp/protocol/version1/packet/009_ssh_fxp_setstat.rb +63 -0
  42. data/lib/hrr_rb_sftp/protocol/version1/packet/010_ssh_fxp_fsetstat.rb +48 -0
  43. data/lib/hrr_rb_sftp/protocol/version1/packet/011_ssh_fxp_opendir.rb +65 -0
  44. data/lib/hrr_rb_sftp/protocol/version1/packet/012_ssh_fxp_readdir.rb +134 -0
  45. data/lib/hrr_rb_sftp/protocol/version1/packet/013_ssh_fxp_remove.rb +57 -0
  46. data/lib/hrr_rb_sftp/protocol/version1/packet/014_ssh_fxp_mkdir.rb +57 -0
  47. data/lib/hrr_rb_sftp/protocol/version1/packet/015_ssh_fxp_rmdir.rb +73 -0
  48. data/lib/hrr_rb_sftp/protocol/version1/packet/016_ssh_fxp_realpath.rb +30 -0
  49. data/lib/hrr_rb_sftp/protocol/version1/packet/017_ssh_fxp_stat.rb +62 -0
  50. data/lib/hrr_rb_sftp/protocol/version1/packet/101_ssh_fxp_status.rb +29 -0
  51. data/lib/hrr_rb_sftp/protocol/version1/packet/102_ssh_fxp_handle.rb +19 -0
  52. data/lib/hrr_rb_sftp/protocol/version1/packet/103_ssh_fxp_data.rb +19 -0
  53. data/lib/hrr_rb_sftp/protocol/version1/packet/104_ssh_fxp_name.rb +33 -0
  54. data/lib/hrr_rb_sftp/protocol/version1/packet/105_ssh_fxp_attrs.rb +19 -0
  55. data/lib/hrr_rb_sftp/protocol/version2.rb +10 -0
  56. data/lib/hrr_rb_sftp/protocol/version2/data_type.rb +9 -0
  57. data/lib/hrr_rb_sftp/protocol/version2/packet.rb +11 -0
  58. data/lib/hrr_rb_sftp/protocol/version2/packet/018_ssh_fxp_rename.rb +70 -0
  59. data/lib/hrr_rb_sftp/protocol/version3.rb +10 -0
  60. data/lib/hrr_rb_sftp/protocol/version3/data_type.rb +9 -0
  61. data/lib/hrr_rb_sftp/protocol/version3/packet.rb +16 -0
  62. data/lib/hrr_rb_sftp/protocol/version3/packet/014_ssh_fxp_mkdir.rb +58 -0
  63. data/lib/hrr_rb_sftp/protocol/version3/packet/019_ssh_fxp_readlink.rb +57 -0
  64. data/lib/hrr_rb_sftp/protocol/version3/packet/020_ssh_fxp_symlink.rb +58 -0
  65. data/lib/hrr_rb_sftp/protocol/version3/packet/101_ssh_fxp_status.rb +31 -0
  66. data/lib/hrr_rb_sftp/protocol/version3/packet/200_ssh_fxp_extended.rb +34 -0
  67. data/lib/hrr_rb_sftp/protocol/version3/packet/201_ssh_fxp_extended_reply.rb +23 -0
  68. data/lib/hrr_rb_sftp/receiver.rb +22 -0
  69. data/lib/hrr_rb_sftp/sender.rb +13 -0
  70. data/lib/hrr_rb_sftp/server.rb +96 -0
  71. data/lib/hrr_rb_sftp/version.rb +3 -0
  72. metadata +114 -0
@@ -0,0 +1,26 @@
1
+ require_relative 'lib/hrr_rb_sftp/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "hrr_rb_sftp"
5
+ spec.version = HrrRbSftp::VERSION
6
+ spec.authors = ["hirura"]
7
+ spec.email = ["hirura@gmail.com"]
8
+
9
+ spec.summary = "Pure Ruby SFTP server implementation."
10
+ spec.description = "Pure Ruby SFTP server implementation."
11
+ spec.homepage = "https://github.com/hirura/hrr_rb_sftp"
12
+ spec.license = "MIT"
13
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.0.0")
14
+
15
+ spec.metadata["homepage_uri"] = spec.homepage
16
+ #spec.metadata["source_code_uri"] = spec.homepage
17
+ #spec.metadata["changelog_uri"] = spec.homepage
18
+
19
+ # Specify which files should be added to the gem when it is released.
20
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
21
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
22
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
23
+ end
24
+
25
+ spec.require_paths = ["lib"]
26
+ end
@@ -0,0 +1,13 @@
1
+ module HrrRbSftp
2
+ end
3
+
4
+ require "fileutils"
5
+ require "stringio"
6
+ require "etc"
7
+
8
+ require "hrr_rb_sftp/version"
9
+ require "hrr_rb_sftp/loggable"
10
+ require "hrr_rb_sftp/protocol"
11
+ require "hrr_rb_sftp/receiver"
12
+ require "hrr_rb_sftp/sender"
13
+ require "hrr_rb_sftp/server"
@@ -0,0 +1,41 @@
1
+ module HrrRbSftp
2
+ module Loggable
3
+ attr_accessor :logger
4
+
5
+ def log_fatal
6
+ if logger
7
+ logger.fatal(log_key){ yield }
8
+ end
9
+ end
10
+
11
+ def log_error
12
+ if logger
13
+ logger.error(log_key){ yield }
14
+ end
15
+ end
16
+
17
+ def log_warn
18
+ if logger
19
+ logger.warn(log_key){ yield }
20
+ end
21
+ end
22
+
23
+ def log_info
24
+ if logger
25
+ logger.info(log_key){ yield }
26
+ end
27
+ end
28
+
29
+ def log_debug
30
+ if logger
31
+ logger.debug(log_key){ yield }
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def log_key
38
+ @log_key ||= self.class.to_s + "[%x]" % object_id
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,62 @@
1
+ module HrrRbSftp
2
+ class Protocol
3
+ include Loggable
4
+
5
+ def self.versions
6
+ constants.select{|c| c.to_s.start_with?("Version")}.map{|c| const_get(c)}.map{|klass| klass::PROTOCOL_VERSION}
7
+ end
8
+
9
+ def initialize version, logger: nil
10
+ self.logger = logger
11
+
12
+ @handles = Hash.new
13
+ @version = version
14
+ @version_class = self.class.const_get(:"Version#{@version}")
15
+ packet_classes = @version_class::Packet.constants.select{|c| c.to_s.start_with?("SSH_FXP_")}.map{|c| @version_class::Packet.const_get(c)}
16
+ @packets = packet_classes.map{|pkt| [pkt::TYPE, pkt.new(@handles, logger: logger)]}.inject(Hash.new){|h,(k,v)| h.update({k => v})}
17
+ end
18
+
19
+ def respond_to request_payload
20
+ request_type = request_payload[0].unpack("C")[0]
21
+ response_packet = if @packets.has_key?(request_type)
22
+ begin
23
+ request_packet = @packets[request_type].decode request_payload
24
+ rescue => e
25
+ {
26
+ :"type" => @version_class::Packet::SSH_FXP_STATUS::TYPE,
27
+ :"request-id" => (request_payload[1,4].unpack("N")[0] || 0),
28
+ :"code" => @version_class::Packet::SSH_FXP_STATUS::SSH_FX_BAD_MESSAGE,
29
+ :"error message" => e.message,
30
+ :"language tag" => "",
31
+ }
32
+ else
33
+ @packets[request_type].respond_to request_packet
34
+ end
35
+ else
36
+ {
37
+ :"type" => @version_class::Packet::SSH_FXP_STATUS::TYPE,
38
+ :"request-id" => (request_payload[1,4].unpack("N")[0] || 0),
39
+ :"code" => @version_class::Packet::SSH_FXP_STATUS::SSH_FX_OP_UNSUPPORTED,
40
+ :"error message" => "Unsupported type: #{request_type}",
41
+ :"language tag" => "",
42
+ }
43
+ end
44
+ response_type = response_packet[:"type"]
45
+ @packets[response_type].encode response_packet
46
+ end
47
+
48
+ def close_handles
49
+ log_info { "closing handles" }
50
+ @handles.each do |k, v|
51
+ v.close rescue nil
52
+ end
53
+ @handles.clear
54
+ log_info { "handles closed" }
55
+ end
56
+ end
57
+ end
58
+
59
+ require "hrr_rb_sftp/protocol/common"
60
+ require "hrr_rb_sftp/protocol/version1"
61
+ require "hrr_rb_sftp/protocol/version2"
62
+ require "hrr_rb_sftp/protocol/version3"
@@ -0,0 +1,10 @@
1
+ module HrrRbSftp
2
+ class Protocol
3
+ module Common
4
+ end
5
+ end
6
+ end
7
+
8
+ require "hrr_rb_sftp/protocol/common/data_type"
9
+ require "hrr_rb_sftp/protocol/common/packetable"
10
+ require "hrr_rb_sftp/protocol/common/packet"
@@ -0,0 +1,15 @@
1
+ module HrrRbSftp
2
+ class Protocol
3
+ module Common
4
+ module DataType
5
+ end
6
+ end
7
+ end
8
+ end
9
+
10
+ require "hrr_rb_sftp/protocol/common/data_type/byte"
11
+ require "hrr_rb_sftp/protocol/common/data_type/uint32"
12
+ require "hrr_rb_sftp/protocol/common/data_type/uint64"
13
+ require 'hrr_rb_sftp/protocol/common/data_type/string'
14
+ require 'hrr_rb_sftp/protocol/common/data_type/extension_pair'
15
+ require 'hrr_rb_sftp/protocol/common/data_type/extension_pairs'
@@ -0,0 +1,22 @@
1
+ module HrrRbSftp
2
+ class Protocol
3
+ module Common
4
+ module DataType
5
+ module Byte
6
+ def self.encode arg
7
+ case arg
8
+ when 0x00..0xff
9
+ [arg].pack("C")
10
+ else
11
+ raise ArgumentError, "must be in #{0x00}..#{0xff}, but got #{arg.inspect}"
12
+ end
13
+ end
14
+
15
+ def self.decode io
16
+ io.read(1).unpack("C")[0]
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ module HrrRbSftp
2
+ class Protocol
3
+ module Common
4
+ module DataType
5
+ module ExtensionPair
6
+ def self.encode arg
7
+ unless arg.kind_of? ::Hash
8
+ raise ArgumentError, "must be a kind of Hash, but got #{arg.inspect}"
9
+ end
10
+ DataType::String.encode(arg[:"extension-name"]) + DataType::String.encode(arg[:"extension-data"])
11
+ end
12
+
13
+ def self.decode io
14
+ {
15
+ :"extension-name" => DataType::String.decode(io),
16
+ :"extension-data" => DataType::String.decode(io),
17
+ }
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ module HrrRbSftp
2
+ class Protocol
3
+ module Common
4
+ module DataType
5
+ module ExtensionPairs
6
+ def self.encode arg
7
+ unless arg.kind_of? ::Array
8
+ raise ArgumentError, "must be a kind of Array, but got #{arg.inspect}"
9
+ end
10
+ arg.map{|arg| ExtensionPair.encode(arg)}.join
11
+ end
12
+
13
+ def self.decode io
14
+ extension_pairs = Array.new
15
+ until io.eof?
16
+ extension_pairs.push ExtensionPair.decode(io)
17
+ end
18
+ extension_pairs
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ module HrrRbSftp
2
+ class Protocol
3
+ module Common
4
+ module DataType
5
+ module String
6
+ def self.encode arg
7
+ unless arg.kind_of? ::String
8
+ raise ArgumentError, "must be a kind of String, but got #{arg.inspect}"
9
+ end
10
+ if arg.bytesize > 0xffff_ffff
11
+ raise ArgumentError, "must be shorter than or equal to #{0xffff_ffff}, but got length #{arg.bytesize}"
12
+ end
13
+ [arg.bytesize, arg].pack("Na*")
14
+ end
15
+
16
+ def self.decode io
17
+ length = io.read(4).unpack("N")[0]
18
+ io.read(length).unpack("a*")[0].force_encoding(Encoding::UTF_8)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ module HrrRbSftp
2
+ class Protocol
3
+ module Common
4
+ module DataType
5
+ module Uint32
6
+ def self.encode arg
7
+ case arg
8
+ when 0x0000_0000..0xffff_ffff
9
+ [arg].pack("N")
10
+ else
11
+ raise ArgumentError, "must be in #{0x0000_0000}..#{0xffff_ffff}, but got #{arg.inspect}"
12
+ end
13
+ end
14
+
15
+ def self.decode io
16
+ io.read(4).unpack("N")[0]
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ module HrrRbSftp
2
+ class Protocol
3
+ module Common
4
+ module DataType
5
+ module Uint64
6
+ def self.encode arg
7
+ case arg
8
+ when 0x0000_0000_0000_0000..0xffff_ffff_ffff_ffff
9
+ [arg >> 32].pack("N") + [arg & 0x0000_0000_ffff_ffff].pack("N")
10
+ else
11
+ raise ArgumentError, "must be in #{0x0000_0000_0000_0000}..#{0xffff_ffff_ffff_ffff}, but got #{arg.inspect}"
12
+ end
13
+ end
14
+
15
+ def self.decode io
16
+ (io.read(4).unpack("N")[0] << 32) + (io.read(4).unpack("N")[0])
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ module HrrRbSftp
2
+ class Protocol
3
+ module Common
4
+ module Packet
5
+ end
6
+ end
7
+ end
8
+ end
9
+
10
+ require "hrr_rb_sftp/protocol/common/packet/001_ssh_fxp_init"
11
+ require "hrr_rb_sftp/protocol/common/packet/002_ssh_fxp_version"
@@ -0,0 +1,18 @@
1
+ module HrrRbSftp
2
+ class Protocol
3
+ module Common
4
+ module Packet
5
+ class SSH_FXP_INIT
6
+ include Packetable
7
+
8
+ TYPE = 1
9
+
10
+ FORMAT = [
11
+ [DataType::Byte, :"type" ],
12
+ [DataType::Uint32, :"version"],
13
+ ]
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ module HrrRbSftp
2
+ class Protocol
3
+ module Common
4
+ module Packet
5
+ class SSH_FXP_VERSION
6
+ include Packetable
7
+
8
+ TYPE = 2
9
+
10
+ FORMAT = [
11
+ [DataType::Byte, :"type" ],
12
+ [DataType::Uint32, :"version" ],
13
+ [DataType::ExtensionPairs, :"extensions"],
14
+ ]
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,72 @@
1
+ module HrrRbSftp
2
+ class Protocol
3
+ module Common
4
+ module Packetable
5
+ include Loggable
6
+
7
+ def initialize handles, logger: nil
8
+ self.logger = logger
9
+
10
+ @handles = handles
11
+ end
12
+
13
+ def encode packet
14
+ log_debug { 'encoding packet: ' + packet.inspect }
15
+ format = common_format + conditional_format(packet)
16
+ format.map{ |data_type, field_name|
17
+ begin
18
+ field_value = packet[field_name]
19
+ data_type.encode field_value
20
+ rescue => e
21
+ log_debug { "'field_name', 'field_value': #{field_name.inspect}, #{field_value.inspect}" }
22
+ raise e
23
+ end
24
+ }.join
25
+ end
26
+
27
+ def decode payload
28
+ payload_io = StringIO.new payload
29
+ format = common_format
30
+ decoded_packet = decode_recursively(payload_io).inject(Hash.new){ |h, (k, v)| h.update({k => v}) }
31
+ log_debug { 'decoded packet: ' + decoded_packet.inspect }
32
+ decoded_packet
33
+ end
34
+
35
+ private
36
+
37
+ def common_format
38
+ self.class::FORMAT
39
+ end
40
+
41
+ def conditional_format packet
42
+ return [] unless self.class.const_defined? :CONDITIONAL_FORMAT
43
+ packet.inject([]){ |a, (field_name, field_value)|
44
+ a + (self.class::CONDITIONAL_FORMAT.fetch(field_name, {})[field_value] || [])
45
+ }
46
+ end
47
+
48
+ def decode_recursively payload_io, packet=nil
49
+ if packet.class == Array and packet.size == 0
50
+ []
51
+ else
52
+ format = case packet
53
+ when nil
54
+ common_format
55
+ when Array
56
+ conditional_format(packet)
57
+ end
58
+ decoded_packet = format.map{ |data_type, field_name|
59
+ begin
60
+ [field_name, data_type.decode(payload_io)]
61
+ rescue => e
62
+ log_debug { "'field_name': #{field_name.inspect}" }
63
+ raise e
64
+ end
65
+ }
66
+ decoded_packet + decode_recursively(payload_io, decoded_packet)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end