fizx-proxymachine 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.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
data/History.txt ADDED
@@ -0,0 +1,22 @@
1
+ = 1.1.0 / 2009-11-05
2
+ * New Features
3
+ * Add { :remote, :data, :reply } command [github.com/coderrr]
4
+ * Minor Changes
5
+ * Namespace connection classes under ProxyMachine instead of EM [github.com/cmelbye]
6
+ * Require socket [github.com/cmelbye]
7
+ * Up EM dep to 0.12.10
8
+ * Add SOCKS4 Proxy example [github.com/coderrr]
9
+
10
+ = 1.0.0 / 2009-10-19
11
+ * No changes. Production ready!
12
+
13
+ = 0.2.8 / 2009-10-14
14
+ * Minor changes
15
+ * Always log proxy connection
16
+ * Add version and total connection count to procline
17
+ * Add max connection count to procline
18
+ * Use Logger for logging
19
+
20
+ = 0.2.7 / 2009-10-12
21
+ * Minor changes
22
+ * Use a 10k buffer to prevent memory growth due to slow clients
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Tom Preston-Werner
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,140 @@
1
+ ProxyMachine
2
+ ============
3
+
4
+ By Tom Preston-Werner (tom@mojombo.com)
5
+
6
+
7
+ Description
8
+ -----------
9
+
10
+ ProxyMachine is a simple content aware (layer 7) TCP routing proxy built on
11
+ EventMachine that lets you configure the routing logic in Ruby.
12
+
13
+ If you need to proxy connections to different backend servers depending on the
14
+ contents of the transmission, then ProxyMachine will make your life easy!
15
+
16
+ The idea here is simple. For each client connection, start receiving data
17
+ chunks and placing them into a buffer. Each time a new chunk arrives, send the
18
+ buffer to a user specified block. The block's job is to parse the buffer to
19
+ determine where the connection should be proxied. If the buffer contains
20
+ enough data to make a determination, the block returns the address and port of
21
+ the correct backend server. If not, it can choose to do nothing and wait for
22
+ more data to arrive, close the connection, or close the connection after
23
+ sending custom data. Once the block returns an address, a connection to the
24
+ backend is made, the buffer is replayed to the backend, and the client and
25
+ backend connections are hooked up to form a transparent proxy. This
26
+ bidirectional proxy continues to exist until either the client or backend
27
+ close the connection.
28
+
29
+ ProxyMachine was developed for GitHub's federated architecture and is
30
+ successfully used in production to proxy millions of requests every day. The
31
+ performance and memory profile have both proven to be excellent.
32
+
33
+
34
+ Installation
35
+ ------------
36
+
37
+ $ gem install proxymachine -s http://gemcutter.org
38
+
39
+
40
+ Running
41
+ -------
42
+
43
+ Usage:
44
+ proxymachine -c <config file> [-h <host>] [-p <port>]
45
+
46
+ Options:
47
+ -c, --config CONFIG Configuration file
48
+ -h, --host HOST Hostname to bind. Default 0.0.0.0
49
+ -p, --port PORT Port to listen on. Default 5432
50
+
51
+
52
+ Signals
53
+ -------
54
+
55
+ QUIT - Graceful shutdown. Stop accepting connections immediately and
56
+ wait as long as necessary for all connections to close.
57
+
58
+ TERM - Fast shutdown. Stop accepting connections immediately and wait
59
+ up to 10 seconds for connections to close before forcing
60
+ termination.
61
+
62
+ INT - Same as TERM
63
+
64
+
65
+ Example routing config file
66
+ ---------------------------
67
+
68
+ class GitRouter
69
+ # Look at the routing table and return the correct address for +name+
70
+ # Returns "<host>:<port>" e.g. "ae8f31c.example.com:9418"
71
+ def self.lookup(name)
72
+ ...
73
+ end
74
+ end
75
+
76
+ # Perform content-aware routing based on the stream data. Here, the
77
+ # header information from the Git protocol is parsed to find the
78
+ # username and a lookup routine is run on the name to find the correct
79
+ # backend server. If no match can be made yet, do nothing with the
80
+ # connection.
81
+ proxy do |data|
82
+ if data =~ %r{^....git-upload-pack /([\w\.\-]+)/[\w\.\-]+\000host=\w+\000}
83
+ name = $1
84
+ { :remote => GitRouter.lookup(name) }
85
+ else
86
+ { :noop => true }
87
+ end
88
+ end
89
+
90
+
91
+ Example SOCKS4 Proxy in 7 Lines
92
+ -------------------------------
93
+
94
+ proxy do |data|
95
+ return if data.size < 9
96
+ v, c, port, o1, o2, o3, o4, user = data.unpack("CCnC4a*")
97
+ return { :close => "\0\x5b\0\0\0\0\0\0" } if v != 4 or c != 1
98
+ return if ! idx = user.index("\0")
99
+ { :remote => "#{[o1,o2,o3,o4]*'.'}:#{port}",
100
+ :reply => "\0\x5a\0\0\0\0\0\0",
101
+ :data => data[idx+9..-1] }
102
+ end
103
+
104
+
105
+ Valid return values
106
+ -------------------
107
+
108
+ `{ :remote => String }` - String is the host:port of the backend server that will be proxied.
109
+ `{ :remote => String, :data => String }` - Same as above, but send the given data instead.
110
+ `{ :remote => String, :data => String, :reply => String}` - Same as above, but reply with given data back to the client
111
+ `{ :noop => true }` - Do nothing.
112
+ `{ :close => true }` - Close the connection.
113
+ `{ :close => String }` - Close the connection after sending the String.
114
+
115
+
116
+ Contribute
117
+ ----------
118
+
119
+ If you'd like to hack on ProxyMachine, start by forking my repo on GitHub:
120
+
121
+ http://github.com/mojombo/proxymachine
122
+
123
+ To get all of the dependencies, install the gem first. The best way to get
124
+ your changes merged back into core is as follows:
125
+
126
+ 1. Clone down your fork
127
+ 1. Create a topic branch to contain your change
128
+ 1. Hack away
129
+ 1. Add tests and make sure everything still passes by running `rake`
130
+ 1. If you are adding new functionality, document it in the README.md
131
+ 1. Do not change the version number, I will do that on my end
132
+ 1. If necessary, rebase your commits into logical chunks, without errors
133
+ 1. Push the branch up to GitHub
134
+ 1. Send me (mojombo) a pull request for your branch
135
+
136
+
137
+ Copyright
138
+ ---------
139
+
140
+ Copyright (c) 2009 Tom Preston-Werner. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,58 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
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"
19
+ end
20
+
21
+ require 'rake/testtask'
22
+ Rake::TestTask.new(:test) do |test|
23
+ test.libs << 'lib' << 'test'
24
+ test.pattern = 'test/**/*_test.rb'
25
+ test.verbose = true
26
+ end
27
+
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
39
+ end
40
+
41
+
42
+ task :default => :test
43
+
44
+ require 'rake/rdoctask'
45
+ 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
+ rdoc.rdoc_dir = 'rdoc'
54
+ rdoc.title = "proxymachine #{version}"
55
+ rdoc.rdoc_files.include('README*')
56
+ rdoc.rdoc_files.include('lib/**/*.rb')
57
+ end
58
+
data/VERSION.yml ADDED
@@ -0,0 +1,5 @@
1
+ ---
2
+ :build:
3
+ :major: 1
4
+ :minor: 2
5
+ :patch: 0
data/bin/proxymachine ADDED
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
4
+
5
+ require 'optparse'
6
+ require 'proxymachine'
7
+
8
+ begin
9
+ options = {:host => "0.0.0.0", :port => 5432}
10
+
11
+ opts = OptionParser.new do |opts|
12
+ opts.banner = <<-EOF
13
+ Usage:
14
+ proxymachine -c <config file> [-h <host>] [-p <port>]
15
+
16
+ Options:
17
+ EOF
18
+
19
+ opts.on("-cCONFIG", "--config CONFIG", "Configuration file") do |x|
20
+ options[:config] = x
21
+ end
22
+
23
+ opts.on("-hHOST", "--host HOST", "Hostname to bind. Default 0.0.0.0") do |x|
24
+ options[:host] = x
25
+ end
26
+
27
+ opts.on("-pPORT", "--port PORT", "Port to listen on. Default 5432") do |x|
28
+ options[:port] = x
29
+ end
30
+ end
31
+
32
+ opts.parse!
33
+
34
+ load(options[:config])
35
+ name = options[:config].split('/').last.chomp('.rb')
36
+ ProxyMachine.run(name, options[:host], options[:port])
37
+ rescue Exception => e
38
+ if e.instance_of?(SystemExit)
39
+ raise
40
+ else
41
+ LOGGER.info 'Uncaught exception'
42
+ LOGGER.info e.message
43
+ LOGGER.info e.backtrace.join("\n")
44
+ end
45
+ end
data/examples/git.rb ADDED
@@ -0,0 +1,26 @@
1
+ # This is a config file for ProxyMachine. It pulls the username out of
2
+ # the Git stream and can proxy to different locations based on that value
3
+ # Run with `proxymachine -c examples/git.rb`
4
+
5
+ class GitRouter
6
+ # Look at the routing table and return the correct address for +name+
7
+ # Returns "<host>:<port>" e.g. "ae8f31c.example.com:9418"
8
+ def self.lookup(name)
9
+ LOGGER.info "Proxying for user #{name}"
10
+ "localhost:9418"
11
+ end
12
+ end
13
+
14
+ # Perform content-aware routing based on the stream data. Here, the
15
+ # header information from the Git protocol is parsed to find the
16
+ # username and a lookup routine is run on the name to find the correct
17
+ # backend server. If no match can be made yet, do nothing with the
18
+ # connection yet.
19
+ proxy do |data|
20
+ if data =~ %r{^....git-upload-pack /([\w\.\-]+)/[\w\.\-]+\000host=(.+)\000}
21
+ name, host = $1, $2
22
+ { :remote => GitRouter.lookup(name) }
23
+ else
24
+ { :noop => true }
25
+ end
26
+ end
data/examples/long.rb ADDED
@@ -0,0 +1,9 @@
1
+ # To try out the graceful exit via SIGQUIT, start up a proxymachine with this
2
+ # configuration, and run the following curl command a few times:
3
+ # curl http://localhost:5432/ubuntu-releases/9.10/ubuntu-9.10-beta-alternate-amd64.iso \
4
+ # -H "Host: mirrors.cat.pdx.edu" > /dev/null
5
+ # Then send a SIGQUIT to the process and stop the long downloads one by one.
6
+
7
+ proxy do |data|
8
+ { :remote => "mirrors.cat.pdx.edu:80" }
9
+ end
@@ -0,0 +1,4 @@
1
+ proxy do |data|
2
+ # p data
3
+ { :remote => "google.com:80" }
4
+ end
@@ -0,0 +1,69 @@
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
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{fizx-proxymachine}
8
+ s.version = "1.2.0"
9
+
10
+ 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-10}
13
+ s.default_executable = %q{proxymachine}
14
+ s.email = %q{tom@mojombo.com}
15
+ 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}
43
+ 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
+ ]
55
+
56
+ if s.respond_to? :specification_version then
57
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
58
+ s.specification_version = 3
59
+
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
69
+
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + "/proxymachine"
@@ -0,0 +1,111 @@
1
+ require 'rubygems'
2
+ require 'eventmachine'
3
+ require 'logger'
4
+ require 'socket'
5
+
6
+ require 'proxymachine/client_connection'
7
+ require 'proxymachine/server_connection'
8
+ require 'proxymachine/callback_server_connection'
9
+
10
+ LOGGER = Logger.new(STDOUT)
11
+
12
+ class ProxyMachine
13
+ MAX_FAST_SHUTDOWN_SECONDS = 10
14
+
15
+ def self.update_procline
16
+ $0 = "proxymachine #{VERSION} - #{@@name} #{@@listen} - #{self.stats} cur/max/tot conns"
17
+ end
18
+
19
+ def self.stats
20
+ "#{@@counter}/#{@@maxcounter}/#{@@totalcounter}"
21
+ end
22
+
23
+ def self.count
24
+ @@counter
25
+ end
26
+
27
+ def self.incr
28
+ @@totalcounter += 1
29
+ @@counter += 1
30
+ @@maxcounter = @@counter if @@counter > @@maxcounter
31
+ self.update_procline
32
+ @@counter
33
+ end
34
+
35
+ def self.decr
36
+ @@counter -= 1
37
+ if $server.nil?
38
+ LOGGER.info "Waiting for #{@@counter} connections to finish."
39
+ end
40
+ self.update_procline
41
+ EM.stop if $server.nil? and @@counter == 0
42
+ @@counter
43
+ end
44
+
45
+ def self.set_router(block)
46
+ @@router = block
47
+ end
48
+
49
+ def self.router
50
+ @@router
51
+ end
52
+
53
+ def self.graceful_shutdown(signal)
54
+ EM.stop_server($server) if $server
55
+ LOGGER.info "Received #{signal} signal. No longer accepting new connections."
56
+ LOGGER.info "Waiting for #{ProxyMachine.count} connections to finish."
57
+ $server = nil
58
+ EM.stop if ProxyMachine.count == 0
59
+ end
60
+
61
+ def self.fast_shutdown(signal)
62
+ EM.stop_server($server) if $server
63
+ LOGGER.info "Received #{signal} signal. No longer accepting new connections."
64
+ LOGGER.info "Maximum time to wait for connections is #{MAX_FAST_SHUTDOWN_SECONDS} seconds."
65
+ LOGGER.info "Waiting for #{ProxyMachine.count} connections to finish."
66
+ $server = nil
67
+ EM.stop if ProxyMachine.count == 0
68
+ Thread.new do
69
+ sleep MAX_FAST_SHUTDOWN_SECONDS
70
+ exit!
71
+ end
72
+ end
73
+
74
+ def self.run(name, host, port)
75
+ @@totalcounter = 0
76
+ @@maxcounter = 0
77
+ @@counter = 0
78
+ @@name = name
79
+ @@listen = "#{host}:#{port}"
80
+ self.update_procline
81
+ EM.epoll
82
+
83
+ EM.run do
84
+ ProxyMachine::ClientConnection.start(host, port)
85
+ trap('QUIT') do
86
+ self.graceful_shutdown('QUIT')
87
+ end
88
+ trap('TERM') do
89
+ self.fast_shutdown('TERM')
90
+ end
91
+ trap('INT') do
92
+ self.fast_shutdown('INT')
93
+ end
94
+ end
95
+ 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
+ end
106
+
107
+ module Kernel
108
+ def proxy(&block)
109
+ ProxyMachine.set_router(block)
110
+ end
111
+ end
@@ -0,0 +1,27 @@
1
+ class ProxyMachine
2
+ class CallbackServerConnection < ServerConnection
3
+
4
+ def post_init
5
+ # empty
6
+ end
7
+
8
+ def callback=(c)
9
+ @callback = c
10
+ end
11
+
12
+ def receive_data(data)
13
+ @buffer ||= []
14
+ @buffer << data
15
+ if returned = @callback.call(@buffer.join(''))
16
+ proxy_incoming_to(@client_side, 10240)
17
+ @client_side.send_data returned
18
+ end
19
+ rescue => e
20
+ LOGGER.info e.message + e.backtrace.join("\n")
21
+ end
22
+
23
+ def unbind
24
+ @client_side.close_connection_after_writing
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,108 @@
1
+ class ProxyMachine
2
+ class ClientConnection < EventMachine::Connection
3
+ def self.start(host, port)
4
+ $server = EM.start_server(host, port, self)
5
+ LOGGER.info "Listening on #{host}:#{port}"
6
+ LOGGER.info "Send QUIT to quit after waiting for all connections to finish."
7
+ LOGGER.info "Send TERM or INT to quit after waiting for up to 10 seconds for connections to finish."
8
+ end
9
+
10
+ def post_init
11
+ LOGGER.info "Accepted #{peer}"
12
+ @buffer = []
13
+ @tries = 0
14
+ ProxyMachine.incr
15
+ end
16
+
17
+ def peer
18
+ @peer ||=
19
+ begin
20
+ port, ip = Socket.unpack_sockaddr_in(get_peername)
21
+ "#{ip}:#{port}"
22
+ end
23
+ end
24
+
25
+ def receive_data(data)
26
+ if !@server_side
27
+ @buffer << data
28
+ ensure_server_side_connection
29
+ end
30
+ rescue => e
31
+ close_connection
32
+ LOGGER.info "#{e.class} - #{e.message}"
33
+ end
34
+
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
65
+ close_connection
66
+ end
67
+ end
68
+ end
69
+
70
+ def try_server_connect(host, port, klass)
71
+ @server_side = klass.request(host, port, self)
72
+ proxy_incoming_to(@server_side, 10240)
73
+ LOGGER.info "Successful connection to #{host}:#{port}."
74
+ true
75
+ rescue => e
76
+ if @tries < 10
77
+ @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
83
+ else
84
+ LOGGER.info "Failed after ten connection attempts."
85
+ end
86
+ false
87
+ end
88
+
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
96
+ end
97
+
98
+ def unbind
99
+ @server_side.close_connection_after_writing if @server_side
100
+ ProxyMachine.decr
101
+ end
102
+
103
+ # Proxy connection has been lost
104
+ def proxy_target_unbound
105
+ @server_side = nil
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,19 @@
1
+ class ProxyMachine
2
+ class ServerConnection < EventMachine::Connection
3
+ def self.request(host, port, client_side)
4
+ EventMachine.connect(host, port, self, client_side)
5
+ end
6
+
7
+ def initialize(conn)
8
+ @client_side = conn
9
+ end
10
+
11
+ def post_init
12
+ proxy_incoming_to(@client_side, 10240)
13
+ end
14
+
15
+ def unbind
16
+ @client_side.close_connection_after_writing
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,27 @@
1
+ LOGGER = Logger.new(File.new('/dev/null', 'w'))
2
+
3
+ callback = proc do |data|
4
+ data + ":callback"
5
+ end
6
+
7
+ proxy do |data|
8
+ if data == 'a'
9
+ { :remote => "localhost:9980" }
10
+ elsif data == 'b'
11
+ { :remote => "localhost:9981" }
12
+ elsif data == 'c'
13
+ { :remote => "localhost:9980", :data => 'ccc' }
14
+ elsif data == 'd'
15
+ { :close => 'ddd' }
16
+ elsif data == 'e' * 2048
17
+ { :noop => true }
18
+ elsif data == 'e' * 2048 + 'f'
19
+ { :remote => "localhost:9980" }
20
+ elsif data == 'g'
21
+ { :remote => "localhost:9980", :data => 'g2', :reply => 'g3-' }
22
+ elsif data == 'h'
23
+ { :remote => "localhost:9980", :callback => callback }
24
+ else
25
+ { :close => true }
26
+ end
27
+ end
@@ -0,0 +1,47 @@
1
+ require 'test_helper'
2
+
3
+ def assert_proxy(host, port, send, recv)
4
+ sock = TCPSocket.new(host, port)
5
+ sock.write(send)
6
+ assert_equal recv, sock.read
7
+ sock.close
8
+ end
9
+
10
+ class ProxymachineTest < Test::Unit::TestCase
11
+ should "handle simple routing" do
12
+ assert_proxy('localhost', 9990, 'a', '9980:a')
13
+ assert_proxy('localhost', 9990, 'b', '9981:b')
14
+ end
15
+
16
+ should "handle connection closing" do
17
+ sock = TCPSocket.new('localhost', 9990)
18
+ sock.write('xxx')
19
+ assert_equal nil, sock.read(1)
20
+ sock.close
21
+ end
22
+
23
+ should "handle rewrite routing" do
24
+ assert_proxy('localhost', 9990, 'c', '9980:ccc')
25
+ end
26
+
27
+ should "handle rewrite closing" do
28
+ assert_proxy('localhost', 9990, 'd', 'ddd')
29
+ end
30
+
31
+ should "handle data plus reply" do
32
+ assert_proxy('localhost', 9990, 'g', 'g3-9980:g2')
33
+ end
34
+
35
+ should "handle noop" do
36
+ sock = TCPSocket.new('localhost', 9990)
37
+ sock.write('e' * 2048)
38
+ sock.flush
39
+ sock.write('f')
40
+ assert_equal '9980:' + 'e' * 2048 + 'f', sock.read
41
+ sock.close
42
+ end
43
+
44
+ should "execute a callback" do
45
+ assert_proxy('localhost', 9990, 'h', '9980:h:callback')
46
+ end
47
+ end
@@ -0,0 +1,57 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+ require 'socket'
5
+
6
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
7
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
8
+ require 'proxymachine'
9
+
10
+ # A simple echo server to use in tests
11
+ module EventMachine
12
+ module Protocols
13
+ class TestConnection < Connection
14
+ def self.start(host, port)
15
+ @@port = port
16
+ EM.start_server(host, port, self)
17
+ end
18
+
19
+ def receive_data(data)
20
+ send_data("#{@@port}:#{data}")
21
+ close_connection_after_writing
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ def harikari(ppid)
28
+ Thread.new do
29
+ loop do
30
+ begin
31
+ Process.kill(0, ppid)
32
+ rescue
33
+ exit
34
+ end
35
+ sleep 1
36
+ end
37
+ end
38
+ end
39
+
40
+ ppid = Process.pid
41
+
42
+ # Start the simple proxymachine
43
+ fork do
44
+ harikari(ppid)
45
+ load(File.join(File.dirname(__FILE__), *%w[configs simple.rb]))
46
+ ProxyMachine.run('simple', 'localhost', 9990)
47
+ end
48
+
49
+ # Start two test daemons
50
+ [9980, 9981].each do |port|
51
+ fork do
52
+ harikari(ppid)
53
+ EM.run do
54
+ EventMachine::Protocols::TestConnection.start('localhost', port)
55
+ end
56
+ end
57
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fizx-proxymachine
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Tom Preston-Werner
8
+ - Kyle Maxwell
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2010-01-10 00:00:00 -08:00
14
+ default_executable: proxymachine
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: eventmachine
18
+ type: :runtime
19
+ version_requirement:
20
+ version_requirements: !ruby/object:Gem::Requirement
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: 0.12.10
25
+ version:
26
+ description:
27
+ email: tom@mojombo.com
28
+ executables:
29
+ - proxymachine
30
+ extensions: []
31
+
32
+ extra_rdoc_files:
33
+ - LICENSE
34
+ - README.md
35
+ files:
36
+ - .document
37
+ - .gitignore
38
+ - History.txt
39
+ - LICENSE
40
+ - README.md
41
+ - Rakefile
42
+ - VERSION.yml
43
+ - bin/proxymachine
44
+ - examples/git.rb
45
+ - examples/long.rb
46
+ - examples/transparent.rb
47
+ - fizx-proxymachine.gemspec
48
+ - lib/fizx_proxymachine.rb
49
+ - lib/proxymachine.rb
50
+ - lib/proxymachine/callback_server_connection.rb
51
+ - lib/proxymachine/client_connection.rb
52
+ - lib/proxymachine/server_connection.rb
53
+ - test/configs/simple.rb
54
+ - test/proxymachine_test.rb
55
+ - test/test_helper.rb
56
+ has_rdoc: true
57
+ homepage: http://github.com/fizx/proxymachine
58
+ licenses: []
59
+
60
+ post_install_message:
61
+ rdoc_options:
62
+ - --charset=UTF-8
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: "0"
70
+ version:
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: "0"
76
+ version:
77
+ requirements: []
78
+
79
+ rubyforge_project:
80
+ rubygems_version: 1.3.5
81
+ signing_key:
82
+ specification_version: 3
83
+ summary: ProxyMachine is a simple content aware (layer 7) TCP routing proxy.
84
+ test_files:
85
+ - test/configs/simple.rb
86
+ - test/proxymachine_test.rb
87
+ - test/test_helper.rb
88
+ - examples/git.rb
89
+ - examples/long.rb
90
+ - examples/transparent.rb