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.
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