experella-proxy 0.0.6
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/.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,100 @@
|
|
1
|
+
module ExperellaProxy
|
2
|
+
|
3
|
+
# BackendServer objects contain information on available BackendServers
|
4
|
+
#
|
5
|
+
# Accepts Requests based on Request header information matched to it's message_pattern
|
6
|
+
#
|
7
|
+
# See {#initialize}
|
8
|
+
class BackendServer
|
9
|
+
|
10
|
+
attr_accessor :host, :port, :concurrency, :workload, :name
|
11
|
+
attr_reader :message_pattern, :mangle
|
12
|
+
|
13
|
+
# Constructor of the BackendServer
|
14
|
+
#
|
15
|
+
# required keys
|
16
|
+
#
|
17
|
+
# :host Host address of the Server, can be IP or domain
|
18
|
+
# :port Port of the Server, Integervalue as String
|
19
|
+
#
|
20
|
+
# optional keys
|
21
|
+
#
|
22
|
+
# :name Name of the Server used in the Logger and Cashing, String
|
23
|
+
# Will default to #{host}:#{port} but needs to be unique, though theoretical there can be
|
24
|
+
# multiple servers with the same host:port value pair!
|
25
|
+
#
|
26
|
+
# :concurrency max Number of concurrent connections, Integervalue as String. Default is 1
|
27
|
+
#
|
28
|
+
# :accepts Hash containing keys matching HTTP headers or URI :path, :port, :query
|
29
|
+
# Values are Regexp as Strings or as Regex, use ^((?!pattern).)*$ to negate matching
|
30
|
+
# Care: Will match any Header/value pairs not defined in the accepts Hash
|
31
|
+
#
|
32
|
+
# :mangle Hash containing keys matching HTTP headers. Values can be callable block or Strings
|
33
|
+
# Mangle modifies the header value based on the given block or replaces the header value with the String
|
34
|
+
#
|
35
|
+
# mangle is applied in {Connection#connect_backendserver}
|
36
|
+
#
|
37
|
+
# @param host [String] host domain-URL oder IP
|
38
|
+
# @param port [String] port as string
|
39
|
+
# @param [Hash] options
|
40
|
+
# @option options [String] :name name used in logs and for storage. will use Host:Port if no name is specified
|
41
|
+
# @option options [String] :concurrency concurrency. will use 1 as default
|
42
|
+
# @option options [Hash|Proc] :accepts message_pattern for request headers this BackendServer accepts or a lambda where the request is passed
|
43
|
+
# Keys get symbolized and values create Regexp objects. Empty Hash is default
|
44
|
+
# @option options [Hash] :mangle Hash which can modify request headers. Keys get symbolized. nil is default
|
45
|
+
def initialize(host, port, options = {})
|
46
|
+
@host = host #host URL as string
|
47
|
+
@port = port #port as string
|
48
|
+
@name = options[:name] || "#{host}:#{port}"
|
49
|
+
if options[:concurrency].nil?
|
50
|
+
@concurrency = 1
|
51
|
+
else
|
52
|
+
@concurrency = options[:concurrency].to_i
|
53
|
+
end
|
54
|
+
@workload = 0
|
55
|
+
|
56
|
+
update_message_pattern(options[:accepts])
|
57
|
+
|
58
|
+
#mangle can be nil
|
59
|
+
@mangle = options[:mangle]
|
60
|
+
#convert keys to symbols to match request header keys
|
61
|
+
@mangle = @mangle.inject({}) { |memo, (k, v)| memo[k.to_sym] = v; memo } unless @mangle.nil?
|
62
|
+
end
|
63
|
+
|
64
|
+
# compares Backend servers accepted message_pattern to request object
|
65
|
+
#
|
66
|
+
# @param request [Request] a request object
|
67
|
+
# @return [Boolean] true if BackendServer accepts the Request, false otherwise
|
68
|
+
def accept?(request)
|
69
|
+
res=@message_pattern.call(request)
|
70
|
+
#puts "#{name} #{request.header['request_url']} #{res}"
|
71
|
+
res
|
72
|
+
end
|
73
|
+
|
74
|
+
# Updates the message_pattern
|
75
|
+
#
|
76
|
+
# @param hsh [Hash] hash containing additional message_pattern information. Keys get symbolized and values create
|
77
|
+
# Regexp objects. Values of duplicate keys will be overwritten
|
78
|
+
def update_message_pattern(obj)
|
79
|
+
@message_pattern =if obj.respond_to?(:call)
|
80
|
+
obj
|
81
|
+
else
|
82
|
+
#precompile message pattern keys to symbols and values to regexp objects
|
83
|
+
keys = (obj||{}).inject({}) { |memo, (k, v)| memo[k.to_sym] = Regexp.new(v); memo }
|
84
|
+
lambda do |request| #TODO: ugly!
|
85
|
+
ret=true
|
86
|
+
keys.each do |key, pattern|
|
87
|
+
#use uri for :port :path and :query keys
|
88
|
+
if key == :port || key == :path || key == :query
|
89
|
+
ret=false unless request.uri[key] && request.uri[key].match(pattern)
|
90
|
+
else #use headers
|
91
|
+
ret=false unless request.header[key] && request.header[key].match(pattern)
|
92
|
+
end
|
93
|
+
break unless ret #stop as soon as possible
|
94
|
+
end
|
95
|
+
ret
|
96
|
+
end #lambda
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
module ExperellaProxy
|
2
|
+
|
3
|
+
# static getter for the config variable
|
4
|
+
#
|
5
|
+
# @return [Configuration] config
|
6
|
+
def self.config
|
7
|
+
@config
|
8
|
+
end
|
9
|
+
|
10
|
+
# static setter for the config variable
|
11
|
+
#
|
12
|
+
# @param config [Configuration] a config object
|
13
|
+
# @return [Configuration] config
|
14
|
+
def self.config=(config)
|
15
|
+
@config=config
|
16
|
+
end
|
17
|
+
# The Configuration loader
|
18
|
+
#
|
19
|
+
# The config specifies following DSL options
|
20
|
+
#
|
21
|
+
# backend
|
22
|
+
# Takes an options hash defining a backend_server, see {BackendServer}
|
23
|
+
#
|
24
|
+
# set_logger
|
25
|
+
# specifies the Logger used by the program. The Logger must support debug/info/warn/error/fatal functions
|
26
|
+
#
|
27
|
+
# set_proxy
|
28
|
+
# Add proxy as Hash with :host => "string-ip/domain" and :proxy => Fixnum
|
29
|
+
# The proxy will listen on every host:port hash added with this function.
|
30
|
+
#
|
31
|
+
# set_timeout
|
32
|
+
# Time as float when an idle persistent connection gets closed (no receive/send events occured)
|
33
|
+
#
|
34
|
+
# set_error_pages
|
35
|
+
# Add html error-pages to the proxy, requires 2 arguments
|
36
|
+
# 1st arg: the error code as Fixnum
|
37
|
+
# 2nd arg: path to an error page html file relative to the config file directory
|
38
|
+
# Currently 404 and 503 error codes are supported
|
39
|
+
#
|
40
|
+
class Configuration
|
41
|
+
|
42
|
+
# Error raised if the Config couldn't be load successfully
|
43
|
+
#
|
44
|
+
# Currently only raised if config filepath is invalid
|
45
|
+
class NoConfigError < StandardError
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
attr_reader :logger, :proxy, :timeout, :backends, :error_pages
|
50
|
+
|
51
|
+
require 'logger'
|
52
|
+
|
53
|
+
# The Configuration
|
54
|
+
#
|
55
|
+
# @param [Hash] options
|
56
|
+
# @option options [String] :configfile the config filepath
|
57
|
+
def initialize(options={})
|
58
|
+
@backends=[]
|
59
|
+
@proxy=[]
|
60
|
+
@error_pages = {404 => "", 503 => ""}
|
61
|
+
default_options={:timeout => 15.0, :logger => Logger.new($stdout)}
|
62
|
+
options=options.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
|
63
|
+
options=default_options.merge(options)
|
64
|
+
options.each do |k,v|
|
65
|
+
self.instance_variable_set("@#{k}",v)
|
66
|
+
end
|
67
|
+
read_config_file(@configfile) if @configfile
|
68
|
+
ExperellaProxy.config=self
|
69
|
+
end
|
70
|
+
|
71
|
+
# Return filenames fullpath relative to configfile directory
|
72
|
+
#
|
73
|
+
# @param filename [String] the filename
|
74
|
+
# @return [String] full filepath
|
75
|
+
def join_config_path(filename)
|
76
|
+
File.expand_path(File.join(File.dirname(@configfile), filename))
|
77
|
+
end
|
78
|
+
|
79
|
+
# Opens the given config file and evaluates it's contents DSL
|
80
|
+
#
|
81
|
+
# @param configfile [String] the config filepath
|
82
|
+
# @return [Boolean] true on success, false if file can't be found
|
83
|
+
def read_config_file(configfile)
|
84
|
+
if !File.exists?(configfile)
|
85
|
+
puts "error reading #{configfile}"
|
86
|
+
raise NoConfigError.new("unable to read config file #{configfile}")
|
87
|
+
end
|
88
|
+
content=File.read(configfile)
|
89
|
+
instance_eval(content)
|
90
|
+
true
|
91
|
+
end
|
92
|
+
|
93
|
+
#DSL:
|
94
|
+
|
95
|
+
# Adds a {BackendServer} specified in the config file to {#backends}
|
96
|
+
# It allows some syntactic sugar to pass :host_port as an abbrev of host and port keys separated by ':'.
|
97
|
+
# @see {BackendServer#initialize}
|
98
|
+
#
|
99
|
+
# @param backend_options [Hash] backends option hash
|
100
|
+
|
101
|
+
def backend(backend_options)
|
102
|
+
host_port=backend_options.delete(:host_port)
|
103
|
+
if host_port
|
104
|
+
host,port = host_port.split(":")
|
105
|
+
backend_options[:host] = host
|
106
|
+
backend_options[:port] = port
|
107
|
+
end
|
108
|
+
@backends << backend_options
|
109
|
+
end
|
110
|
+
|
111
|
+
# Sets the {Connection} timeout specified in the config file
|
112
|
+
#
|
113
|
+
# @param to [Float] timeout as float
|
114
|
+
def set_timeout(to)
|
115
|
+
@timeout=to
|
116
|
+
end
|
117
|
+
|
118
|
+
# Sets the global Logger object specified in the config file
|
119
|
+
#
|
120
|
+
# Logger can be any object that responds to Ruby Loggers debug, info, warn, error, fatal functions
|
121
|
+
#
|
122
|
+
# @param logger [Logger] the logger object
|
123
|
+
def set_logger(logger)
|
124
|
+
@logger=logger
|
125
|
+
end
|
126
|
+
|
127
|
+
# Adds a Proxy specified in the config file to {#proxy}
|
128
|
+
#
|
129
|
+
# Multiple proxies can be added with multiple calls. Needs a Hash containing :host => "domain/ip", :port => fixnum
|
130
|
+
#
|
131
|
+
# @param proxy [Hash] proxy Hash with :host => "domain/ip", :port => fixnum, :options => options
|
132
|
+
# @option options [Boolean] :tls true if proxy uses TLS encryption
|
133
|
+
# @option options [String] :private_key_file file path to ssl private_key_file relative to config file directory
|
134
|
+
# @option options [String] :cert_chain_file file path to ssl cert_chain_file relative to config file directory
|
135
|
+
def set_proxy(proxy)
|
136
|
+
if proxy[:options] && proxy[:options][:tls]
|
137
|
+
proxy[:options][:private_key_file] = join_config_path(proxy[:options][:private_key_file])
|
138
|
+
proxy[:options][:cert_chain_file] = join_config_path(proxy[:options][:cert_chain_file])
|
139
|
+
end
|
140
|
+
@proxy << proxy
|
141
|
+
end
|
142
|
+
|
143
|
+
# Loads the Errorpages specified in the config file
|
144
|
+
#
|
145
|
+
# currently 404 and 503 errors are supported
|
146
|
+
#
|
147
|
+
# @param key [Fixnum] HTTP Error code
|
148
|
+
# @param page_path [String] page_path relative to config file directory
|
149
|
+
def set_error_pages(key, page_path)
|
150
|
+
@error_pages[key] = File.read(join_config_path(page_path))
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
end
|