fizx-proxymachine 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +5 -0
- data/History.txt +22 -0
- data/LICENSE +20 -0
- data/README.md +140 -0
- data/Rakefile +58 -0
- data/VERSION.yml +5 -0
- data/bin/proxymachine +45 -0
- data/examples/git.rb +26 -0
- data/examples/long.rb +9 -0
- data/examples/transparent.rb +4 -0
- data/fizx-proxymachine.gemspec +69 -0
- data/lib/fizx_proxymachine.rb +1 -0
- data/lib/proxymachine.rb +111 -0
- data/lib/proxymachine/callback_server_connection.rb +27 -0
- data/lib/proxymachine/client_connection.rb +108 -0
- data/lib/proxymachine/server_connection.rb +19 -0
- data/test/configs/simple.rb +27 -0
- data/test/proxymachine_test.rb +47 -0
- data/test/test_helper.rb +57 -0
- metadata +90 -0
data/.document
ADDED
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
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,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"
|
data/lib/proxymachine.rb
ADDED
@@ -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
|
data/test/test_helper.rb
ADDED
@@ -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
|