ftpproxy 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +20 -0
- data/ChangeLog +16 -0
- data/README +9 -0
- data/bin/ftpproxy +44 -0
- data/lib/ftpproxy.rb +240 -0
- metadata +77 -0
data/COPYING
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2007-2010 Charles Lowe
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
20
|
+
|
data/ChangeLog
ADDED
data/README
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
= Introduction
|
2
|
+
|
3
|
+
This project is a small, simple, transparent FTP proxy daemon providing support
|
4
|
+
for optional active/passive translation.
|
5
|
+
|
6
|
+
Support usage behind firewalls by chaining to a parent proxy. Useful when using
|
7
|
+
certain software that doesn't support passive mode (eg standard windows ftp
|
8
|
+
client).
|
9
|
+
|
data/bin/ftpproxy
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
#! /usr/bin/ruby
|
2
|
+
|
3
|
+
require 'ftpproxy'
|
4
|
+
require 'optparse'
|
5
|
+
|
6
|
+
opts = {:mode => :client, :port => 21, :proxy => nil, :log => STDERR, :level => Logger::INFO}
|
7
|
+
op = OptionParser.new do |op|
|
8
|
+
op.banner = "Usage: #{File.basename $0} [options]"
|
9
|
+
op.separator ''
|
10
|
+
op.on('-p', '--port=PORT', 'Port to listen on (default 21)') { |port| opts[:port] = Integer(port) }
|
11
|
+
op.on('-P', '--[no-]proxy=PROXY', 'Connect through ftp proxy (default direct)') { |proxy| opts[:proxy] = proxy }
|
12
|
+
op.on('-m', '--mode=active|passive', 'Mode for data connections (default client)') do |mode|
|
13
|
+
raise ArgumentError, 'invalid mode - %s' % mode unless %w[active passive client].include?(mode.downcase)
|
14
|
+
opts[:mode] = mode.downcase.to_sym
|
15
|
+
end
|
16
|
+
begin
|
17
|
+
Process.uid = Process.uid
|
18
|
+
rescue NotImplementedError
|
19
|
+
# windows...
|
20
|
+
else
|
21
|
+
op.on('-u', '--uid=UID', 'Switch uid after binding to port') { |uid| opts[:uid] = Integer(uid) }
|
22
|
+
op.on('-g', '--gid=GID', 'Switch gid after binding to port') { |gid| opts[:gid] = Integer(gid) }
|
23
|
+
end
|
24
|
+
op.separator ''
|
25
|
+
op.on('-l', '--log=FILE', 'Log to FILE (default is STDERR)') { |file| opts[:log] = file }
|
26
|
+
op.on('-v', '--[no-]verbose', 'Run verbosely') { |v| opts[:level] = v ? Logger::DEBUG : Logger::INFO }
|
27
|
+
op.on_tail('-h', '--help', 'Show this message') { puts op; exit }
|
28
|
+
end
|
29
|
+
|
30
|
+
unless op.parse(ARGV).empty?
|
31
|
+
puts 'Invalid trailing arguments.'
|
32
|
+
puts op
|
33
|
+
exit 1
|
34
|
+
end
|
35
|
+
|
36
|
+
log = Logger.new opts[:log]
|
37
|
+
log.progname = File.basename $0
|
38
|
+
log.level = opts.delete :level
|
39
|
+
opts[:log] = log
|
40
|
+
|
41
|
+
FtpProxy.new(opts).start
|
42
|
+
#FtpProxy.new(:mode => :active, :proxy => 'proxy.example.com').start
|
43
|
+
#FtpProxy.new(:mode => :passive).start
|
44
|
+
|
data/lib/ftpproxy.rb
ADDED
@@ -0,0 +1,240 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
class FtpProxy
|
5
|
+
VERSION = '1.2.0'
|
6
|
+
WHITELIST = %w[cwd cdup dele list mdtm mkd nlst pass pwd quit retr rmd rnfr rnto size stor syst type user xpwd]
|
7
|
+
|
8
|
+
# relay is used to proxy for the data channel
|
9
|
+
class Relay
|
10
|
+
def initialize left, right
|
11
|
+
@left, @right = left, right
|
12
|
+
end
|
13
|
+
|
14
|
+
def relay
|
15
|
+
# find out which way to relay
|
16
|
+
(src, ), dst, ignore = IO.select [@left, @right], nil, nil, 60000
|
17
|
+
raise 'timeout waiting for data' if !src
|
18
|
+
dst, = [@left, @right] - [src]
|
19
|
+
while data = src.read(8192)
|
20
|
+
dst.write data
|
21
|
+
end
|
22
|
+
close
|
23
|
+
end
|
24
|
+
|
25
|
+
def close
|
26
|
+
@left.close if @left and !@left.closed?
|
27
|
+
@right.close if @right and !@right.closed?
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class ActiveRelay < Relay
|
32
|
+
def initialize srv, right
|
33
|
+
@srv = srv
|
34
|
+
super nil, right
|
35
|
+
end
|
36
|
+
|
37
|
+
def relay
|
38
|
+
raise 'timeout waiting for connection' unless IO.select [@srv], nil, nil, 60000
|
39
|
+
@left = @srv.accept
|
40
|
+
@srv.close
|
41
|
+
super
|
42
|
+
end
|
43
|
+
|
44
|
+
def close
|
45
|
+
@srv.close if !@srv.closed?
|
46
|
+
super
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class Session
|
51
|
+
def initialize socket, params={}
|
52
|
+
@client = socket
|
53
|
+
@server = nil
|
54
|
+
@relay = nil
|
55
|
+
@log = params[:log]
|
56
|
+
@mode = params[:mode]
|
57
|
+
if @proxy = params[:proxy]
|
58
|
+
raise ArgumentError, 'malformed proxy' unless @proxy =~ /^(.*)(?::(\d+))?$/
|
59
|
+
@proxy = [$1, $2]
|
60
|
+
end
|
61
|
+
rescue
|
62
|
+
@log.error $!
|
63
|
+
raise
|
64
|
+
end
|
65
|
+
|
66
|
+
# handle multiline responses
|
67
|
+
def read_server
|
68
|
+
data = @server.readline
|
69
|
+
if data[3] == ?-
|
70
|
+
code = data[0, 3]
|
71
|
+
begin
|
72
|
+
line = @server.readline
|
73
|
+
data << line
|
74
|
+
end until line[0, 3] == code and line[3] != ?-
|
75
|
+
end
|
76
|
+
@log.debug "@SERVER>> #{data.inspect}"
|
77
|
+
data.gsub(/\r\n|\r/, "\n")
|
78
|
+
end
|
79
|
+
|
80
|
+
def read_client
|
81
|
+
line = @client.gets
|
82
|
+
@log.debug "@CLIENT>> #{line.inspect}"
|
83
|
+
line
|
84
|
+
end
|
85
|
+
|
86
|
+
def write_server str
|
87
|
+
@log.debug "@SERVER<< #{str.inspect}"
|
88
|
+
@server.write str + "\r\n"
|
89
|
+
end
|
90
|
+
|
91
|
+
def write_client str
|
92
|
+
@log.debug "@CLIENT<< #{str.inspect}"
|
93
|
+
@client.write str + "\r\n"
|
94
|
+
end
|
95
|
+
|
96
|
+
def start
|
97
|
+
write_client "220 #{Socket.gethostname} (#{File.basename $0}) #{Time.now.strftime '%a, %d %b %Y %H:%M:%S'}"
|
98
|
+
while line = read_client
|
99
|
+
line.chomp!
|
100
|
+
cmd, params = line.split ' ', 2
|
101
|
+
cmd = cmd.downcase
|
102
|
+
params = nil if params == ''
|
103
|
+
msg = "cmd_#{cmd}"
|
104
|
+
if @server and WHITELIST.include?(cmd)
|
105
|
+
proxy line
|
106
|
+
elsif cmd == 'user' or @server && respond_to?(msg)
|
107
|
+
send msg, params
|
108
|
+
elsif WHITELIST.include?(cmd) or respond_to?(msg)
|
109
|
+
write_client '530 Not logged in.'
|
110
|
+
else
|
111
|
+
write_client '500 Syntax error, command unrecognized.'
|
112
|
+
end
|
113
|
+
end
|
114
|
+
rescue Errno::EINVAL
|
115
|
+
# common, when client just aborts
|
116
|
+
rescue
|
117
|
+
@log.warn $!
|
118
|
+
ensure
|
119
|
+
@client.close if !@client.closed?
|
120
|
+
@server.close if @server and !@server.closed?
|
121
|
+
@relay.close if @relay
|
122
|
+
end
|
123
|
+
|
124
|
+
def proxy line
|
125
|
+
write_server line
|
126
|
+
if %w[125 150].include? relay_cmd[0, 3]
|
127
|
+
begin
|
128
|
+
# relay data channel
|
129
|
+
@relay.relay
|
130
|
+
rescue
|
131
|
+
@log.error $!
|
132
|
+
write_client '425 Data connection failed'
|
133
|
+
else
|
134
|
+
relay_cmd
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# relay command channel
|
140
|
+
def relay_cmd
|
141
|
+
resp = read_server
|
142
|
+
resp.each { |line| write_client line.chomp }
|
143
|
+
resp
|
144
|
+
end
|
145
|
+
|
146
|
+
def cmd_user user
|
147
|
+
raise 'malformed user string' unless user =~ /(.+)@(.+?)(?::(\d+))?$/
|
148
|
+
user, host, port = $1, $2, $3
|
149
|
+
# this is to double-proxy through an upstream proxy
|
150
|
+
if @proxy
|
151
|
+
user, pass = "#{user}@#{host}#{':' + port.to_s if port}", pass
|
152
|
+
host, port = @proxy
|
153
|
+
end
|
154
|
+
@log.debug "Connecting to #{host}:#{port || 21}"
|
155
|
+
@server = TCPSocket.open host, (port || '21').to_i
|
156
|
+
raise 'invalid server response' unless read_server[0] == ?2
|
157
|
+
write_server "USER #{user}"
|
158
|
+
rescue
|
159
|
+
@log.error $!
|
160
|
+
write_client '530 Not logged in.'
|
161
|
+
else
|
162
|
+
relay_cmd
|
163
|
+
end
|
164
|
+
|
165
|
+
def cmd_port params
|
166
|
+
nums = params.split(',')
|
167
|
+
raise 'invalid parameters' unless nums.length == 6
|
168
|
+
host = nums[0, 4].join('.')
|
169
|
+
port = nums[4].to_i * 256 + nums[5].to_i
|
170
|
+
@log.debug "Opening active connection to client on #{host}:#{port}"
|
171
|
+
socket = TCPSocket.open(host, port)
|
172
|
+
write_client "200 Connection established (#{port})"
|
173
|
+
make_relay socket, :active
|
174
|
+
rescue
|
175
|
+
@log.error $!
|
176
|
+
write_client '425 Data connection failed'
|
177
|
+
end
|
178
|
+
|
179
|
+
def cmd_pasv params
|
180
|
+
socket = TCPServer.open @server.addr[3], 0
|
181
|
+
host = socket.addr[3]
|
182
|
+
port = socket.addr[1]
|
183
|
+
@log.debug "Waiting for passive connection from client on #{host}:#{port}"
|
184
|
+
write_client "227 Entering Passive Mode (#{(host.split('.') + port.divmod(256)).join ','})"
|
185
|
+
make_relay socket.accept, :passive
|
186
|
+
rescue
|
187
|
+
@log.error $!
|
188
|
+
write_client '425 Data connection failed'
|
189
|
+
end
|
190
|
+
|
191
|
+
def make_relay clientdata, mode
|
192
|
+
@relay.close if @relay
|
193
|
+
case mode = @mode || mode
|
194
|
+
when :active
|
195
|
+
socket = TCPServer.open @server.addr[3], 0
|
196
|
+
host = socket.addr[3]
|
197
|
+
port = socket.addr[1]
|
198
|
+
@log.debug "Waiting for active connection from server on #{host}:#{port}"
|
199
|
+
write_server "PORT #{(host.split('.') + port.divmod(256)).join ','}"
|
200
|
+
raise 'invalid server response' unless read_server[0] == ?2
|
201
|
+
@relay = ActiveRelay.new socket, clientdata
|
202
|
+
when :passive
|
203
|
+
write_server 'PASV'
|
204
|
+
raise 'invalid server response' unless nums = read_server[/^227[^(]*\(([^)]*)\)/, 1]
|
205
|
+
nums = nums.split(',')
|
206
|
+
raise 'invalid server response' unless nums.length == 6
|
207
|
+
host = nums[0, 4].join('.')
|
208
|
+
port = nums[4].to_i * 256 + nums[5].to_i
|
209
|
+
@log.debug "Opening passive connection to server on #{host}:#{port}"
|
210
|
+
@relay = Relay.new TCPSocket.new(host, port), clientdata
|
211
|
+
else
|
212
|
+
raise 'unhandled server data channel mode - %p' % mode
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def initialize params={}
|
218
|
+
params = {:mode => :client, :port => 21, :proxy => nil}.merge(params)
|
219
|
+
params[:mode] = nil if params[:mode] == :client
|
220
|
+
@params = params
|
221
|
+
unless @log = params[:log]
|
222
|
+
@log = @params[:log] = Logger.new(STDERR)
|
223
|
+
end
|
224
|
+
@log.info "Starting ftpproxy v#{VERSION} on #{Socket.gethostname}:#{params[:port]}"
|
225
|
+
@server = TCPServer.new '', params[:port]
|
226
|
+
Process.egid = params[:gid] if params[:gid]
|
227
|
+
Process.euid = params[:uid] if params[:uid]
|
228
|
+
end
|
229
|
+
|
230
|
+
def start
|
231
|
+
threads = []
|
232
|
+
@log.debug 'Waiting for connections'
|
233
|
+
while socket = @server.accept
|
234
|
+
@log.debug "Accepted connection from #{socket.peeraddr.join(', ')}"
|
235
|
+
threads << Thread.new { Session.new(socket, @params).start }
|
236
|
+
end
|
237
|
+
threads.each { |t| t.join }
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
metadata
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ftpproxy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 31
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 2
|
9
|
+
- 0
|
10
|
+
version: 1.2.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Charles Lowe
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-11-06 00:00:00 -04:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: Thin and simple FTP proxy providing support for active/passive translation.
|
23
|
+
email: aquasync@gmail.com
|
24
|
+
executables:
|
25
|
+
- ftpproxy
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files:
|
29
|
+
- README
|
30
|
+
- ChangeLog
|
31
|
+
files:
|
32
|
+
- README
|
33
|
+
- COPYING
|
34
|
+
- ChangeLog
|
35
|
+
- bin/ftpproxy
|
36
|
+
- lib/ftpproxy.rb
|
37
|
+
has_rdoc: true
|
38
|
+
homepage: http://github.com/aquasync/ftpproxy
|
39
|
+
licenses: []
|
40
|
+
|
41
|
+
post_install_message:
|
42
|
+
rdoc_options:
|
43
|
+
- --main
|
44
|
+
- README
|
45
|
+
- --title
|
46
|
+
- ftpproxy documentation
|
47
|
+
- --tab-width
|
48
|
+
- "2"
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
hash: 3
|
57
|
+
segments:
|
58
|
+
- 0
|
59
|
+
version: "0"
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
hash: 3
|
66
|
+
segments:
|
67
|
+
- 0
|
68
|
+
version: "0"
|
69
|
+
requirements: []
|
70
|
+
|
71
|
+
rubyforge_project:
|
72
|
+
rubygems_version: 1.3.7
|
73
|
+
signing_key:
|
74
|
+
specification_version: 3
|
75
|
+
summary: FTP proxy daemon.
|
76
|
+
test_files: []
|
77
|
+
|