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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rspec +3 -0
- data/.travis.yml +25 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +135 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/demo/hrr_rb_sftp_server.rb +23 -0
- data/demo/instantiate_hrr_rb_sftp_server.rb +67 -0
- data/demo/spawn_hrr_rb_sftp_server.rb +66 -0
- data/hrr_rb_sftp.gemspec +26 -0
- data/lib/hrr_rb_sftp.rb +13 -0
- data/lib/hrr_rb_sftp/loggable.rb +41 -0
- data/lib/hrr_rb_sftp/protocol.rb +62 -0
- data/lib/hrr_rb_sftp/protocol/common.rb +10 -0
- data/lib/hrr_rb_sftp/protocol/common/data_type.rb +15 -0
- data/lib/hrr_rb_sftp/protocol/common/data_type/byte.rb +22 -0
- data/lib/hrr_rb_sftp/protocol/common/data_type/extension_pair.rb +23 -0
- data/lib/hrr_rb_sftp/protocol/common/data_type/extension_pairs.rb +24 -0
- data/lib/hrr_rb_sftp/protocol/common/data_type/string.rb +24 -0
- data/lib/hrr_rb_sftp/protocol/common/data_type/uint32.rb +22 -0
- data/lib/hrr_rb_sftp/protocol/common/data_type/uint64.rb +22 -0
- data/lib/hrr_rb_sftp/protocol/common/packet.rb +11 -0
- data/lib/hrr_rb_sftp/protocol/common/packet/001_ssh_fxp_init.rb +18 -0
- data/lib/hrr_rb_sftp/protocol/common/packet/002_ssh_fxp_version.rb +19 -0
- data/lib/hrr_rb_sftp/protocol/common/packetable.rb +72 -0
- data/lib/hrr_rb_sftp/protocol/version1.rb +10 -0
- data/lib/hrr_rb_sftp/protocol/version1/data_type.rb +11 -0
- data/lib/hrr_rb_sftp/protocol/version1/data_type/attrs.rb +54 -0
- data/lib/hrr_rb_sftp/protocol/version1/packet.rb +29 -0
- data/lib/hrr_rb_sftp/protocol/version1/packet/003_ssh_fxp_open.rb +109 -0
- data/lib/hrr_rb_sftp/protocol/version1/packet/004_ssh_fxp_close.rb +44 -0
- data/lib/hrr_rb_sftp/protocol/version1/packet/005_ssh_fxp_read.rb +53 -0
- data/lib/hrr_rb_sftp/protocol/version1/packet/006_ssh_fxp_write.rb +46 -0
- data/lib/hrr_rb_sftp/protocol/version1/packet/007_ssh_fxp_lstat.rb +62 -0
- data/lib/hrr_rb_sftp/protocol/version1/packet/008_ssh_fxp_fstat.rb +48 -0
- data/lib/hrr_rb_sftp/protocol/version1/packet/009_ssh_fxp_setstat.rb +63 -0
- data/lib/hrr_rb_sftp/protocol/version1/packet/010_ssh_fxp_fsetstat.rb +48 -0
- data/lib/hrr_rb_sftp/protocol/version1/packet/011_ssh_fxp_opendir.rb +65 -0
- data/lib/hrr_rb_sftp/protocol/version1/packet/012_ssh_fxp_readdir.rb +134 -0
- data/lib/hrr_rb_sftp/protocol/version1/packet/013_ssh_fxp_remove.rb +57 -0
- data/lib/hrr_rb_sftp/protocol/version1/packet/014_ssh_fxp_mkdir.rb +57 -0
- data/lib/hrr_rb_sftp/protocol/version1/packet/015_ssh_fxp_rmdir.rb +73 -0
- data/lib/hrr_rb_sftp/protocol/version1/packet/016_ssh_fxp_realpath.rb +30 -0
- data/lib/hrr_rb_sftp/protocol/version1/packet/017_ssh_fxp_stat.rb +62 -0
- data/lib/hrr_rb_sftp/protocol/version1/packet/101_ssh_fxp_status.rb +29 -0
- data/lib/hrr_rb_sftp/protocol/version1/packet/102_ssh_fxp_handle.rb +19 -0
- data/lib/hrr_rb_sftp/protocol/version1/packet/103_ssh_fxp_data.rb +19 -0
- data/lib/hrr_rb_sftp/protocol/version1/packet/104_ssh_fxp_name.rb +33 -0
- data/lib/hrr_rb_sftp/protocol/version1/packet/105_ssh_fxp_attrs.rb +19 -0
- data/lib/hrr_rb_sftp/protocol/version2.rb +10 -0
- data/lib/hrr_rb_sftp/protocol/version2/data_type.rb +9 -0
- data/lib/hrr_rb_sftp/protocol/version2/packet.rb +11 -0
- data/lib/hrr_rb_sftp/protocol/version2/packet/018_ssh_fxp_rename.rb +70 -0
- data/lib/hrr_rb_sftp/protocol/version3.rb +10 -0
- data/lib/hrr_rb_sftp/protocol/version3/data_type.rb +9 -0
- data/lib/hrr_rb_sftp/protocol/version3/packet.rb +16 -0
- data/lib/hrr_rb_sftp/protocol/version3/packet/014_ssh_fxp_mkdir.rb +58 -0
- data/lib/hrr_rb_sftp/protocol/version3/packet/019_ssh_fxp_readlink.rb +57 -0
- data/lib/hrr_rb_sftp/protocol/version3/packet/020_ssh_fxp_symlink.rb +58 -0
- data/lib/hrr_rb_sftp/protocol/version3/packet/101_ssh_fxp_status.rb +31 -0
- data/lib/hrr_rb_sftp/protocol/version3/packet/200_ssh_fxp_extended.rb +34 -0
- data/lib/hrr_rb_sftp/protocol/version3/packet/201_ssh_fxp_extended_reply.rb +23 -0
- data/lib/hrr_rb_sftp/receiver.rb +22 -0
- data/lib/hrr_rb_sftp/sender.rb +13 -0
- data/lib/hrr_rb_sftp/server.rb +96 -0
- data/lib/hrr_rb_sftp/version.rb +3 -0
- metadata +114 -0
data/hrr_rb_sftp.gemspec
ADDED
@@ -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
|
data/lib/hrr_rb_sftp.rb
ADDED
@@ -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,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,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
|