fizx-proxymachine 1.3.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/History.txt CHANGED
@@ -1,3 +1,27 @@
1
+ = 1.2.4 / 2011-02-01
2
+ * Bug fixes
3
+ * Fix version number in procline
4
+ * Unrequire rubygems. Some people consider this a bug.
5
+
6
+ = 1.2.3 / 2010-02-23
7
+ * Bug fixes
8
+ * Never retry after connection is established and data is sent
9
+
10
+ = 1.2.2 / 2010-02-19
11
+ * Bug fixes
12
+ * Bring back the buffer limit
13
+
14
+ = 1.2.1 / 2010-02-11
15
+ * Bug fixes
16
+ * Don't count client closes as connection errors
17
+
18
+ = 1.2.0 / 2010-02-09
19
+ * New Features
20
+ * Connection Errors and Timeouts
21
+ * Inactivity Timeouts
22
+ * Enhancements
23
+ * Better async retry logic
24
+
1
25
  = 1.1.0 / 2009-11-05
2
26
  * New Features
3
27
  * Add { :remote, :data, :reply } command [github.com/coderrr]
@@ -19,4 +43,4 @@
19
43
 
20
44
  = 0.2.7 / 2009-10-12
21
45
  * Minor changes
22
- * Use a 10k buffer to prevent memory growth due to slow clients
46
+ * Use a 10k buffer to prevent memory growth due to slow clients
data/README.md CHANGED
@@ -112,6 +112,48 @@ Valid return values
112
112
  `{ :close => true }` - Close the connection.
113
113
  `{ :close => String }` - Close the connection after sending the String.
114
114
 
115
+ Connection Errors and Timeouts
116
+ ------------------------------
117
+
118
+ It's possible to register a custom callback for handling connection
119
+ errors. The callback is passed the remote when a connection is either
120
+ rejected or a connection timeout occurs:
121
+
122
+ proxy do |data|
123
+ if data =~ /your thing/
124
+ { :remote => 'localhost:1234', :connect_timeout => 1.0 }
125
+ else
126
+ { :noop => true }
127
+ end
128
+ end
129
+
130
+ proxy_connect_error do |remote|
131
+ puts "error connecting to #{remote}"
132
+ end
133
+
134
+ You must provide a `:connect_timeout` value in the `proxy` return value
135
+ to enable connection timeouts. The `:connect_timeout` value is a float
136
+ representing the number of seconds to wait before a connection is
137
+ established. Hard connection rejections always trigger the callback, even
138
+ when no `:connect_timeout` is provided.
139
+
140
+ Inactivity Timeouts
141
+ -------------------
142
+
143
+ Inactivity timeouts work like connect timeouts but are triggered after
144
+ the configured amount of time elapses without receiving the first byte
145
+ of data from an already connected server:
146
+
147
+ proxy do |data|
148
+ { :remote => 'localhost:1234', :inactivity_timeout => 10.0 }
149
+ end
150
+
151
+ proxy_inactivity_error do |remote|
152
+ puts "#{remote} did not send any data for 10 seconds"
153
+ end
154
+
155
+ If no `:inactivity_timeout` is provided, the `proxy_inactivity_error`
156
+ callback is never triggered.
115
157
 
116
158
  Contribute
117
159
  ----------
data/Rakefile CHANGED
@@ -1,23 +1,50 @@
1
1
  require 'rubygems'
2
2
  require 'rake'
3
+ require 'date'
3
4
 
4
- begin
5
- require 'jeweler'
6
- Jeweler::Tasks.new do |gem|
7
- gem.name = "fizx-proxymachine"
8
- gem.summary = %Q{ProxyMachine is a simple content aware (layer 7) TCP routing proxy.}
9
- gem.email = "tom@mojombo.com"
10
- gem.homepage = "http://github.com/fizx/proxymachine"
11
- gem.authors = ["Tom Preston-Werner", "Kyle Maxwell"]
12
- gem.add_dependency('eventmachine', '>= 0.12.10')
13
-
14
- # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
15
- end
16
- Jeweler::GemcutterTasks.new
17
- rescue LoadError
18
- puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
5
+ #############################################################################
6
+ #
7
+ # Helper functions
8
+ #
9
+ #############################################################################
10
+
11
+ def name
12
+ @name ||= Dir['*.gemspec'].first.split('.').first
13
+ end
14
+
15
+ def version
16
+ line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/]
17
+ line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
18
+ end
19
+
20
+ def date
21
+ Date.today.to_s
19
22
  end
20
23
 
24
+ def rubyforge_project
25
+ name
26
+ end
27
+
28
+ def gemspec_file
29
+ "#{name}.gemspec"
30
+ end
31
+
32
+ def gem_file
33
+ "#{name}-#{version}.gem"
34
+ end
35
+
36
+ def replace_header(head, header_name)
37
+ head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"}
38
+ end
39
+
40
+ #############################################################################
41
+ #
42
+ # Standard tasks
43
+ #
44
+ #############################################################################
45
+
46
+ task :default => :test
47
+
21
48
  require 'rake/testtask'
22
49
  Rake::TestTask.new(:test) do |test|
23
50
  test.libs << 'lib' << 'test'
@@ -25,34 +52,99 @@ Rake::TestTask.new(:test) do |test|
25
52
  test.verbose = true
26
53
  end
27
54
 
28
- begin
29
- require 'rcov/rcovtask'
30
- Rcov::RcovTask.new do |test|
31
- test.libs << 'test'
32
- test.pattern = 'test/**/*_test.rb'
33
- test.verbose = true
34
- end
35
- rescue LoadError
36
- task :rcov do
37
- abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
38
- end
55
+ desc "Generate RCov test coverage and open in your browser"
56
+ task :coverage do
57
+ require 'rcov'
58
+ sh "rm -fr coverage"
59
+ sh "rcov test/test_*.rb"
60
+ sh "open coverage/index.html"
39
61
  end
40
62
 
41
-
42
- task :default => :test
43
-
44
63
  require 'rake/rdoctask'
45
64
  Rake::RDocTask.new do |rdoc|
46
- if File.exist?('VERSION.yml')
47
- config = YAML.load(File.read('VERSION.yml'))
48
- version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
49
- else
50
- version = ""
51
- end
52
-
53
65
  rdoc.rdoc_dir = 'rdoc'
54
- rdoc.title = "proxymachine #{version}"
66
+ rdoc.title = "#{name} #{version}"
55
67
  rdoc.rdoc_files.include('README*')
56
68
  rdoc.rdoc_files.include('lib/**/*.rb')
57
69
  end
58
70
 
71
+ desc "Open an irb session preloaded with this library"
72
+ task :console do
73
+ sh "irb -rubygems -r ./lib/#{name}.rb"
74
+ end
75
+
76
+ #############################################################################
77
+ #
78
+ # Custom tasks (add your own tasks here)
79
+ #
80
+ #############################################################################
81
+
82
+
83
+
84
+ #############################################################################
85
+ #
86
+ # Packaging tasks
87
+ #
88
+ #############################################################################
89
+
90
+ desc "Create tag v#{version} and build and push #{gem_file} to Rubygems"
91
+ task :release => :build do
92
+ unless `git branch` =~ /^\* master$/
93
+ puts "You must be on the master branch to release!"
94
+ exit!
95
+ end
96
+ sh "git commit --allow-empty -a -m 'Release #{version}'"
97
+ sh "git tag v#{version}"
98
+ sh "git push origin master"
99
+ sh "git push origin v#{version}"
100
+ sh "gem push pkg/#{name}-#{version}.gem"
101
+ end
102
+
103
+ desc "Build #{gem_file} into the pkg directory"
104
+ task :build => :gemspec do
105
+ sh "mkdir -p pkg"
106
+ sh "gem build #{gemspec_file}"
107
+ sh "mv #{gem_file} pkg"
108
+ end
109
+
110
+ desc "Generate #{gemspec_file}"
111
+ task :gemspec => :validate do
112
+ # read spec file and split out manifest section
113
+ spec = File.read(gemspec_file)
114
+ head, manifest, tail = spec.split(" # = MANIFEST =\n")
115
+
116
+ # replace name version and date
117
+ replace_header(head, :name)
118
+ replace_header(head, :version)
119
+ replace_header(head, :date)
120
+ #comment this out if your rubyforge_project has a different name
121
+ replace_header(head, :rubyforge_project)
122
+
123
+ # determine file list from git ls-files
124
+ files = `git ls-files`.
125
+ split("\n").
126
+ sort.
127
+ reject { |file| file =~ /^\./ }.
128
+ reject { |file| file =~ /^(rdoc|pkg)/ }.
129
+ map { |file| " #{file}" }.
130
+ join("\n")
131
+
132
+ # piece file back together and write
133
+ manifest = " s.files = %w[\n#{files}\n ]\n"
134
+ spec = [head, manifest, tail].join(" # = MANIFEST =\n")
135
+ File.open(gemspec_file, 'w') { |io| io.write(spec) }
136
+ puts "Updated #{gemspec_file}"
137
+ end
138
+
139
+ desc "Validate #{gemspec_file}"
140
+ task :validate do
141
+ libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"]
142
+ # unless libfiles.empty?
143
+ # puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir."
144
+ # exit!
145
+ # end
146
+ unless Dir['VERSION*'].empty?
147
+ puts "A `VERSION` file at root level violates Gem best practices."
148
+ exit!
149
+ end
150
+ end
@@ -1,69 +1,84 @@
1
- # Generated by jeweler
2
- # DO NOT EDIT THIS FILE DIRECTLY
3
- # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
- # -*- encoding: utf-8 -*-
5
-
1
+ ## This is the rakegem gemspec template. Make sure you read and understand
2
+ ## all of the comments. Some sections require modification, and others can
3
+ ## be deleted if you don't need them. Once you understand the contents of
4
+ ## this file, feel free to delete any comments that begin with two hash marks.
5
+ ## You can find comprehensive Gem::Specification documentation, at
6
+ ## http://docs.rubygems.org/read/chapter/20
6
7
  Gem::Specification.new do |s|
7
- s.name = %q{fizx-proxymachine}
8
- s.version = "1.3.0"
9
-
8
+ s.specification_version = 2 if s.respond_to? :specification_version=
10
9
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
- s.authors = ["Tom Preston-Werner", "Kyle Maxwell"]
12
- s.date = %q{2010-01-20}
13
- s.default_executable = %q{proxymachine}
14
- s.email = %q{tom@mojombo.com}
10
+ s.rubygems_version = '1.3.5'
11
+
12
+ ## Leave these as is they will be modified for you by the rake gemspec task.
13
+ ## If your rubyforge_project name is different, then edit it and comment out
14
+ ## the sub! line in the Rakefile
15
+ s.name = 'fizx-proxymachine'
16
+ s.version = '1.4.0'
17
+ s.date = '2011-05-07'
18
+ s.rubyforge_project = 'fizx-proxymachine'
19
+
20
+ ## Make sure your summary is short. The description may be as long
21
+ ## as you like.
22
+ s.summary = "ProxyMachine is a simple content aware (layer 7) TCP routing proxy."
23
+ s.description = "ProxyMachine is a simple content aware (layer 7) TCP routing proxy written in Ruby with EventMachine."
24
+
25
+ ## List the primary authors. If there are a bunch of authors, it's probably
26
+ ## better to set the email to an email list or something. If you don't have
27
+ ## a custom homepage, consider using your GitHub URL or the like.
28
+ s.authors = ["Tom Preston-Werner", "Kyle Maxwell"]
29
+ s.email = 'tom@mojombo.com'
30
+ s.homepage = 'http://github.com/fizx/proxymachine'
31
+
32
+ ## This gets added to the $LOAD_PATH so that 'lib/NAME.rb' can be required as
33
+ ## require 'NAME.rb' or'/lib/NAME/file.rb' can be as require 'NAME/file.rb'
34
+ s.require_paths = %w[lib]
35
+
36
+ ## If your gem includes any executables, list them here.
15
37
  s.executables = ["proxymachine"]
16
- s.extra_rdoc_files = [
17
- "LICENSE",
18
- "README.md"
19
- ]
20
- s.files = [
21
- ".document",
22
- ".gitignore",
23
- "History.txt",
24
- "LICENSE",
25
- "README.md",
26
- "Rakefile",
27
- "VERSION.yml",
28
- "bin/proxymachine",
29
- "examples/git.rb",
30
- "examples/long.rb",
31
- "examples/transparent.rb",
32
- "fizx-proxymachine.gemspec",
33
- "lib/fizx_proxymachine.rb",
34
- "lib/proxymachine.rb",
35
- "lib/proxymachine/callback_server_connection.rb",
36
- "lib/proxymachine/client_connection.rb",
37
- "lib/proxymachine/server_connection.rb",
38
- "test/configs/simple.rb",
39
- "test/proxymachine_test.rb",
40
- "test/test_helper.rb"
41
- ]
42
- s.homepage = %q{http://github.com/fizx/proxymachine}
38
+ s.default_executable = 'proxymachine'
39
+
40
+ ## Specify any RDoc options here. You'll want to add your README and
41
+ ## LICENSE files to the extra_rdoc_files list.
43
42
  s.rdoc_options = ["--charset=UTF-8"]
44
- s.require_paths = ["lib"]
45
- s.rubygems_version = %q{1.3.5}
46
- s.summary = %q{ProxyMachine is a simple content aware (layer 7) TCP routing proxy.}
47
- s.test_files = [
48
- "test/configs/simple.rb",
49
- "test/proxymachine_test.rb",
50
- "test/test_helper.rb",
51
- "examples/git.rb",
52
- "examples/long.rb",
53
- "examples/transparent.rb"
54
- ]
43
+ s.extra_rdoc_files = %w[README.md LICENSE]
44
+
45
+ ## List your runtime dependencies here. Runtime dependencies are those
46
+ ## that are needed for an end user to actually USE your code.
47
+ s.add_runtime_dependency(%q<eventmachine>, [">= 0.12.10"])
55
48
 
56
- if s.respond_to? :specification_version then
57
- current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
58
- s.specification_version = 3
49
+ ## List your development dependencies here. Development dependencies are
50
+ ## those that are only needed during development
51
+ s.add_development_dependency(%q<rake>, ["~> 0.8.7"])
52
+ s.add_development_dependency(%q<shoulda>, ["~> 2.11.3"])
53
+ s.add_development_dependency(%q<jeweler>, ["~> 1.5.2"])
59
54
 
60
- if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
61
- s.add_runtime_dependency(%q<eventmachine>, [">= 0.12.10"])
62
- else
63
- s.add_dependency(%q<eventmachine>, [">= 0.12.10"])
64
- end
65
- else
66
- s.add_dependency(%q<eventmachine>, [">= 0.12.10"])
67
- end
68
- end
55
+ ## Leave this section as-is. It will be automatically generated from the
56
+ ## contents of your Git repository via the gemspec task. DO NOT REMOVE
57
+ ## THE MANIFEST COMMENTS, they are used as delimiters by the task.
58
+ # = MANIFEST =
59
+ s.files = %w[
60
+ Gemfile
61
+ History.txt
62
+ LICENSE
63
+ README.md
64
+ Rakefile
65
+ bin/proxymachine
66
+ examples/git.rb
67
+ examples/long.rb
68
+ examples/transparent.rb
69
+ fizx-proxymachine.gemspec
70
+ lib/fizx-proxymachine.rb
71
+ lib/proxymachine.rb
72
+ lib/proxymachine/callback_server_connection.rb
73
+ lib/proxymachine/client_connection.rb
74
+ lib/proxymachine/server_connection.rb
75
+ test/configs/simple.rb
76
+ test/proxymachine_test.rb
77
+ test/test_helper.rb
78
+ ]
79
+ # = MANIFEST =
69
80
 
81
+ ## Test files will be grabbed from the file list. Make sure the path glob
82
+ ## matches what you actually use.
83
+ s.test_files = s.files.select { |path| path =~ /^test\/.*_test\.rb$/ }
84
+ end
@@ -0,0 +1,3 @@
1
+ require File.dirname(__FILE__) + "/proxymachine"
2
+
3
+ VERSION="1.4.0"
data/lib/proxymachine.rb CHANGED
@@ -1,4 +1,4 @@
1
- require 'rubygems'
1
+ require 'yaml'
2
2
  require 'eventmachine'
3
3
  require 'logger'
4
4
  require 'socket'
@@ -10,6 +10,8 @@ require 'proxymachine/callback_server_connection'
10
10
  $logger = Logger.new(STDOUT)
11
11
 
12
12
  class ProxyMachine
13
+ VERSION = '1.2.4'
14
+
13
15
  MAX_FAST_SHUTDOWN_SECONDS = 10
14
16
 
15
17
  def self.update_procline
@@ -71,12 +73,30 @@ class ProxyMachine
71
73
  end
72
74
  end
73
75
 
76
+ def self.set_connect_error_callback(&block)
77
+ @@connect_error_callback = block
78
+ end
79
+
80
+ def self.connect_error_callback
81
+ @@connect_error_callback
82
+ end
83
+
84
+ def self.set_inactivity_error_callback(&block)
85
+ @@inactivity_error_callback = block
86
+ end
87
+
88
+ def self.inactivity_error_callback
89
+ @@inactivity_error_callback
90
+ end
91
+
74
92
  def self.run(name, host, port)
75
93
  @@totalcounter = 0
76
94
  @@maxcounter = 0
77
95
  @@counter = 0
78
96
  @@name = name
79
97
  @@listen = "#{host}:#{port}"
98
+ @@connect_error_callback ||= proc { |remote| }
99
+ @@inactivity_error_callback ||= proc { |remote| }
80
100
  self.update_procline
81
101
  EM.epoll
82
102
 
@@ -93,19 +113,18 @@ class ProxyMachine
93
113
  end
94
114
  end
95
115
  end
96
-
97
- def self.version
98
- yml = YAML.load(File.read(File.join(File.dirname(__FILE__), *%w[.. VERSION.yml])))
99
- "#{yml[:major]}.#{yml[:minor]}.#{yml[:patch]}"
100
- rescue
101
- 'unknown'
102
- end
103
-
104
- VERSION = self.version
105
116
  end
106
117
 
107
118
  module Kernel
108
119
  def proxy(&block)
109
120
  ProxyMachine.set_router(block)
110
121
  end
111
- end
122
+
123
+ def proxy_connect_error(&block)
124
+ ProxyMachine.set_connect_error_callback(&block)
125
+ end
126
+
127
+ def proxy_inactivity_error(&block)
128
+ ProxyMachine.set_inactivity_error_callback(&block)
129
+ end
130
+ end
@@ -13,8 +13,8 @@ class ProxyMachine
13
13
  @buffer ||= []
14
14
  @buffer << data
15
15
  if returned = @callback.call(@buffer.join(''))
16
- proxy_incoming_to(@client_side, 10240)
17
16
  @client_side.send_data returned
17
+ proxy_incoming_to(@client_side, 10240)
18
18
  end
19
19
  rescue => e
20
20
  $logger.info e.message + e.backtrace.join("\n")
@@ -10,7 +10,11 @@ class ProxyMachine
10
10
  def post_init
11
11
  $logger.info "Accepted #{peer}"
12
12
  @buffer = []
13
+ @remote = nil
13
14
  @tries = 0
15
+ @connected = false
16
+ @connect_timeout = nil
17
+ @inactivity_timeout = nil
14
18
  ProxyMachine.incr
15
19
  end
16
20
 
@@ -23,76 +27,103 @@ class ProxyMachine
23
27
  end
24
28
 
25
29
  def receive_data(data)
26
- if !@server_side
30
+ if !@connected
27
31
  @buffer << data
28
- ensure_server_side_connection
32
+ establish_remote_server if @remote.nil?
29
33
  end
30
34
  rescue => e
31
35
  close_connection
32
- $logger.info "#{e.class} - #{e.message}"
36
+ $logger.error "#{e.class} - #{e.message}"
33
37
  end
34
38
 
35
- def ensure_server_side_connection
36
- @timer.cancel if @timer
37
- unless @server_side
38
- commands = ProxyMachine.router.call(@buffer.join)
39
- $logger.info "#{peer} #{commands.inspect}"
40
- close_connection unless commands.instance_of?(Hash)
41
- if remote = commands[:remote]
42
- m, host, port = *remote.match(/^(.+):(.+)$/)
43
- klass = commands[:callback] ? CallbackServerConnection : ServerConnection
44
- if try_server_connect(host, port.to_i, klass)
45
- @server_side.callback = commands[:callback] if commands[:callback]
46
-
47
- if data = commands[:data]
48
- @buffer = [data]
49
- end
50
- if reply = commands[:reply]
51
- send_data(reply)
52
- end
53
- send_and_clear_buffer
54
- end
55
- elsif close = commands[:close]
56
- if close == true
57
- close_connection
58
- else
59
- send_data(close)
60
- close_connection_after_writing
61
- end
62
- elsif commands[:noop]
63
- # do nothing
64
- else
39
+ # Called when new data is available from the client but no remote
40
+ # server has been established. If a remote can be established, an
41
+ # attempt is made to connect and proxy to the remote server.
42
+ def establish_remote_server
43
+ fail "establish_remote_server called with remote established" if @remote
44
+ @commands = ProxyMachine.router.call(@buffer.join)
45
+ $logger.info "#{peer} #{@commands.inspect}"
46
+ close_connection unless @commands.instance_of?(Hash)
47
+ if remote = @commands[:remote]
48
+ m, host, port = *remote.match(/^(.+):(.+)$/)
49
+ @remote = [host, port]
50
+ if data = @commands[:data]
51
+ @buffer = [data]
52
+ end
53
+ if reply = @commands[:reply]
54
+ send_data(reply)
55
+ end
56
+ @connect_timeout = @commands[:connect_timeout]
57
+ @inactivity_timeout = @commands[:inactivity_timeout]
58
+ connect_to_server
59
+ elsif close = @commands[:close]
60
+ if close == true
65
61
  close_connection
62
+ else
63
+ send_data(close)
64
+ close_connection_after_writing
66
65
  end
66
+ elsif @commands[:noop]
67
+ # do nothing
68
+ else
69
+ close_connection
67
70
  end
68
71
  end
69
72
 
70
- def try_server_connect(host, port, klass)
73
+ # Connect to the remote server
74
+ def connect_to_server
75
+ fail "connect_server called without remote established" if @remote.nil?
76
+ host, port = @remote
77
+ $logger.info "Establishing new connection with #{host}:#{port}"
78
+ cb = @commands[:callback]
79
+ klass = cb ? CallbackServerConnection : ServerConnection
71
80
  @server_side = klass.request(host, port, self)
81
+ @server_side.callback = cb if cb
82
+ @server_side.pending_connect_timeout = @connect_timeout
83
+ @server_side.comm_inactivity_timeout = @inactivity_timeout
84
+ end
85
+
86
+ # Called by the server side immediately after the server connection was
87
+ # successfully established. Send any buffer we've accumulated and start
88
+ # raw proxying.
89
+ def server_connection_success
90
+ $logger.info "Successful connection to #{@remote.join(':')}"
91
+ @connected = true
92
+ @buffer.each { |data| @server_side.send_data(data) }
93
+ @buffer = []
72
94
  proxy_incoming_to(@server_side, 10240)
73
- $logger.info "Successful connection to #{host}:#{port}."
74
- true
75
- rescue => e
76
- if @tries < 10
95
+ end
96
+
97
+ # Called by the server side when a connection could not be established,
98
+ # either due to a hard connection failure or to a connection timeout.
99
+ # Leave the client connection open and retry the server connection up to
100
+ # 10 times.
101
+ def server_connection_failed
102
+ @server_side = nil
103
+ if @connected
104
+ $logger.error "Connection with #{@remote.join(':')} was terminated prematurely."
105
+ close_connection
106
+ ProxyMachine.connect_error_callback.call(@remote.join(':'))
107
+ elsif @tries < 10
77
108
  @tries += 1
78
- $logger.info "Failed on server connect attempt #{@tries}. Trying again..."
79
- @timer.cancel if @timer
80
- @timer = EventMachine::Timer.new(0.1) do
81
- self.ensure_server_side_connection
82
- end
109
+ $logger.warn "Retrying connection with #{@remote.join(':')} (##{@tries})"
110
+ EM.add_timer(0.1) { connect_to_server }
83
111
  else
84
- $logger.info "Failed after ten connection attempts."
112
+ $logger.error "Connect #{@remote.join(':')} failed after ten attempts."
113
+ close_connection
114
+ ProxyMachine.connect_error_callback.call(@remote.join(':'))
85
115
  end
86
- false
87
116
  end
88
117
 
89
- def send_and_clear_buffer
90
- if !@buffer.empty?
91
- @buffer.each do |x|
92
- @server_side.send_data(x)
93
- end
94
- @buffer = []
95
- end
118
+ # Called by the server when an inactivity timeout is detected. The timeout
119
+ # argument is the configured inactivity timeout in seconds as a float; the
120
+ # elapsed argument is the amount of time that actually elapsed since
121
+ # connecting but not receiving any data.
122
+ def server_inactivity_timeout(timeout, elapsed)
123
+ $logger.error "Disconnecting #{@remote.join(':')} after #{elapsed}s of inactivity (> #{timeout.inspect})"
124
+ @server_side = nil
125
+ close_connection
126
+ ProxyMachine.inactivity_error_callback.call(@remote.join(':'))
96
127
  end
97
128
 
98
129
  def unbind
@@ -6,14 +6,45 @@ class ProxyMachine
6
6
 
7
7
  def initialize(conn)
8
8
  @client_side = conn
9
+ @connected = false
10
+ @data_received = false
11
+ @timeout = nil
9
12
  end
10
13
 
11
- def post_init
14
+ def receive_data(data)
15
+ fail "receive_data called after raw proxy enabled" if @data_received
16
+ @data_received = true
17
+ @client_side.send_data(data)
12
18
  proxy_incoming_to(@client_side, 10240)
13
19
  end
14
20
 
21
+ def connection_completed
22
+ @connected = Time.now
23
+ @timeout = comm_inactivity_timeout || 0.0
24
+ @client_side.server_connection_success
25
+ end
26
+
15
27
  def unbind
16
- @client_side.close_connection_after_writing
28
+ now = Time.now
29
+ if @client_side.error?
30
+ # the client side disconnected while we were in progress with
31
+ # the server. do nothing.
32
+ $logger.info "Client closed while server connection in progress. Dropping."
33
+ elsif !@connected
34
+ # a connection error or timeout occurred
35
+ @client_side.server_connection_failed
36
+ elsif !@data_received
37
+ if @timeout > 0.0 && (elapsed = now - @connected) >= @timeout
38
+ # EM aborted the connection due to an inactivity timeout
39
+ @client_side.server_inactivity_timeout(@timeout, elapsed)
40
+ else
41
+ # server disconnected soon after connecting without sending data
42
+ # treat this like a failed server connection
43
+ @client_side.server_connection_failed
44
+ end
45
+ else
46
+ @client_side.close_connection_after_writing
47
+ end
17
48
  end
18
49
  end
19
50
  end
@@ -21,7 +21,21 @@ proxy do |data|
21
21
  { :remote => "localhost:9980", :data => 'g2', :reply => 'g3-' }
22
22
  elsif data == 'h'
23
23
  { :remote => "localhost:9980", :callback => callback }
24
+ elsif data == 'connect reject'
25
+ { :remote => "localhost:9989" }
26
+ elsif data == 'inactivity'
27
+ { :remote => "localhost:9980", :data => 'sleep 3', :inactivity_timeout => 1 }
24
28
  else
25
29
  { :close => true }
26
30
  end
27
- end
31
+ end
32
+
33
+ ERROR_FILE = File.expand_path('../../proxy_error', __FILE__)
34
+
35
+ proxy_connect_error do |remote|
36
+ File.open(ERROR_FILE, 'wb') { |fd| fd.write("connect error: #{remote}") }
37
+ end
38
+
39
+ proxy_inactivity_error do |remote|
40
+ File.open(ERROR_FILE, 'wb') { |fd| fd.write("activity error: #{remote}") }
41
+ end
@@ -8,6 +8,14 @@ def assert_proxy(host, port, send, recv)
8
8
  end
9
9
 
10
10
  class ProxymachineTest < Test::Unit::TestCase
11
+ def setup
12
+ @proxy_error_file = "#{File.dirname(__FILE__)}/proxy_error"
13
+ end
14
+
15
+ def teardown
16
+ File.unlink(@proxy_error_file) rescue nil
17
+ end
18
+
11
19
  should "handle simple routing" do
12
20
  assert_proxy('localhost', 9990, 'a', '9980:a')
13
21
  assert_proxy('localhost', 9990, 'b', '9981:b')
@@ -44,4 +52,32 @@ class ProxymachineTest < Test::Unit::TestCase
44
52
  should "execute a callback" do
45
53
  assert_proxy('localhost', 9990, 'h', '9980:h:callback')
46
54
  end
55
+
56
+ should "call proxy_connect_error when a connection is rejected" do
57
+ sock = TCPSocket.new('localhost', 9990)
58
+ sock.write('connect reject')
59
+ sock.flush
60
+ assert_equal "", sock.read
61
+ sock.close
62
+ assert_equal "connect error: localhost:9989", File.read(@proxy_error_file)
63
+ end
64
+
65
+ should "call proxy_inactivity_error when initial read times out" do
66
+ sock = TCPSocket.new('localhost', 9990)
67
+ sent = Time.now
68
+ sock.write('inactivity')
69
+ sock.flush
70
+ assert_equal "", sock.read
71
+ assert_operator Time.now - sent, :>=, 1.0
72
+ assert_equal "activity error: localhost:9980", File.read(@proxy_error_file)
73
+ sock.close
74
+ end
75
+
76
+ should "not consider client disconnect a server error" do
77
+ sock = TCPSocket.new('localhost', 9990)
78
+ sock.write('inactivity')
79
+ sock.close
80
+ sleep 3.1
81
+ assert !File.exist?(@proxy_error_file)
82
+ end
47
83
  end
data/test/test_helper.rb CHANGED
@@ -17,6 +17,7 @@ module EventMachine
17
17
  end
18
18
 
19
19
  def receive_data(data)
20
+ sleep $1.to_f if data =~ /^sleep (.*)/
20
21
  send_data("#{@@port}:#{data}")
21
22
  close_connection_after_writing
22
23
  end
@@ -54,4 +55,4 @@ end
54
55
  EventMachine::Protocols::TestConnection.start('localhost', port)
55
56
  end
56
57
  end
57
- end
58
+ end
metadata CHANGED
@@ -1,7 +1,8 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fizx-proxymachine
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ prerelease:
5
+ version: 1.4.0
5
6
  platform: ruby
6
7
  authors:
7
8
  - Tom Preston-Werner
@@ -10,42 +11,74 @@ autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
13
 
13
- date: 2010-01-20 00:00:00 -08:00
14
+ date: 2011-05-07 00:00:00 -07:00
14
15
  default_executable: proxymachine
15
16
  dependencies:
16
17
  - !ruby/object:Gem::Dependency
17
18
  name: eventmachine
18
- type: :runtime
19
- version_requirement:
20
- version_requirements: !ruby/object:Gem::Requirement
19
+ prerelease: false
20
+ requirement: &id001 !ruby/object:Gem::Requirement
21
+ none: false
21
22
  requirements:
22
23
  - - ">="
23
24
  - !ruby/object:Gem::Version
24
25
  version: 0.12.10
25
- version:
26
- description:
26
+ type: :runtime
27
+ version_requirements: *id001
28
+ - !ruby/object:Gem::Dependency
29
+ name: rake
30
+ prerelease: false
31
+ requirement: &id002 !ruby/object:Gem::Requirement
32
+ none: false
33
+ requirements:
34
+ - - ~>
35
+ - !ruby/object:Gem::Version
36
+ version: 0.8.7
37
+ type: :development
38
+ version_requirements: *id002
39
+ - !ruby/object:Gem::Dependency
40
+ name: shoulda
41
+ prerelease: false
42
+ requirement: &id003 !ruby/object:Gem::Requirement
43
+ none: false
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: 2.11.3
48
+ type: :development
49
+ version_requirements: *id003
50
+ - !ruby/object:Gem::Dependency
51
+ name: jeweler
52
+ prerelease: false
53
+ requirement: &id004 !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ~>
57
+ - !ruby/object:Gem::Version
58
+ version: 1.5.2
59
+ type: :development
60
+ version_requirements: *id004
61
+ description: ProxyMachine is a simple content aware (layer 7) TCP routing proxy written in Ruby with EventMachine.
27
62
  email: tom@mojombo.com
28
63
  executables:
29
64
  - proxymachine
30
65
  extensions: []
31
66
 
32
67
  extra_rdoc_files:
33
- - LICENSE
34
68
  - README.md
69
+ - LICENSE
35
70
  files:
36
- - .document
37
- - .gitignore
71
+ - Gemfile
38
72
  - History.txt
39
73
  - LICENSE
40
74
  - README.md
41
75
  - Rakefile
42
- - VERSION.yml
43
76
  - bin/proxymachine
44
77
  - examples/git.rb
45
78
  - examples/long.rb
46
79
  - examples/transparent.rb
47
80
  - fizx-proxymachine.gemspec
48
- - lib/fizx_proxymachine.rb
81
+ - lib/fizx-proxymachine.rb
49
82
  - lib/proxymachine.rb
50
83
  - lib/proxymachine/callback_server_connection.rb
51
84
  - lib/proxymachine/client_connection.rb
@@ -63,28 +96,23 @@ rdoc_options:
63
96
  require_paths:
64
97
  - lib
65
98
  required_ruby_version: !ruby/object:Gem::Requirement
99
+ none: false
66
100
  requirements:
67
101
  - - ">="
68
102
  - !ruby/object:Gem::Version
69
103
  version: "0"
70
- version:
71
104
  required_rubygems_version: !ruby/object:Gem::Requirement
105
+ none: false
72
106
  requirements:
73
107
  - - ">="
74
108
  - !ruby/object:Gem::Version
75
109
  version: "0"
76
- version:
77
110
  requirements: []
78
111
 
79
- rubyforge_project:
80
- rubygems_version: 1.3.5
112
+ rubyforge_project: fizx-proxymachine
113
+ rubygems_version: 1.6.2
81
114
  signing_key:
82
- specification_version: 3
115
+ specification_version: 2
83
116
  summary: ProxyMachine is a simple content aware (layer 7) TCP routing proxy.
84
117
  test_files:
85
- - test/configs/simple.rb
86
118
  - test/proxymachine_test.rb
87
- - test/test_helper.rb
88
- - examples/git.rb
89
- - examples/long.rb
90
- - examples/transparent.rb
data/.document DELETED
@@ -1,5 +0,0 @@
1
- README.rdoc
2
- lib/**/*.rb
3
- bin/*
4
- features/**/*.feature
5
- LICENSE
data/.gitignore DELETED
@@ -1,5 +0,0 @@
1
- *.sw?
2
- .DS_Store
3
- coverage
4
- rdoc
5
- pkg
data/VERSION.yml DELETED
@@ -1,5 +0,0 @@
1
- ---
2
- :build:
3
- :major: 1
4
- :minor: 3
5
- :patch: 0
@@ -1 +0,0 @@
1
- require File.dirname(__FILE__) + "/proxymachine"