fizx-proxymachine 1.3.0 → 1.4.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.
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"