proxymachine 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +7 -0
- data/README.md +42 -0
- data/VERSION.yml +2 -1
- data/lib/proxymachine.rb +27 -1
- data/lib/proxymachine/client_connection.rb +74 -48
- data/lib/proxymachine/server_connection.rb +29 -3
- data/proxymachine.gemspec +5 -4
- data/test/configs/simple.rb +15 -1
- data/test/proxymachine_test.rb +28 -0
- data/test/test_helper.rb +2 -1
- metadata +2 -2
data/History.txt
CHANGED
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/VERSION.yml
CHANGED
data/lib/proxymachine.rb
CHANGED
@@ -70,12 +70,30 @@ class ProxyMachine
|
|
70
70
|
end
|
71
71
|
end
|
72
72
|
|
73
|
+
def self.set_connect_error_callback(&block)
|
74
|
+
@@connect_error_callback = block
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.connect_error_callback
|
78
|
+
@@connect_error_callback
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.set_inactivity_error_callback(&block)
|
82
|
+
@@inactivity_error_callback = block
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.inactivity_error_callback
|
86
|
+
@@inactivity_error_callback
|
87
|
+
end
|
88
|
+
|
73
89
|
def self.run(name, host, port)
|
74
90
|
@@totalcounter = 0
|
75
91
|
@@maxcounter = 0
|
76
92
|
@@counter = 0
|
77
93
|
@@name = name
|
78
94
|
@@listen = "#{host}:#{port}"
|
95
|
+
@@connect_error_callback ||= proc { |remote| }
|
96
|
+
@@inactivity_error_callback ||= proc { |remote| }
|
79
97
|
self.update_procline
|
80
98
|
EM.epoll
|
81
99
|
|
@@ -107,4 +125,12 @@ module Kernel
|
|
107
125
|
def proxy(&block)
|
108
126
|
ProxyMachine.set_router(block)
|
109
127
|
end
|
110
|
-
|
128
|
+
|
129
|
+
def proxy_connect_error(&block)
|
130
|
+
ProxyMachine.set_connect_error_callback(&block)
|
131
|
+
end
|
132
|
+
|
133
|
+
def proxy_inactivity_error(&block)
|
134
|
+
ProxyMachine.set_inactivity_error_callback(&block)
|
135
|
+
end
|
136
|
+
end
|
@@ -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,73 +27,95 @@ class ProxyMachine
|
|
23
27
|
end
|
24
28
|
|
25
29
|
def receive_data(data)
|
26
|
-
if !@
|
30
|
+
if !@connected
|
27
31
|
@buffer << data
|
28
|
-
|
32
|
+
establish_remote_server if @remote.nil?
|
29
33
|
end
|
30
34
|
rescue => e
|
31
35
|
close_connection
|
32
36
|
LOGGER.info "#{e.class} - #{e.message}"
|
33
37
|
end
|
34
38
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
close_connection_after_writing
|
58
|
-
end
|
59
|
-
elsif commands[:noop]
|
60
|
-
# do nothing
|
61
|
-
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
|
62
61
|
close_connection
|
62
|
+
else
|
63
|
+
send_data(close)
|
64
|
+
close_connection_after_writing
|
63
65
|
end
|
66
|
+
elsif commands[:noop]
|
67
|
+
# do nothing
|
68
|
+
else
|
69
|
+
close_connection
|
64
70
|
end
|
65
71
|
end
|
66
72
|
|
67
|
-
|
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}"
|
68
78
|
@server_side = ServerConnection.request(host, port, self)
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
79
|
+
@server_side.pending_connect_timeout = @connect_timeout
|
80
|
+
@server_side.comm_inactivity_timeout = @inactivity_timeout
|
81
|
+
end
|
82
|
+
|
83
|
+
# Called by the server side immediately after the server connection was
|
84
|
+
# successfully established. Send any buffer we've accumulated and start
|
85
|
+
# raw proxying.
|
86
|
+
def server_connection_success
|
87
|
+
LOGGER.info "Successful connection to #{@remote.join(':')}"
|
88
|
+
@connected = true
|
89
|
+
@buffer.each { |data| @server_side.send_data(data) }
|
90
|
+
proxy_incoming_to @server_side
|
91
|
+
end
|
92
|
+
|
93
|
+
# Called by the server side when a connection could not be established,
|
94
|
+
# either due to a hard connection failure or to a connection timeout.
|
95
|
+
# Leave the client connection open and retry the server connection up to
|
96
|
+
# 10 times.
|
97
|
+
def server_connection_failed
|
98
|
+
@server_side = nil
|
73
99
|
if @tries < 10
|
74
100
|
@tries += 1
|
75
|
-
LOGGER.info "
|
76
|
-
|
77
|
-
@timer = EventMachine::Timer.new(0.1) do
|
78
|
-
self.ensure_server_side_connection
|
79
|
-
end
|
101
|
+
LOGGER.info "Retrying connection with #{@remote.join(':')} (##{@tries})"
|
102
|
+
EM.add_timer(0.1) { connect_to_server }
|
80
103
|
else
|
81
|
-
LOGGER.info "
|
104
|
+
LOGGER.info "Connect #{@remote.join(':')} failed after ten attempts."
|
105
|
+
close_connection
|
106
|
+
ProxyMachine.connect_error_callback.call(@remote.join(':'))
|
82
107
|
end
|
83
|
-
false
|
84
108
|
end
|
85
109
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
110
|
+
# Called by the server when an inactivity timeout is detected. The timeout
|
111
|
+
# argument is the configured inactivity timeout in seconds as a float; the
|
112
|
+
# elapsed argument is the amount of time that actually elapsed since
|
113
|
+
# connecting but not receiving any data.
|
114
|
+
def server_inactivity_timeout(timeout, elapsed)
|
115
|
+
LOGGER.info "Disconnecting #{@remote.join(':')} after #{elapsed}s of inactivity (> #{timeout.inspect})"
|
116
|
+
@server_side = nil
|
117
|
+
close_connection
|
118
|
+
ProxyMachine.inactivity_error_callback.call(@remote.join(':'))
|
93
119
|
end
|
94
120
|
|
95
121
|
def unbind
|
@@ -6,14 +6,40 @@ 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
|
12
|
-
|
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)
|
18
|
+
proxy_incoming_to @client_side
|
19
|
+
end
|
20
|
+
|
21
|
+
def connection_completed
|
22
|
+
@connected = Time.now
|
23
|
+
@timeout = comm_inactivity_timeout || 0.0
|
24
|
+
@client_side.server_connection_success
|
13
25
|
end
|
14
26
|
|
15
27
|
def unbind
|
16
|
-
|
28
|
+
now = Time.now
|
29
|
+
if !@connected
|
30
|
+
@client_side.server_connection_failed
|
31
|
+
elsif !@data_received
|
32
|
+
if @timeout > 0.0 && (elapsed = now - @connected) >= @timeout
|
33
|
+
# EM aborted the connection due to an inactivity timeout
|
34
|
+
@client_side.server_inactivity_timeout(@timeout, elapsed)
|
35
|
+
else
|
36
|
+
# server disconnected soon after connecting without sending data
|
37
|
+
# treat this like a failed server connection
|
38
|
+
@client_side.server_connection_failed
|
39
|
+
end
|
40
|
+
else
|
41
|
+
@client_side.close_connection_after_writing
|
42
|
+
end
|
17
43
|
end
|
18
44
|
end
|
19
45
|
end
|
data/proxymachine.gemspec
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
# Generated by jeweler
|
2
|
-
# DO NOT EDIT THIS FILE
|
3
|
-
# Instead, edit Jeweler::Tasks in
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in rakefile, and run the gemspec command
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{proxymachine}
|
8
|
-
s.version = "1.
|
8
|
+
s.version = "1.2.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Tom Preston-Werner"]
|
12
|
-
s.date = %q{
|
12
|
+
s.date = %q{2010-02-09}
|
13
13
|
s.default_executable = %q{proxymachine}
|
14
14
|
s.email = %q{tom@mojombo.com}
|
15
15
|
s.executables = ["proxymachine"]
|
@@ -64,3 +64,4 @@ Gem::Specification.new do |s|
|
|
64
64
|
s.add_dependency(%q<eventmachine>, [">= 0.12.10"])
|
65
65
|
end
|
66
66
|
end
|
67
|
+
|
data/test/configs/simple.rb
CHANGED
@@ -15,7 +15,21 @@ proxy do |data|
|
|
15
15
|
{ :remote => "localhost:9980" }
|
16
16
|
elsif data == 'g'
|
17
17
|
{ :remote => "localhost:9980", :data => 'g2', :reply => 'g3-' }
|
18
|
+
elsif data == 'connect reject'
|
19
|
+
{ :remote => "localhost:9989" }
|
20
|
+
elsif data == 'inactivity'
|
21
|
+
{ :remote => "localhost:9980", :data => 'sleep 3', :inactivity_timeout => 1 }
|
18
22
|
else
|
19
23
|
{ :close => true }
|
20
24
|
end
|
21
|
-
end
|
25
|
+
end
|
26
|
+
|
27
|
+
ERROR_FILE = File.expand_path('../../proxy_error', __FILE__)
|
28
|
+
|
29
|
+
proxy_connect_error do |remote|
|
30
|
+
File.open(ERROR_FILE, 'wb') { |fd| fd.write("connect error: #{remote}") }
|
31
|
+
end
|
32
|
+
|
33
|
+
proxy_inactivity_error do |remote|
|
34
|
+
File.open(ERROR_FILE, 'wb') { |fd| fd.write("activity error: #{remote}") }
|
35
|
+
end
|
data/test/proxymachine_test.rb
CHANGED
@@ -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')
|
@@ -40,4 +48,24 @@ class ProxymachineTest < Test::Unit::TestCase
|
|
40
48
|
assert_equal '9980:' + 'e' * 2048 + 'f', sock.read
|
41
49
|
sock.close
|
42
50
|
end
|
51
|
+
|
52
|
+
should "call proxy_connect_error when a connection is rejected" do
|
53
|
+
sock = TCPSocket.new('localhost', 9990)
|
54
|
+
sock.write('connect reject')
|
55
|
+
sock.flush
|
56
|
+
assert_equal "", sock.read
|
57
|
+
sock.close
|
58
|
+
assert_equal "connect error: localhost:9989", File.read(@proxy_error_file)
|
59
|
+
end
|
60
|
+
|
61
|
+
should "call proxy_inactivity_error when initial read times out" do
|
62
|
+
sock = TCPSocket.new('localhost', 9990)
|
63
|
+
sent = Time.now
|
64
|
+
sock.write('inactivity')
|
65
|
+
sock.flush
|
66
|
+
assert_equal "", sock.read
|
67
|
+
assert_operator Time.now - sent, :>=, 1.0
|
68
|
+
assert_equal "activity error: localhost:9980", File.read(@proxy_error_file)
|
69
|
+
sock.close
|
70
|
+
end
|
43
71
|
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,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: proxymachine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tom Preston-Werner
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date:
|
12
|
+
date: 2010-02-09 00:00:00 -08:00
|
13
13
|
default_executable: proxymachine
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|