fizx-proxymachine 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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