hrr_rb_sftp 0.1.0

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