experella-proxy 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/.gitignore +15 -0
  2. data/Gemfile +3 -0
  3. data/README.md +219 -0
  4. data/Rakefile +25 -0
  5. data/TODO.txt +20 -0
  6. data/bin/experella-proxy +54 -0
  7. data/config/default/404.html +16 -0
  8. data/config/default/503.html +18 -0
  9. data/config/default/config.rb +64 -0
  10. data/config/default/ssl/certs/experella-proxy.pem +18 -0
  11. data/config/default/ssl/private/experella-proxy.key +28 -0
  12. data/dev/experella-proxy +62 -0
  13. data/experella-proxy.gemspec +39 -0
  14. data/lib/experella-proxy/backend.rb +58 -0
  15. data/lib/experella-proxy/backend_server.rb +100 -0
  16. data/lib/experella-proxy/configuration.rb +154 -0
  17. data/lib/experella-proxy/connection.rb +557 -0
  18. data/lib/experella-proxy/connection_manager.rb +167 -0
  19. data/lib/experella-proxy/globals.rb +37 -0
  20. data/lib/experella-proxy/http_status_codes.rb +45 -0
  21. data/lib/experella-proxy/proxy.rb +61 -0
  22. data/lib/experella-proxy/request.rb +106 -0
  23. data/lib/experella-proxy/response.rb +204 -0
  24. data/lib/experella-proxy/server.rb +68 -0
  25. data/lib/experella-proxy/version.rb +15 -0
  26. data/lib/experella-proxy.rb +93 -0
  27. data/spec/echo-server/echo_server.rb +49 -0
  28. data/spec/experella-proxy/backend_server_spec.rb +101 -0
  29. data/spec/experella-proxy/configuration_spec.rb +27 -0
  30. data/spec/experella-proxy/connection_manager_spec.rb +159 -0
  31. data/spec/experella-proxy/experella-proxy_spec.rb +471 -0
  32. data/spec/experella-proxy/request_spec.rb +88 -0
  33. data/spec/experella-proxy/response_spec.rb +44 -0
  34. data/spec/fixtures/404.html +16 -0
  35. data/spec/fixtures/503.html +18 -0
  36. data/spec/fixtures/spec.log +331 -0
  37. data/spec/fixtures/test_config.rb +34 -0
  38. data/spec/spec.log +235 -0
  39. data/spec/spec_helper.rb +35 -0
  40. data/test/sinatra/hello_world_server.rb +17 -0
  41. data/test/sinatra/server_one.rb +89 -0
  42. data/test/sinatra/server_two.rb +89 -0
  43. 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