ftpproxy 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. data/COPYING +20 -0
  2. data/ChangeLog +16 -0
  3. data/README +9 -0
  4. data/bin/ftpproxy +44 -0
  5. data/lib/ftpproxy.rb +240 -0
  6. 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
+
@@ -0,0 +1,16 @@
1
+ == 1.2.0 / 2011-11-06
2
+
3
+ - Add packaging trappings for gem release.
4
+
5
+ == 1.1.0 / 2010-08-30
6
+
7
+ - Use correct address in log output.
8
+
9
+ == 1.0.1 / 2010-06-15
10
+
11
+ - Minor code cleanups.
12
+
13
+ == 1.0.0 / 2010-04-23
14
+
15
+ - Initial release
16
+
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
+
@@ -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
+
@@ -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
+