ftpproxy 1.2.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.
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
+