experella-proxy 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +15 -0
- data/Gemfile +3 -0
- data/README.md +219 -0
- data/Rakefile +25 -0
- data/TODO.txt +20 -0
- data/bin/experella-proxy +54 -0
- data/config/default/404.html +16 -0
- data/config/default/503.html +18 -0
- data/config/default/config.rb +64 -0
- data/config/default/ssl/certs/experella-proxy.pem +18 -0
- data/config/default/ssl/private/experella-proxy.key +28 -0
- data/dev/experella-proxy +62 -0
- data/experella-proxy.gemspec +39 -0
- data/lib/experella-proxy/backend.rb +58 -0
- data/lib/experella-proxy/backend_server.rb +100 -0
- data/lib/experella-proxy/configuration.rb +154 -0
- data/lib/experella-proxy/connection.rb +557 -0
- data/lib/experella-proxy/connection_manager.rb +167 -0
- data/lib/experella-proxy/globals.rb +37 -0
- data/lib/experella-proxy/http_status_codes.rb +45 -0
- data/lib/experella-proxy/proxy.rb +61 -0
- data/lib/experella-proxy/request.rb +106 -0
- data/lib/experella-proxy/response.rb +204 -0
- data/lib/experella-proxy/server.rb +68 -0
- data/lib/experella-proxy/version.rb +15 -0
- data/lib/experella-proxy.rb +93 -0
- data/spec/echo-server/echo_server.rb +49 -0
- data/spec/experella-proxy/backend_server_spec.rb +101 -0
- data/spec/experella-proxy/configuration_spec.rb +27 -0
- data/spec/experella-proxy/connection_manager_spec.rb +159 -0
- data/spec/experella-proxy/experella-proxy_spec.rb +471 -0
- data/spec/experella-proxy/request_spec.rb +88 -0
- data/spec/experella-proxy/response_spec.rb +44 -0
- data/spec/fixtures/404.html +16 -0
- data/spec/fixtures/503.html +18 -0
- data/spec/fixtures/spec.log +331 -0
- data/spec/fixtures/test_config.rb +34 -0
- data/spec/spec.log +235 -0
- data/spec/spec_helper.rb +35 -0
- data/test/sinatra/hello_world_server.rb +17 -0
- data/test/sinatra/server_one.rb +89 -0
- data/test/sinatra/server_two.rb +89 -0
- metadata +296 -0
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'eventmachine'
|
3
|
+
require 'http_parser.rb'
|
4
|
+
require 'experella-proxy/globals'
|
5
|
+
require 'experella-proxy/http_status_codes'
|
6
|
+
require 'experella-proxy/configuration'
|
7
|
+
require 'experella-proxy/server'
|
8
|
+
require 'experella-proxy/connection_manager'
|
9
|
+
require 'experella-proxy/backend_server'
|
10
|
+
require 'experella-proxy/connection'
|
11
|
+
require 'experella-proxy/proxy'
|
12
|
+
require 'experella-proxy/backend'
|
13
|
+
require 'experella-proxy/request'
|
14
|
+
require 'experella-proxy/response'
|
15
|
+
|
16
|
+
# Namespace Module for the Proxy Server
|
17
|
+
#
|
18
|
+
# Startup code and options parser is defined here
|
19
|
+
#
|
20
|
+
# @example startup
|
21
|
+
# ARGV << '--help' if ARGV.empty?
|
22
|
+
#
|
23
|
+
# options = {}
|
24
|
+
# OptionParser.new do |opt|
|
25
|
+
# opt.banner = "Usage: experella-proxy <command> <options> -- <application options>\n\n"
|
26
|
+
# opt.banner << "where <applicaion options> are"
|
27
|
+
#
|
28
|
+
# opt.on("-c", "--config=CONFIGFILE", "start server with config in given filepath") do |v|
|
29
|
+
# options[:configfile] = File.expand_path(v)
|
30
|
+
# ExperellaProxy.run(options)
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# end.parse!
|
34
|
+
#
|
35
|
+
# @see file:README.md README
|
36
|
+
# @author Dennis-Florian Herr 2014 @Experteer GmbH
|
37
|
+
module ExperellaProxy
|
38
|
+
|
39
|
+
# Initializes ExperellaProxy's {Configuration} and {ConnectionManager}
|
40
|
+
#
|
41
|
+
# @param [Hash] options Hash passed to the configuration
|
42
|
+
# @option option [String] :configfile the config filepath
|
43
|
+
# @return [Boolean] true if successful false if NoConfigError was raised
|
44
|
+
def self.init(options={})
|
45
|
+
begin
|
46
|
+
Configuration.new(options)
|
47
|
+
rescue Configuration::NoConfigError => e
|
48
|
+
puts e.message
|
49
|
+
puts e.backtrace.join("\n\t")
|
50
|
+
return false
|
51
|
+
end
|
52
|
+
@connection_manager = ConnectionManager.new
|
53
|
+
true
|
54
|
+
end
|
55
|
+
|
56
|
+
# Creates Server object and calls {Server#run} if {ExperellaProxy#init}
|
57
|
+
#
|
58
|
+
# @param [Hash] options Hash passed to the configuration
|
59
|
+
# @option option [String] :configfile the config filepath
|
60
|
+
def self.run(options={})
|
61
|
+
@server = Server.new(options).run if ExperellaProxy.init(options)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Fresh restarts ExperellaProxy with same options.
|
65
|
+
#
|
66
|
+
# Loses all buffered data
|
67
|
+
def self.restart
|
68
|
+
opts = @server.options
|
69
|
+
self.stop
|
70
|
+
Server.new(opts).run if ExperellaProxy.init(opts)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Stops ExperellaProxy
|
74
|
+
#
|
75
|
+
def self.stop
|
76
|
+
Proxy.stop
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
#startup
|
82
|
+
ARGV << '--help' if ARGV.empty?
|
83
|
+
|
84
|
+
options = {}
|
85
|
+
OptionParser.new do |opt|
|
86
|
+
opt.banner = "Usage: experella-proxy <command> <options> -- <application options>\n\n"
|
87
|
+
opt.banner << "where <applicaion options> are"
|
88
|
+
|
89
|
+
opt.on("-c", "--config=CONFIGFILE", "start server with config in given filepath") do |v|
|
90
|
+
options[:configfile] = File.expand_path(v)
|
91
|
+
ExperellaProxy.run(options)
|
92
|
+
end
|
93
|
+
end.parse!
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'http_parser.rb'
|
3
|
+
module EchoServer
|
4
|
+
|
5
|
+
def post_init
|
6
|
+
@parser = Http::Parser.new
|
7
|
+
@buffer = String.new
|
8
|
+
|
9
|
+
@parser.on_headers_complete = proc do |h|
|
10
|
+
if @parser.request_path == "/chunked"
|
11
|
+
@chunked = true
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
@parser.on_message_complete = proc do
|
16
|
+
if @chunked
|
17
|
+
answer = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n"
|
18
|
+
answer << "9\r\nchunk one\r\n"
|
19
|
+
answer << "a\r\n chunk two\r\n"
|
20
|
+
answer << "d\r\n chunk three \r\n"
|
21
|
+
answer << "b\r\nchunk four \r\n"
|
22
|
+
answer << "15\r\nchunk 123456789abcdef\r\n0\r\n\r\n"
|
23
|
+
else
|
24
|
+
@buffer = "you sent: " + @buffer.dump
|
25
|
+
answer = "HTTP/1.1 200 OK\r\nContent-Length: #{@buffer.length}\r\nConnection: close\r\n\r\n"
|
26
|
+
answer << @buffer
|
27
|
+
end
|
28
|
+
send_data answer
|
29
|
+
close_connection_after_writing
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def receive_data data
|
34
|
+
@buffer << data
|
35
|
+
@parser << data
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
EventMachine.run do
|
41
|
+
trap("QUIT") { EM.stop }
|
42
|
+
|
43
|
+
if ARGV.count == 2
|
44
|
+
EventMachine.start_server ARGV.first, ARGV.last.to_i, EchoServer
|
45
|
+
else
|
46
|
+
raise "invalid number of params, expected [server] [port]"
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ExperellaProxy::BackendServer do
|
4
|
+
|
5
|
+
let(:backend) {
|
6
|
+
ExperellaProxy::BackendServer.new("host", "port", {:concurrency => "2", :name => "name",
|
7
|
+
:accepts => {"Host" => "experella"},
|
8
|
+
:mangle => {"Connection" => "close"}
|
9
|
+
})
|
10
|
+
}
|
11
|
+
let(:min_backend) {
|
12
|
+
ExperellaProxy::BackendServer.new("host", "port")
|
13
|
+
}
|
14
|
+
let(:pattern) {
|
15
|
+
backend.message_pattern
|
16
|
+
}
|
17
|
+
|
18
|
+
describe "#new" do
|
19
|
+
|
20
|
+
it "returns a ExperellaProxy::BackendServer" do
|
21
|
+
backend.should be_an_instance_of ExperellaProxy::BackendServer
|
22
|
+
end
|
23
|
+
|
24
|
+
it "has a host" do
|
25
|
+
backend.host.should eql "host"
|
26
|
+
end
|
27
|
+
|
28
|
+
it "has a port" do
|
29
|
+
backend.port.should eql "port"
|
30
|
+
end
|
31
|
+
|
32
|
+
it "has a name" do
|
33
|
+
backend.name.should eql "name"
|
34
|
+
min_backend.name.should eql "host:port"
|
35
|
+
end
|
36
|
+
|
37
|
+
it "has a concurrency" do
|
38
|
+
backend.concurrency.should eql 2
|
39
|
+
min_backend.concurrency.should eql 1
|
40
|
+
end
|
41
|
+
|
42
|
+
it "has a message pattern" do
|
43
|
+
backend.message_pattern.should_not be_nil
|
44
|
+
min_backend.message_pattern.should_not be_nil
|
45
|
+
end
|
46
|
+
|
47
|
+
it "mangle should be nil" do
|
48
|
+
backend.mangle.should == {:Connection => "close"}
|
49
|
+
min_backend.mangle.should be_nil
|
50
|
+
end
|
51
|
+
|
52
|
+
it "has workload zero" do
|
53
|
+
backend.workload.should eql 0
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
shared_examples "the message pattern" do
|
58
|
+
|
59
|
+
it "returns a proc" do
|
60
|
+
pattern.should be_an_instance_of Proc
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
describe "#update_message_pattern" do
|
67
|
+
|
68
|
+
#it "adds the hash parameter to the message_pattern and overwrites values of duplicate keys" do
|
69
|
+
# backend.update_message_pattern({"Host" => "experella.de", "Connection" => "keep-alive"})
|
70
|
+
# pattern = backend.message_pattern
|
71
|
+
# pattern[:Host].should eql Regexp.new("experella.de")
|
72
|
+
# pattern[:Connection].should eql Regexp.new("keep-alive")
|
73
|
+
#end
|
74
|
+
|
75
|
+
it_should_behave_like "the message pattern"
|
76
|
+
end
|
77
|
+
|
78
|
+
describe "#accept?" do
|
79
|
+
|
80
|
+
|
81
|
+
it "returns false if request header doesn't have all keys of message_pattern" do
|
82
|
+
request = ExperellaProxy::Request.new("")
|
83
|
+
request.update_header(:request_url => "/docs")
|
84
|
+
backend.accept?(request).should be_false
|
85
|
+
end
|
86
|
+
|
87
|
+
it "returns false if request headers don't match full message_pattern" do
|
88
|
+
request = ExperellaProxy::Request.new("")
|
89
|
+
request.update_header(:Host => "google.com", :request_url => "/docs")
|
90
|
+
backend.accept?(request).should be_false
|
91
|
+
end
|
92
|
+
|
93
|
+
it "returns true if full message pattern finds matches in request headers" do
|
94
|
+
request = ExperellaProxy::Request.new("")
|
95
|
+
request.update_header(:Host => "experella.com", :request_url => "/docs")
|
96
|
+
backend.accept?(request).should be_true
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ExperellaProxy::Configuration do
|
4
|
+
let(:config) {
|
5
|
+
ExperellaProxy.config
|
6
|
+
}
|
7
|
+
|
8
|
+
let(:no_config){
|
9
|
+
ExperellaProxy::Configuration.new(:configfile => "")
|
10
|
+
}
|
11
|
+
|
12
|
+
it "should load a config file" do
|
13
|
+
config.backends.size.should == 4
|
14
|
+
config.timeout.should == 6.0
|
15
|
+
config.proxy.size.should == 2
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should load error pages" do
|
19
|
+
config.error_pages[404].empty?.should be_false
|
20
|
+
config.error_pages[503].empty?.should be_false
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should raise NoConfigError if config filepath doesn't exist" do
|
24
|
+
lambda { config.read_config_file("/a/non/existing/filepath")
|
25
|
+
}.should raise_error(ExperellaProxy::Configuration::NoConfigError)
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ExperellaProxy::ConnectionManager do
|
4
|
+
|
5
|
+
let(:manager) {
|
6
|
+
ExperellaProxy::ConnectionManager.new
|
7
|
+
}
|
8
|
+
|
9
|
+
class Testconnection
|
10
|
+
|
11
|
+
def initialize()
|
12
|
+
@request = ExperellaProxy::Request.new(self)
|
13
|
+
end
|
14
|
+
|
15
|
+
def get_request
|
16
|
+
@request
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "#backend_count" do
|
21
|
+
it "returns size of the backend_list" do
|
22
|
+
manager.backend_count.should == 0
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "#backend_queue_count" do
|
27
|
+
it "returns size of the backend_queue" do
|
28
|
+
manager.backend_queue_count.should == 0
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "#add_backend" do
|
33
|
+
it "should add a BackendServer to list and queue and return true" do
|
34
|
+
ret = manager.add_backend(ExperellaProxy::BackendServer.new("host", "port"))
|
35
|
+
manager.backend_count.should == 1
|
36
|
+
manager.backend_queue_count.should == 1
|
37
|
+
ret.should == true
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should add a BackendServer to list and return conn if a queued conn matches" do
|
41
|
+
manager.add_backend(ExperellaProxy::BackendServer.new("host", "port"))
|
42
|
+
manager.backend_count.should == 1
|
43
|
+
manager.backend_queue_count.should == 1
|
44
|
+
manager.backend_available?(Testconnection.new().get_request)
|
45
|
+
manager.backend_available?(Testconnection.new().get_request)
|
46
|
+
manager.backend_count.should == 1
|
47
|
+
manager.backend_queue_count.should == 0
|
48
|
+
manager.connection_count.should == 1
|
49
|
+
ret = manager.add_backend(ExperellaProxy::BackendServer.new("hostx", "port"))
|
50
|
+
manager.backend_count.should == 2
|
51
|
+
manager.backend_queue_count.should == 0
|
52
|
+
ret.should be_an_instance_of Testconnection
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "#remove_backend" do
|
58
|
+
it "should return true if a backend was removed" do
|
59
|
+
backend = ExperellaProxy::BackendServer.new("host", "port")
|
60
|
+
manager.add_backend(backend)
|
61
|
+
manager.backend_count.should == 1
|
62
|
+
manager.backend_queue_count.should == 1
|
63
|
+
ret = manager.remove_backend(backend)
|
64
|
+
ret.should be_true
|
65
|
+
manager.backend_count.should == 0
|
66
|
+
manager.backend_queue_count.should == 0
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should return false if no backend was removed" do
|
70
|
+
backend = ExperellaProxy::BackendServer.new("host", "port")
|
71
|
+
manager.remove_backend(backend).should be_false
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe "#free_backend" do
|
76
|
+
it "should return a connection if any queued conn matches" do
|
77
|
+
backend = ExperellaProxy::BackendServer.new("host", "port")
|
78
|
+
manager.add_backend(backend)
|
79
|
+
manager.backend_available?(Testconnection.new().get_request)
|
80
|
+
manager.backend_available?(Testconnection.new().get_request)
|
81
|
+
ret = manager.free_backend(backend)
|
82
|
+
ret.should be_an_instance_of Testconnection
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should return nil and requeue backend if no conn matches" do
|
86
|
+
backend = ExperellaProxy::BackendServer.new("host", "port")
|
87
|
+
manager.add_backend(backend)
|
88
|
+
manager.backend_available?(Testconnection.new().get_request)
|
89
|
+
backend_queue = manager.backend_queue_count
|
90
|
+
workload = backend.workload
|
91
|
+
ret = manager.free_backend(backend)
|
92
|
+
ret.should == nil
|
93
|
+
manager.backend_queue_count.should == (backend_queue + 1)
|
94
|
+
backend.workload.should == (workload - 1)
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
describe "#free_connection" do
|
100
|
+
it "should remove connection from queue" do
|
101
|
+
backend = ExperellaProxy::BackendServer.new("host", "port")
|
102
|
+
manager.add_backend(backend)
|
103
|
+
connections = []
|
104
|
+
connections[1] = Testconnection.new()
|
105
|
+
connections[2] = Testconnection.new()
|
106
|
+
manager.backend_available?(connections[1].get_request)
|
107
|
+
manager.backend_available?(connections[2].get_request)
|
108
|
+
count_before = manager.connection_count
|
109
|
+
manager.free_connection(connections[2])
|
110
|
+
manager.connection_count.should == (count_before - 1)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
describe "#backend_available?" do
|
115
|
+
before(:each) do
|
116
|
+
manager.add_backend(ExperellaProxy::BackendServer.new("host", "port", {:concurrency => 3, :accepts => {"Host" => "test"}}))
|
117
|
+
manager.add_backend(ExperellaProxy::BackendServer.new("host2", "port", {:accepts => {"Host" => "bla"}}))
|
118
|
+
manager.add_backend(ExperellaProxy::BackendServer.new("host3", "port", {:accepts => {"Host" => "blax"}}))
|
119
|
+
end
|
120
|
+
it "returns first matching backend and removes it from queue" do
|
121
|
+
conn = Testconnection.new
|
122
|
+
conn.get_request.update_header("Host" => "loremblaxipsum")
|
123
|
+
before_count = manager.backend_queue_count
|
124
|
+
ret = manager.backend_available?(conn.get_request)
|
125
|
+
ret.name.should == "host2:port"
|
126
|
+
manager.backend_queue_count.should == (before_count - 1)
|
127
|
+
end
|
128
|
+
|
129
|
+
it "should requeue matching backend if concurrency is smaller than workload" do
|
130
|
+
conn = Testconnection.new
|
131
|
+
conn.get_request.update_header("Host" => "loremtestipsum")
|
132
|
+
before_count = manager.backend_queue_count
|
133
|
+
ret = manager.backend_available?(conn.get_request)
|
134
|
+
ret.name.should == "host:port"
|
135
|
+
manager.backend_queue_count.should == before_count
|
136
|
+
end
|
137
|
+
|
138
|
+
it "returns :queued if no matching backend is currently available" do
|
139
|
+
conn = Testconnection.new
|
140
|
+
conn.get_request.update_header("Host" => "loremblaipsum")
|
141
|
+
manager.backend_available?(conn.get_request)
|
142
|
+
before_count = manager.backend_queue_count
|
143
|
+
ret = manager.backend_available?(conn.get_request)
|
144
|
+
ret.should == :queued
|
145
|
+
manager.backend_queue_count.should == before_count
|
146
|
+
end
|
147
|
+
|
148
|
+
it "returns false if no registered backend matches" do
|
149
|
+
conn = Testconnection.new
|
150
|
+
conn.get_request.update_header("Host" => "loremipsum")
|
151
|
+
before_count = manager.backend_queue_count
|
152
|
+
ret = manager.backend_available?(conn.get_request)
|
153
|
+
ret.should be_false
|
154
|
+
manager.backend_queue_count.should == before_count
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|