ftpd 0.0.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of ftpd might be problematic. Click here for more details.
- data/Gemfile +9 -0
- data/Gemfile.lock +43 -0
- data/LICENSE.md +9 -0
- data/README.md +57 -0
- data/Rakefile +8 -0
- data/VERSION +1 -0
- data/features/command_errors.feature +33 -0
- data/features/delete.feature +34 -0
- data/features/directory_navigation.feature +34 -0
- data/features/file_structure.feature +40 -0
- data/features/get.feature +62 -0
- data/features/list.feature +77 -0
- data/features/login.feature +54 -0
- data/features/mode.feature +40 -0
- data/features/name_list.feature +77 -0
- data/features/noop.feature +14 -0
- data/features/port.feature +25 -0
- data/features/put.feature +65 -0
- data/features/quit.feature +20 -0
- data/features/step_definitions/client_and_server_files.rb +24 -0
- data/features/step_definitions/client_files.rb +9 -0
- data/features/step_definitions/command.rb +5 -0
- data/features/step_definitions/connect.rb +15 -0
- data/features/step_definitions/delete.rb +15 -0
- data/features/step_definitions/directories.rb +22 -0
- data/features/step_definitions/error.rb +87 -0
- data/features/step_definitions/file_structure.rb +16 -0
- data/features/step_definitions/get.rb +16 -0
- data/features/step_definitions/invalid_commands.rb +11 -0
- data/features/step_definitions/line_endings.rb +7 -0
- data/features/step_definitions/list.rb +46 -0
- data/features/step_definitions/login.rb +69 -0
- data/features/step_definitions/mode.rb +15 -0
- data/features/step_definitions/noop.rb +13 -0
- data/features/step_definitions/passive.rb +3 -0
- data/features/step_definitions/port.rb +5 -0
- data/features/step_definitions/put.rb +16 -0
- data/features/step_definitions/quit.rb +15 -0
- data/features/step_definitions/server.rb +7 -0
- data/features/step_definitions/server_files.rb +18 -0
- data/features/step_definitions/type.rb +15 -0
- data/features/support/env.rb +4 -0
- data/features/support/file_templates/ascii_unix +4 -0
- data/features/support/file_templates/ascii_windows +4 -0
- data/features/support/file_templates/binary +0 -0
- data/features/support/test_client.rb +89 -0
- data/features/support/test_file_templates.rb +33 -0
- data/features/support/test_server.rb +52 -0
- data/features/syntax_errors.feature +15 -0
- data/features/type.feature +53 -0
- data/ftpd.gemspec +112 -0
- data/insecure-test-cert.pem +29 -0
- data/lib/ftpd.rb +6 -0
- data/lib/ftpd/FakeFtpServer.rb +736 -0
- data/lib/ftpd/FakeServer.rb +57 -0
- data/lib/ftpd/FakeTlsServer.rb +52 -0
- data/lib/ftpd/ObjectUtil.rb +66 -0
- data/lib/ftpd/TempDir.rb +54 -0
- data/lib/ftpd/q.rb +92 -0
- data/rake_tasks/cucumber.rake +7 -0
- data/rake_tasks/jeweler.rake +25 -0
- metadata +164 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
Feature: Miscellaneous Errors
|
2
|
+
|
3
|
+
As a client
|
4
|
+
I want good error messages
|
5
|
+
So that I can figure out what went wrong
|
6
|
+
|
7
|
+
Scenario: Empty command
|
8
|
+
Given a successful connection
|
9
|
+
When the client sends an empty command
|
10
|
+
Then the server returns a syntax error
|
11
|
+
|
12
|
+
Scenario: Command contains non-word characters
|
13
|
+
Given a successful connection
|
14
|
+
When the client sends a non-word command
|
15
|
+
Then the server returns a syntax error
|
@@ -0,0 +1,53 @@
|
|
1
|
+
Feature: Representation Type
|
2
|
+
|
3
|
+
As a client
|
4
|
+
I want to set the representation type
|
5
|
+
So that I can interoperate with foreign operating systems
|
6
|
+
|
7
|
+
Scenario: Type ASCII
|
8
|
+
Given a successful login
|
9
|
+
Then the client successfully sets type "A"
|
10
|
+
|
11
|
+
Scenario: Type IMAGE
|
12
|
+
Given a successful login
|
13
|
+
Then the client successfully sets type "I"
|
14
|
+
|
15
|
+
Scenario: Type EBCDIC
|
16
|
+
Given a successful login
|
17
|
+
When the client sets type "E"
|
18
|
+
Then the server returns a type not implemented error
|
19
|
+
|
20
|
+
Scenario: Type Local
|
21
|
+
Given a successful login
|
22
|
+
When the client sets type "L 7"
|
23
|
+
Then the server returns a type not implemented error
|
24
|
+
|
25
|
+
Scenario: Invalid Type
|
26
|
+
Given a successful login
|
27
|
+
When the client sets type "*"
|
28
|
+
Then the server returns an invalid type error
|
29
|
+
|
30
|
+
Scenario: Format Telnet
|
31
|
+
Given a successful login
|
32
|
+
When the client sets type "A T"
|
33
|
+
Then the server returns a format not implemented error
|
34
|
+
|
35
|
+
Scenario: Format Carriage Control
|
36
|
+
Given a successful login
|
37
|
+
When the client sets type "A C"
|
38
|
+
Then the server returns a format not implemented error
|
39
|
+
|
40
|
+
Scenario: Invalid Format
|
41
|
+
Given a successful login
|
42
|
+
When the client sets type "A *"
|
43
|
+
Then the server returns an invalid format error
|
44
|
+
|
45
|
+
Scenario: Not logged in
|
46
|
+
Given a successful connection
|
47
|
+
When the client sets type "S"
|
48
|
+
Then the server returns a not logged in error
|
49
|
+
|
50
|
+
Scenario: Missing parameter
|
51
|
+
Given a successful login
|
52
|
+
When the client sets type with no parameter
|
53
|
+
Then the server returns a syntax error
|
data/ftpd.gemspec
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "ftpd"
|
8
|
+
s.version = "0.0.0.pre1"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new("> 1.3.1") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Wayne Conrad"]
|
12
|
+
s.date = "2013-02-10"
|
13
|
+
s.description = "ftpd is a pure Ruby FTP server library. It supports implicit and explicit TLS, and can be used by a text fixture or FTP daemon."
|
14
|
+
s.email = "wconrad@yagni.com"
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE.md",
|
17
|
+
"README.md"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
"Gemfile",
|
21
|
+
"Gemfile.lock",
|
22
|
+
"LICENSE.md",
|
23
|
+
"README.md",
|
24
|
+
"Rakefile",
|
25
|
+
"VERSION",
|
26
|
+
"features/command_errors.feature",
|
27
|
+
"features/delete.feature",
|
28
|
+
"features/directory_navigation.feature",
|
29
|
+
"features/file_structure.feature",
|
30
|
+
"features/get.feature",
|
31
|
+
"features/list.feature",
|
32
|
+
"features/login.feature",
|
33
|
+
"features/mode.feature",
|
34
|
+
"features/name_list.feature",
|
35
|
+
"features/noop.feature",
|
36
|
+
"features/port.feature",
|
37
|
+
"features/put.feature",
|
38
|
+
"features/quit.feature",
|
39
|
+
"features/step_definitions/client_and_server_files.rb",
|
40
|
+
"features/step_definitions/client_files.rb",
|
41
|
+
"features/step_definitions/command.rb",
|
42
|
+
"features/step_definitions/connect.rb",
|
43
|
+
"features/step_definitions/delete.rb",
|
44
|
+
"features/step_definitions/directories.rb",
|
45
|
+
"features/step_definitions/error.rb",
|
46
|
+
"features/step_definitions/file_structure.rb",
|
47
|
+
"features/step_definitions/get.rb",
|
48
|
+
"features/step_definitions/invalid_commands.rb",
|
49
|
+
"features/step_definitions/line_endings.rb",
|
50
|
+
"features/step_definitions/list.rb",
|
51
|
+
"features/step_definitions/login.rb",
|
52
|
+
"features/step_definitions/mode.rb",
|
53
|
+
"features/step_definitions/noop.rb",
|
54
|
+
"features/step_definitions/passive.rb",
|
55
|
+
"features/step_definitions/port.rb",
|
56
|
+
"features/step_definitions/put.rb",
|
57
|
+
"features/step_definitions/quit.rb",
|
58
|
+
"features/step_definitions/server.rb",
|
59
|
+
"features/step_definitions/server_files.rb",
|
60
|
+
"features/step_definitions/type.rb",
|
61
|
+
"features/support/env.rb",
|
62
|
+
"features/support/file_templates/ascii_unix",
|
63
|
+
"features/support/file_templates/ascii_windows",
|
64
|
+
"features/support/file_templates/binary",
|
65
|
+
"features/support/test_client.rb",
|
66
|
+
"features/support/test_file_templates.rb",
|
67
|
+
"features/support/test_server.rb",
|
68
|
+
"features/syntax_errors.feature",
|
69
|
+
"features/type.feature",
|
70
|
+
"ftpd.gemspec",
|
71
|
+
"insecure-test-cert.pem",
|
72
|
+
"lib/ftpd.rb",
|
73
|
+
"lib/ftpd/FakeFtpServer.rb",
|
74
|
+
"lib/ftpd/FakeServer.rb",
|
75
|
+
"lib/ftpd/FakeTlsServer.rb",
|
76
|
+
"lib/ftpd/ObjectUtil.rb",
|
77
|
+
"lib/ftpd/TempDir.rb",
|
78
|
+
"lib/ftpd/q.rb",
|
79
|
+
"rake_tasks/cucumber.rake",
|
80
|
+
"rake_tasks/jeweler.rake"
|
81
|
+
]
|
82
|
+
s.homepage = "http://github.com/wconrad/ftpd"
|
83
|
+
s.licenses = ["MIT"]
|
84
|
+
s.require_paths = ["lib"]
|
85
|
+
s.rubygems_version = "1.8.17"
|
86
|
+
s.summary = "Pure Ruby FTP server library"
|
87
|
+
|
88
|
+
if s.respond_to? :specification_version then
|
89
|
+
s.specification_version = 3
|
90
|
+
|
91
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
92
|
+
s.add_development_dependency(%q<cucumber>, ["~> 1.2.1"])
|
93
|
+
s.add_development_dependency(%q<double-bag-ftps>, ["~> 0.1.0"])
|
94
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.8.4"])
|
95
|
+
s.add_development_dependency(%q<rake>, ["~> 10.0.3"])
|
96
|
+
s.add_development_dependency(%q<rspec>, ["~> 2.0.1"])
|
97
|
+
else
|
98
|
+
s.add_dependency(%q<cucumber>, ["~> 1.2.1"])
|
99
|
+
s.add_dependency(%q<double-bag-ftps>, ["~> 0.1.0"])
|
100
|
+
s.add_dependency(%q<jeweler>, ["~> 1.8.4"])
|
101
|
+
s.add_dependency(%q<rake>, ["~> 10.0.3"])
|
102
|
+
s.add_dependency(%q<rspec>, ["~> 2.0.1"])
|
103
|
+
end
|
104
|
+
else
|
105
|
+
s.add_dependency(%q<cucumber>, ["~> 1.2.1"])
|
106
|
+
s.add_dependency(%q<double-bag-ftps>, ["~> 0.1.0"])
|
107
|
+
s.add_dependency(%q<jeweler>, ["~> 1.8.4"])
|
108
|
+
s.add_dependency(%q<rake>, ["~> 10.0.3"])
|
109
|
+
s.add_dependency(%q<rspec>, ["~> 2.0.1"])
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
2
|
+
MIICIzCCAYwCCQDPNA1ZOq8CbzANBgkqhkiG9w0BAQUFADBWMQswCQYDVQQGEwJV
|
3
|
+
UzEPMA0GA1UECAwGRGVuaWFsMRQwEgYDVQQHDAtTcHJpbmdmaWVsZDEMMAoGA1UE
|
4
|
+
CgwDRGlzMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTIwNjIyMTMyMTI1WhcNMjIw
|
5
|
+
NjIwMTMyMTI1WjBWMQswCQYDVQQGEwJVUzEPMA0GA1UECAwGRGVuaWFsMRQwEgYD
|
6
|
+
VQQHDAtTcHJpbmdmaWVsZDEMMAoGA1UECgwDRGlzMRIwEAYDVQQDDAlsb2NhbGhv
|
7
|
+
c3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKyacIYurNopaS1tHCE5VrUb
|
8
|
+
1tFveP5kWm6kyeE42dYFMcb0wSKofKDWPju+jEwxZ/SLBnF/IvDKqfFH8A7bzdTi
|
9
|
+
mdtiWZgqMjs1QxFWF3ohoHm0bB2l0zSWufefjSjstSJanazOW4seq3Zm9ut233Mm
|
10
|
+
7h2fKgmM8mzUIKqqLCFfAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAJdwBZm4BI+7y
|
11
|
+
Ul0TJtzdLuKo5lKJXX/l8v7hlvziR6Vbv0H7bdqJ+5N3DLDDHZ4DEbypP67lxf2i
|
12
|
+
cyKdbash/nJsMUVUr+MsvJ9VyRRiSyhzqCN/RgaN9nx4Z0fl5I8aQp2qcZi1t8R5
|
13
|
+
ZLgk9oqiPOEca6i22DDBSg0cnhBH9Lk=
|
14
|
+
-----END CERTIFICATE-----
|
15
|
+
-----BEGIN RSA PRIVATE KEY-----
|
16
|
+
MIICXAIBAAKBgQCsmnCGLqzaKWktbRwhOVa1G9bRb3j+ZFpupMnhONnWBTHG9MEi
|
17
|
+
qHyg1j47voxMMWf0iwZxfyLwyqnxR/AO283U4pnbYlmYKjI7NUMRVhd6IaB5tGwd
|
18
|
+
pdM0lrn3n40o7LUiWp2szluLHqt2Zvbrdt9zJu4dnyoJjPJs1CCqqiwhXwIDAQAB
|
19
|
+
AoGAcyj/1qchsNVcVXCtCgXFskSGyWnEooa2R4gvIdPak48XrRT0H3mm3XDUSOxT
|
20
|
+
kyqLn396pxMabunpBRDoPCGvbDdphhcSKIJPRga0LJBnMVp87xeaw0JvNB1EsdzP
|
21
|
+
xbsXwSt39zjJeAE1IAOgMCHC/GRisvRnkZuKOM7XYe7UAGECQQDeawf/5Zgfcvgc
|
22
|
+
Bqxv6ZPCJxAh7FKyWDUqa8RTtD0qWuXOYnlVzaEkN8FfimrPdmQBx+eMacsqCrLG
|
23
|
+
v8hvubt5AkEAxqnzo+IU4bsUdezndEjYKOVH3qs9qO+8bneKCqD4h2k2Db2va8OG
|
24
|
+
sP44hRMvgkkRiZjHWAUeG+ytgWGU7CK1lwJAe1fvv8GbcxVW8nPg/M8T2f+/upBL
|
25
|
+
7AtusG/DGIhDw1FVT/bcQvEeA+/HlSw1v4dwPmyVxBCHUnFMY1vH0+20QQJBAI6s
|
26
|
+
4eCx7qNLM1+Z24RFCJEeUWZWfzsDqcWALnCBuNuvMPXfY8u2KdaVTUwtQjKEfYbf
|
27
|
+
ZVMOodgWO2mvBkAskVMCQE98evHiZkDEpVU89TbbClYpmGOSRjQTrXEaVePLb0Hr
|
28
|
+
GwylNdJEAClM4gK+GnXa4m57xs13eBwuXsM+77fdU2I=
|
29
|
+
-----END RSA PRIVATE KEY-----
|
data/lib/ftpd.rb
ADDED
@@ -0,0 +1,736 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require 'openssl'
|
5
|
+
require 'pathname'
|
6
|
+
require File.expand_path('FakeTlsServer', File.dirname(__FILE__))
|
7
|
+
require File.expand_path('TempDir', File.dirname(__FILE__))
|
8
|
+
require File.expand_path('q', File.dirname(__FILE__))
|
9
|
+
|
10
|
+
class FakeFtpServer < FakeTlsServer
|
11
|
+
|
12
|
+
attr_accessor :user
|
13
|
+
attr_accessor :password
|
14
|
+
attr_accessor :debug_path
|
15
|
+
attr_accessor :response_delay
|
16
|
+
attr_accessor :implicit_tls
|
17
|
+
|
18
|
+
def initialize(data_path)
|
19
|
+
super()
|
20
|
+
self.user = 'user'
|
21
|
+
self.password = 'password'
|
22
|
+
self.debug_path = '/dev/stdout'
|
23
|
+
@data_path = Pathname.new(data_path)
|
24
|
+
@response_delay = 0
|
25
|
+
@implicit_tls = false
|
26
|
+
end
|
27
|
+
|
28
|
+
def session(socket)
|
29
|
+
Session.new(:socket => socket,
|
30
|
+
:user => user,
|
31
|
+
:password => password,
|
32
|
+
:data_path => @data_path,
|
33
|
+
:debug_path => debug_path,
|
34
|
+
:response_delay => response_delay,
|
35
|
+
:implicit_tls => @implicit_tls).run
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
class Session
|
41
|
+
|
42
|
+
def initialize(args)
|
43
|
+
@socket = args[:socket]
|
44
|
+
@socket.encrypt if args[:implicit_tls]
|
45
|
+
@expected_user = args[:user]
|
46
|
+
@expected_password = args[:password]
|
47
|
+
@data_path = @cwd = args[:data_path].realpath
|
48
|
+
@debug_path = args[:debug_path]
|
49
|
+
@data_type = 'A'
|
50
|
+
@mode = 'S'
|
51
|
+
@format = 'N'
|
52
|
+
@structure = 'F'
|
53
|
+
@response_delay = args[:response_delay]
|
54
|
+
@data_channel_protection_level = :clear
|
55
|
+
end
|
56
|
+
|
57
|
+
def run
|
58
|
+
reply "220 FakeFtpServer"
|
59
|
+
@state = :user
|
60
|
+
catch :done do
|
61
|
+
loop do
|
62
|
+
begin
|
63
|
+
s = get_command
|
64
|
+
syntax_error unless s =~ /^(\w+)(?: (.*))?$/
|
65
|
+
command, argument = $1.downcase, $2
|
66
|
+
unless VALID_COMMANDS.include?(command)
|
67
|
+
error "500 Syntax error, command unrecognized: #{s}"
|
68
|
+
end
|
69
|
+
method = 'cmd_' + command
|
70
|
+
unless self.class.private_method_defined?(method)
|
71
|
+
error "502 Command not implemented: #{command}"
|
72
|
+
end
|
73
|
+
send(method, argument)
|
74
|
+
rescue Error => e
|
75
|
+
reply e.message
|
76
|
+
rescue Errno::ECONNRESET, Errno::EPIPE
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
class Error < StandardError
|
85
|
+
end
|
86
|
+
|
87
|
+
VALID_COMMANDS = [
|
88
|
+
"abor",
|
89
|
+
"acct",
|
90
|
+
"allo",
|
91
|
+
"appe",
|
92
|
+
"auth",
|
93
|
+
"pbsz",
|
94
|
+
"cdup",
|
95
|
+
"cwd",
|
96
|
+
"dele",
|
97
|
+
"help",
|
98
|
+
"list",
|
99
|
+
"mkd",
|
100
|
+
"mode",
|
101
|
+
"nlst",
|
102
|
+
"noop",
|
103
|
+
"pass",
|
104
|
+
"pasv",
|
105
|
+
"port",
|
106
|
+
"prot",
|
107
|
+
"pwd",
|
108
|
+
"quit",
|
109
|
+
"rein",
|
110
|
+
"rest",
|
111
|
+
"retr",
|
112
|
+
"rmd",
|
113
|
+
"rnfr",
|
114
|
+
"rnto",
|
115
|
+
"site",
|
116
|
+
"smnt",
|
117
|
+
"stat",
|
118
|
+
"stor",
|
119
|
+
"stou",
|
120
|
+
"stru",
|
121
|
+
"syst",
|
122
|
+
"type",
|
123
|
+
"user",
|
124
|
+
]
|
125
|
+
|
126
|
+
def cmd_user(argument)
|
127
|
+
syntax_error unless argument
|
128
|
+
bad_sequence unless @state == :user
|
129
|
+
@user = argument
|
130
|
+
@state = :password
|
131
|
+
reply "331 Password required"
|
132
|
+
end
|
133
|
+
|
134
|
+
def bad_sequence
|
135
|
+
error "503 Bad sequence of commands"
|
136
|
+
end
|
137
|
+
|
138
|
+
def cmd_pass(argument)
|
139
|
+
syntax_error unless argument
|
140
|
+
bad_sequence unless @state == :password
|
141
|
+
password = argument
|
142
|
+
if @user != @expected_user || password != @expected_password
|
143
|
+
@state = :user
|
144
|
+
error "530 Login incorrect"
|
145
|
+
end
|
146
|
+
reply "230 Logged in"
|
147
|
+
@state = :logged_in
|
148
|
+
end
|
149
|
+
|
150
|
+
def cmd_quit(argument)
|
151
|
+
syntax_error if argument
|
152
|
+
check_logged_in
|
153
|
+
reply "221 Byebye"
|
154
|
+
@state = :user
|
155
|
+
end
|
156
|
+
|
157
|
+
def syntax_error
|
158
|
+
error "501 Syntax error"
|
159
|
+
end
|
160
|
+
|
161
|
+
def cmd_port(argument)
|
162
|
+
check_logged_in
|
163
|
+
pieces = argument.split(/,/)
|
164
|
+
syntax_error unless pieces.size == 6
|
165
|
+
pieces.collect! do |s|
|
166
|
+
syntax_error unless s =~ /^\d{1,3}$/
|
167
|
+
i = s.to_i
|
168
|
+
syntax_error unless (0..255) === i
|
169
|
+
i
|
170
|
+
end
|
171
|
+
@data_hostname = pieces[0..3].join('.')
|
172
|
+
@data_port = pieces[4] << 8 | pieces[5]
|
173
|
+
reply "200 PORT command successful"
|
174
|
+
end
|
175
|
+
|
176
|
+
def cmd_stor(argument)
|
177
|
+
close_data_server_socket_when_done do
|
178
|
+
check_logged_in
|
179
|
+
path = argument
|
180
|
+
syntax_error unless path
|
181
|
+
target = target_path(path)
|
182
|
+
ensure_path_is_in_data_dir(target)
|
183
|
+
contents = receive_file(path)
|
184
|
+
write_file(target, contents)
|
185
|
+
reply "226 Transfer complete"
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def cmd_retr(argument)
|
190
|
+
close_data_server_socket_when_done do
|
191
|
+
check_logged_in
|
192
|
+
path = argument
|
193
|
+
syntax_error unless path
|
194
|
+
target = target_path(path)
|
195
|
+
ensure_path_is_in_data_dir(target)
|
196
|
+
contents = read_file(target)
|
197
|
+
transmit_file(contents)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def cmd_dele(argument)
|
202
|
+
check_logged_in
|
203
|
+
path = argument
|
204
|
+
error "501 Path required" unless path
|
205
|
+
target = target_path(path)
|
206
|
+
ensure_path_is_in_data_dir(target)
|
207
|
+
ensure_path_exists target
|
208
|
+
File.unlink(target)
|
209
|
+
reply "250 DELE command successful"
|
210
|
+
end
|
211
|
+
|
212
|
+
def cmd_list(argument)
|
213
|
+
ls(argument, '-l')
|
214
|
+
end
|
215
|
+
|
216
|
+
def cmd_nlst(argument)
|
217
|
+
ls(argument, '-1')
|
218
|
+
end
|
219
|
+
|
220
|
+
def ls(path, option)
|
221
|
+
close_data_server_socket_when_done do
|
222
|
+
check_logged_in
|
223
|
+
ls_dir, ls_path = get_ls_dir_and_path(path)
|
224
|
+
list = get_file_list(ls_dir, ls_path, option)
|
225
|
+
transmit_file(list, 'A')
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def get_ls_dir_and_path(path)
|
230
|
+
path = path || '.'
|
231
|
+
target = target_path(path)
|
232
|
+
target = realpath(target)
|
233
|
+
ensure_path_is_in_data_dir(target)
|
234
|
+
if target.to_s.index(@cwd.to_s) == 0
|
235
|
+
ls_dir = @cwd
|
236
|
+
ls_path = target.to_s[@cwd.to_s.length..-1]
|
237
|
+
else
|
238
|
+
raise
|
239
|
+
end
|
240
|
+
if ls_path =~ /^\//
|
241
|
+
ls_path = $'
|
242
|
+
end
|
243
|
+
[ls_dir, ls_path]
|
244
|
+
end
|
245
|
+
|
246
|
+
def get_file_list(ls_dir, ls_path, option)
|
247
|
+
command = [
|
248
|
+
'ls',
|
249
|
+
option,
|
250
|
+
ls_path,
|
251
|
+
'2>&1',
|
252
|
+
].compact.join(' ')
|
253
|
+
list = Dir.chdir(ls_dir) do
|
254
|
+
`#{command}`
|
255
|
+
end
|
256
|
+
list = "" if $? != 0
|
257
|
+
list = list.gsub(/^total \d+\n/, '')
|
258
|
+
list
|
259
|
+
end
|
260
|
+
|
261
|
+
def realpath(pathname)
|
262
|
+
handle_system_error do
|
263
|
+
basename = File.basename(pathname.to_s)
|
264
|
+
if is_glob?(basename)
|
265
|
+
pathname.dirname.realpath + basename
|
266
|
+
else
|
267
|
+
pathname.realpath
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
def is_glob?(filename)
|
273
|
+
filename =~ /[.*]/
|
274
|
+
end
|
275
|
+
|
276
|
+
def cmd_type(argument)
|
277
|
+
check_logged_in
|
278
|
+
syntax_error unless argument =~ /^(\S)(?: (\S+))?$/
|
279
|
+
type_code = $1
|
280
|
+
format_code = $2
|
281
|
+
set_type(type_code)
|
282
|
+
set_format(format_code)
|
283
|
+
reply "200 Type set to #{@data_type}"
|
284
|
+
end
|
285
|
+
|
286
|
+
def set_type(type_code)
|
287
|
+
name, implemented = DATA_TYPES[type_code]
|
288
|
+
error "504 Invalid type code" unless name
|
289
|
+
error "504 Type not implemented" unless implemented
|
290
|
+
@data_type = type_code
|
291
|
+
end
|
292
|
+
|
293
|
+
def set_format(format_code)
|
294
|
+
format_code ||= 'N'
|
295
|
+
name, implemented = FORMAT_TYPES[format_code]
|
296
|
+
error "504 Invalid format code" unless name
|
297
|
+
error "504 Format not implemented" unless implemented
|
298
|
+
@data_format = format_code
|
299
|
+
end
|
300
|
+
|
301
|
+
def cmd_mode(argument)
|
302
|
+
syntax_error unless argument
|
303
|
+
check_logged_in
|
304
|
+
name, implemented = TRANSMISSION_MODES[argument]
|
305
|
+
error "504 Invalid mode code" unless name
|
306
|
+
error "504 Mode not implemented" unless implemented
|
307
|
+
@mode = argument
|
308
|
+
reply "200 Mode set to #{name}"
|
309
|
+
end
|
310
|
+
|
311
|
+
def cmd_stru(argument)
|
312
|
+
syntax_error unless argument
|
313
|
+
check_logged_in
|
314
|
+
name, implemented = FILE_STRUCTURES[argument]
|
315
|
+
error "504 Invalid structure code" unless name
|
316
|
+
error "504 Structure not implemented" unless implemented
|
317
|
+
@structure = argument
|
318
|
+
reply "200 File structure set to #{name}"
|
319
|
+
end
|
320
|
+
|
321
|
+
def cmd_noop(argument)
|
322
|
+
syntax_error if argument
|
323
|
+
reply "200 Nothing done"
|
324
|
+
end
|
325
|
+
|
326
|
+
def cmd_pasv(argument)
|
327
|
+
check_logged_in
|
328
|
+
if @data_server
|
329
|
+
reply "200 Already in passive mode"
|
330
|
+
else
|
331
|
+
@data_server = TCPServer.new('localhost', 0)
|
332
|
+
ip = @data_server.addr[3]
|
333
|
+
port = @data_server.addr[1]
|
334
|
+
quads = [
|
335
|
+
ip.scan(/\d+/),
|
336
|
+
port >> 8,
|
337
|
+
port & 0xff,
|
338
|
+
].flatten.join(',')
|
339
|
+
reply "227 Entering passive mode (#{quads})"
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
def cmd_cwd(argument)
|
344
|
+
check_logged_in
|
345
|
+
target = if argument =~ %r"^/(.*)$"
|
346
|
+
@data_path + $1
|
347
|
+
else
|
348
|
+
@cwd + argument
|
349
|
+
end
|
350
|
+
ensure_path_is_in_data_dir(target)
|
351
|
+
restore_cwd_on_error do
|
352
|
+
@cwd = target
|
353
|
+
pwd
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
def cmd_cdup(argument)
|
358
|
+
check_logged_in
|
359
|
+
cmd_cwd('..')
|
360
|
+
end
|
361
|
+
|
362
|
+
def cmd_pwd(argument)
|
363
|
+
check_logged_in
|
364
|
+
pwd
|
365
|
+
end
|
366
|
+
|
367
|
+
def cmd_auth(security_scheme)
|
368
|
+
if @socket.encrypted?
|
369
|
+
raise Error, "503 AUTH already done"
|
370
|
+
end
|
371
|
+
unless security_scheme =~ /^TLS(-C)?$/i
|
372
|
+
raise Error, "500 Security scheme not implemented: #{security_scheme}"
|
373
|
+
end
|
374
|
+
reply "234 AUTH #{security_scheme} OK."
|
375
|
+
@socket.encrypt
|
376
|
+
end
|
377
|
+
|
378
|
+
def cmd_pbsz(buffer_size)
|
379
|
+
syntax_error unless buffer_size =~ /^\d+$/
|
380
|
+
buffer_size = buffer_size.to_i
|
381
|
+
unless @socket.encrypted?
|
382
|
+
raise Error, "503 PBSZ must be preceded by AUTH"
|
383
|
+
end
|
384
|
+
unless buffer_size == 0
|
385
|
+
raise Error, "501 PBSZ=0"
|
386
|
+
end
|
387
|
+
reply "200 PBSZ=0"
|
388
|
+
@protection_buffer_size_set = true
|
389
|
+
end
|
390
|
+
|
391
|
+
def cmd_prot(level_arg)
|
392
|
+
level_code = level_arg.upcase
|
393
|
+
unless @protection_buffer_size_set
|
394
|
+
raise Error, "503 PROT must be preceded by PBSZ"
|
395
|
+
end
|
396
|
+
level = DATA_CHANNEL_PROTECTION_LEVELS[level_code]
|
397
|
+
unless level
|
398
|
+
raise Error, "504 Unknown protection level"
|
399
|
+
end
|
400
|
+
unless level == :private
|
401
|
+
raise Error, "536 Unsupported protection level #{level}"
|
402
|
+
end
|
403
|
+
@data_channel_protection_level = level
|
404
|
+
reply "200 Data protection level #{level_code}"
|
405
|
+
end
|
406
|
+
|
407
|
+
def pwd
|
408
|
+
reply %Q(257 "#{sanitized_cwd}" is current directory)
|
409
|
+
end
|
410
|
+
|
411
|
+
def relative_to_data_path(path)
|
412
|
+
data_path = realpath(@data_path).to_s
|
413
|
+
path = realpath(path).to_s
|
414
|
+
path = path.gsub(data_path, '')
|
415
|
+
path = '/' if path.empty?
|
416
|
+
path
|
417
|
+
end
|
418
|
+
|
419
|
+
def sanitized_cwd
|
420
|
+
relative_to_data_path(@cwd)
|
421
|
+
end
|
422
|
+
|
423
|
+
def error(message)
|
424
|
+
raise Error, message
|
425
|
+
end
|
426
|
+
|
427
|
+
TRANSMISSION_MODES = {
|
428
|
+
'B'=>['Block', false],
|
429
|
+
'C'=>['Compressed', false],
|
430
|
+
'S'=>['Stream', true],
|
431
|
+
}
|
432
|
+
|
433
|
+
FORMAT_TYPES = {
|
434
|
+
'N'=>['Non-print', true],
|
435
|
+
'T'=>['Telnet format effectors', false],
|
436
|
+
'C'=>['Carriage Control (ASA)', false],
|
437
|
+
}
|
438
|
+
|
439
|
+
DATA_TYPES = {
|
440
|
+
'A'=>['ASCII', true],
|
441
|
+
'E'=>['EBCDIC', false],
|
442
|
+
'I'=>['BINARY', true],
|
443
|
+
'L'=>['LOCAL', false],
|
444
|
+
}
|
445
|
+
|
446
|
+
FILE_STRUCTURES = {
|
447
|
+
'R'=>['Record', false],
|
448
|
+
'F'=>['File', true],
|
449
|
+
'P'=>['Page', false],
|
450
|
+
}
|
451
|
+
|
452
|
+
DATA_CHANNEL_PROTECTION_LEVELS = {
|
453
|
+
'C'=>:clear,
|
454
|
+
'S'=>:safe,
|
455
|
+
'E'=>:confidential,
|
456
|
+
'P'=>:private
|
457
|
+
}
|
458
|
+
|
459
|
+
def check_logged_in
|
460
|
+
return if @state == :logged_in
|
461
|
+
error "530 Not logged in"
|
462
|
+
end
|
463
|
+
|
464
|
+
def ensure_path_is_in_data_dir(path)
|
465
|
+
unless child_path_of?(@data_path, path)
|
466
|
+
error "550 Access denied"
|
467
|
+
end
|
468
|
+
end
|
469
|
+
|
470
|
+
def ensure_path_exists(path)
|
471
|
+
unless File.exists?(path)
|
472
|
+
error '450 No such file or directory'
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
def child_path_of?(parent, child)
|
477
|
+
child.cleanpath.to_s.index(parent.cleanpath.to_s) == 0
|
478
|
+
end
|
479
|
+
|
480
|
+
def target_path(path)
|
481
|
+
path = Pathname.new(path)
|
482
|
+
base, path = if path.to_s =~ /^\/(.*)/
|
483
|
+
[@data_path, $1]
|
484
|
+
else
|
485
|
+
[@cwd, path]
|
486
|
+
end
|
487
|
+
base + path
|
488
|
+
end
|
489
|
+
|
490
|
+
def read_file(path)
|
491
|
+
handle_system_error do
|
492
|
+
File.open(path, 'rb') do |file|
|
493
|
+
file.read
|
494
|
+
end
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
def write_file(dest, contents)
|
499
|
+
handle_system_error do
|
500
|
+
File.open(dest, 'w') do |file|
|
501
|
+
file.write(contents)
|
502
|
+
end
|
503
|
+
end
|
504
|
+
end
|
505
|
+
|
506
|
+
def handle_system_error
|
507
|
+
begin
|
508
|
+
yield
|
509
|
+
rescue SystemCallError => e
|
510
|
+
error "550 #{e}"
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
514
|
+
def transmit_file(contents, data_type = @data_type)
|
515
|
+
open_data_connection do |data_socket|
|
516
|
+
contents = unix_to_nvt_ascii(contents) if data_type == 'A'
|
517
|
+
data_socket.write(contents)
|
518
|
+
debug("Sent #{contents.size} bytes")
|
519
|
+
reply "226 Transfer complete"
|
520
|
+
end
|
521
|
+
end
|
522
|
+
|
523
|
+
def receive_file(path)
|
524
|
+
open_data_connection do |data_socket|
|
525
|
+
contents = data_socket.read
|
526
|
+
contents = nvt_ascii_to_unix(contents) if @data_type == 'A'
|
527
|
+
debug("Received #{contents.size} bytes")
|
528
|
+
contents
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
532
|
+
def unix_to_nvt_ascii(s)
|
533
|
+
return s if s =~ /\r\n/
|
534
|
+
s.gsub(/\n/, "\r\n")
|
535
|
+
end
|
536
|
+
|
537
|
+
def nvt_ascii_to_unix(s)
|
538
|
+
s.gsub(/\r\n/, "\n")
|
539
|
+
end
|
540
|
+
|
541
|
+
def open_data_connection(&block)
|
542
|
+
reply "150 Opening #{data_connection_description}"
|
543
|
+
if @data_server
|
544
|
+
if encrypt_data?
|
545
|
+
open_passive_tls_data_connection(&block)
|
546
|
+
else
|
547
|
+
open_passive_data_connection(&block)
|
548
|
+
end
|
549
|
+
else
|
550
|
+
if encrypt_data?
|
551
|
+
open_active_tls_data_connection(&block)
|
552
|
+
else
|
553
|
+
open_active_data_connection(&block)
|
554
|
+
end
|
555
|
+
end
|
556
|
+
end
|
557
|
+
|
558
|
+
def data_connection_description
|
559
|
+
[
|
560
|
+
DATA_TYPES[@data_type][0],
|
561
|
+
"mode data connection",
|
562
|
+
("(TLS)" if encrypt_data?)
|
563
|
+
].compact.join(' ')
|
564
|
+
end
|
565
|
+
|
566
|
+
def encrypt_data?
|
567
|
+
@data_channel_protection_level != :clear
|
568
|
+
end
|
569
|
+
|
570
|
+
def open_active_data_connection
|
571
|
+
data_socket = TCPSocket.new(@data_hostname, @data_port)
|
572
|
+
begin
|
573
|
+
yield(data_socket)
|
574
|
+
ensure
|
575
|
+
data_socket.close
|
576
|
+
end
|
577
|
+
end
|
578
|
+
|
579
|
+
def open_active_tls_data_connection
|
580
|
+
open_active_data_connection do |socket|
|
581
|
+
make_tls_connection(socket) do |ssl_socket|
|
582
|
+
yield(ssl_socket)
|
583
|
+
end
|
584
|
+
end
|
585
|
+
end
|
586
|
+
|
587
|
+
def open_passive_data_connection
|
588
|
+
data_socket = @data_server.accept
|
589
|
+
begin
|
590
|
+
yield(data_socket)
|
591
|
+
ensure
|
592
|
+
data_socket.close
|
593
|
+
end
|
594
|
+
end
|
595
|
+
|
596
|
+
def close_data_server_socket_when_done
|
597
|
+
yield
|
598
|
+
ensure
|
599
|
+
close_data_server_socket
|
600
|
+
end
|
601
|
+
|
602
|
+
def close_data_server_socket
|
603
|
+
return unless @data_server
|
604
|
+
@data_server.close
|
605
|
+
@data_server = nil
|
606
|
+
end
|
607
|
+
|
608
|
+
def open_passive_tls_data_connection
|
609
|
+
open_passive_data_connection do |socket|
|
610
|
+
make_tls_connection(socket) do |ssl_socket|
|
611
|
+
yield(ssl_socket)
|
612
|
+
end
|
613
|
+
end
|
614
|
+
end
|
615
|
+
|
616
|
+
def make_tls_connection(socket)
|
617
|
+
ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, @socket.ssl_context)
|
618
|
+
ssl_socket.accept
|
619
|
+
begin
|
620
|
+
yield(ssl_socket)
|
621
|
+
ensure
|
622
|
+
ssl_socket.close
|
623
|
+
end
|
624
|
+
end
|
625
|
+
|
626
|
+
def get_command
|
627
|
+
s = @socket.gets
|
628
|
+
throw :done if s.nil?
|
629
|
+
s = s.chomp
|
630
|
+
debug(s)
|
631
|
+
s
|
632
|
+
end
|
633
|
+
|
634
|
+
def reply(s)
|
635
|
+
if @response_delay.to_i != 0
|
636
|
+
debug "#{@response_delay} second delay before replying"
|
637
|
+
sleep @response_delay
|
638
|
+
end
|
639
|
+
debug(s)
|
640
|
+
@socket.puts(s)
|
641
|
+
end
|
642
|
+
|
643
|
+
def debug(*s)
|
644
|
+
return unless debug?
|
645
|
+
File.open(@debug_path, 'a') do |file|
|
646
|
+
file.puts(*s)
|
647
|
+
end
|
648
|
+
end
|
649
|
+
|
650
|
+
def debug?
|
651
|
+
ENV['DEBUG'].to_i != 0
|
652
|
+
end
|
653
|
+
|
654
|
+
def restore_cwd_on_error
|
655
|
+
orig_cwd = @cwd
|
656
|
+
yield
|
657
|
+
rescue
|
658
|
+
@cwd = orig_cwd
|
659
|
+
raise
|
660
|
+
end
|
661
|
+
|
662
|
+
end
|
663
|
+
|
664
|
+
end
|
665
|
+
|
666
|
+
class Scaffold
|
667
|
+
|
668
|
+
def initialize
|
669
|
+
@data_dir = TempDir.new
|
670
|
+
create_files
|
671
|
+
@server = FakeFtpServer.new(@data_dir.path)
|
672
|
+
set_credentials
|
673
|
+
display_connection_info
|
674
|
+
create_connection_script
|
675
|
+
end
|
676
|
+
|
677
|
+
def run
|
678
|
+
wait_until_stopped
|
679
|
+
end
|
680
|
+
|
681
|
+
private
|
682
|
+
|
683
|
+
HOST = 'localhost'
|
684
|
+
|
685
|
+
def create_files
|
686
|
+
[
|
687
|
+
'README',
|
688
|
+
'outgoing/getme',
|
689
|
+
].each do |path|
|
690
|
+
base_name = File.basename(path)
|
691
|
+
dir_name = File.dirname(path)
|
692
|
+
dir_path = File.join(@data_dir.path, dir_name)
|
693
|
+
file_path = File.join(dir_path, base_name)
|
694
|
+
FileUtils.mkdir_p(dir_path)
|
695
|
+
File.open(file_path, 'w') do |file|
|
696
|
+
file.puts "Contents of #{path}"
|
697
|
+
end
|
698
|
+
end
|
699
|
+
end
|
700
|
+
|
701
|
+
def set_credentials
|
702
|
+
@server.user = ENV['LOGNAME']
|
703
|
+
@server.password = ''
|
704
|
+
end
|
705
|
+
|
706
|
+
def display_connection_info
|
707
|
+
puts "Host: #{HOST}"
|
708
|
+
puts "Port: #{@server.port}"
|
709
|
+
puts "User: #{@server.user}"
|
710
|
+
puts "Pass: #{@server.password}"
|
711
|
+
puts "Directory: #{@data_dir.path}"
|
712
|
+
end
|
713
|
+
|
714
|
+
def create_connection_script
|
715
|
+
command_path = '/tmp/connect_to_fake_ftp_server.sh'
|
716
|
+
File.open(command_path, 'w') do |file|
|
717
|
+
file.puts "#!/bin/bash"
|
718
|
+
file.puts "ftp $FTP_ARGS #{HOST} #{@server.port}"
|
719
|
+
end
|
720
|
+
system("chmod +x #{command_path}")
|
721
|
+
puts "Connection script written to #{command_path}"
|
722
|
+
end
|
723
|
+
|
724
|
+
def wait_until_stopped
|
725
|
+
puts "FTP server started. Press ENTER or c-C to stop it"
|
726
|
+
$stdout.flush
|
727
|
+
begin
|
728
|
+
gets
|
729
|
+
rescue Interrupt
|
730
|
+
puts "Interrupt"
|
731
|
+
end
|
732
|
+
end
|
733
|
+
|
734
|
+
end
|
735
|
+
|
736
|
+
Scaffold.new.run if $0 == __FILE__
|