proxymachine 1.1.0 → 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/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
|