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,167 @@
|
|
1
|
+
module ExperellaProxy
|
2
|
+
|
3
|
+
# static getter for the connection_manager variable
|
4
|
+
#
|
5
|
+
# @return [ConnectionManager] connection_manager
|
6
|
+
def self.connection_manager
|
7
|
+
@connection_manager
|
8
|
+
end
|
9
|
+
|
10
|
+
# The ConnectionManager is responsible for queueing and matching frontend {Connection} and {BackendServer} objects
|
11
|
+
class ConnectionManager
|
12
|
+
|
13
|
+
# The constructor
|
14
|
+
#
|
15
|
+
def initialize
|
16
|
+
@connection_queue = [] #array queue of client connection objects
|
17
|
+
@backend_queue = [] #array queue of available backend servers
|
18
|
+
@backend_list = {} #list of all backend servers
|
19
|
+
end
|
20
|
+
|
21
|
+
# Matches {Request} to queued {BackendServer}
|
22
|
+
#
|
23
|
+
# Removes first matching {BackendServer} from queue and returns it.
|
24
|
+
# It will requeue the {BackendServer} instantly,
|
25
|
+
# if {BackendServer#workload} is smaller than {BackendServer#concurrency}
|
26
|
+
#
|
27
|
+
# Queues {Request#conn} if no available {BackendServer} matches
|
28
|
+
#
|
29
|
+
# Returns false if no registered {BackendServer} matches
|
30
|
+
#
|
31
|
+
# @return [BackendServer] first matching BackendServer from the queue
|
32
|
+
# @return [Symbol] :queued if Connection was queued
|
33
|
+
# @return [Boolean] false if no registered Backend matches the Request
|
34
|
+
def backend_available?(request)
|
35
|
+
@backend_queue.each do |backend|
|
36
|
+
if backend.accept?(request)
|
37
|
+
#connect backend to requests connection if request matches
|
38
|
+
backend.workload += 1
|
39
|
+
ret = @backend_queue.delete(backend)
|
40
|
+
#requeue backend if concurrency isnt maxed
|
41
|
+
@backend_queue.push(backend) if backend.workload < backend.concurrency
|
42
|
+
return ret
|
43
|
+
end
|
44
|
+
end
|
45
|
+
if match_any_backend?(request)
|
46
|
+
#push requests connection on queue if no backend was connected
|
47
|
+
@connection_queue.push(request.conn)
|
48
|
+
:queued
|
49
|
+
else
|
50
|
+
false
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Called by a {Connection} when the {BackendServer} is done.
|
55
|
+
#
|
56
|
+
# Connects backend to a matching queued {Connection} or pushes server back on queue
|
57
|
+
#
|
58
|
+
# @param backend [BackendServer] BackendServer which got free
|
59
|
+
# @return [NilClass]
|
60
|
+
def free_backend(backend)
|
61
|
+
#check if any queued connections match new available backend
|
62
|
+
conn = match_connections(backend)
|
63
|
+
if conn
|
64
|
+
#return matching connection
|
65
|
+
#you should try to connect the new backend to this connection
|
66
|
+
return conn
|
67
|
+
else
|
68
|
+
#push free backend on queue if it wasn't used for a queued conn or is already queued (concurrency)
|
69
|
+
@backend_queue.push(backend) if @backend_list.include?(backend.name) && !@backend_queue.include?(backend)
|
70
|
+
backend.workload -= 1
|
71
|
+
end
|
72
|
+
nil
|
73
|
+
end
|
74
|
+
|
75
|
+
# Adds a new {BackendServer} to the list and queues or connects it
|
76
|
+
#
|
77
|
+
# @param backend [BackendServer] a new BackendServer
|
78
|
+
# @return [Connection] a queued connection that would match the BackendServer
|
79
|
+
# @return [Boolean] true if backend was added to list
|
80
|
+
def add_backend(backend)
|
81
|
+
|
82
|
+
@backend_list[backend.name] = backend
|
83
|
+
|
84
|
+
#check if any queued connections match new available backend
|
85
|
+
conn = match_connections(backend)
|
86
|
+
if conn
|
87
|
+
#return matching connection
|
88
|
+
#you should try to connect the new backend to this connection
|
89
|
+
return conn
|
90
|
+
else
|
91
|
+
#queue new backend
|
92
|
+
@backend_queue.push(backend)
|
93
|
+
end
|
94
|
+
true
|
95
|
+
end
|
96
|
+
|
97
|
+
# Removes a {BackendServer} from list and queue
|
98
|
+
#
|
99
|
+
# @param backend [BackendServer] the BackendServer to be removed
|
100
|
+
# @return [Boolean] true if a backend was removed, else returns false
|
101
|
+
def remove_backend(backend)
|
102
|
+
|
103
|
+
ret = @backend_list.delete(backend.name)
|
104
|
+
@backend_queue.delete(backend)
|
105
|
+
|
106
|
+
if ret
|
107
|
+
true
|
108
|
+
else
|
109
|
+
false
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Removes a connection from the connection_queue
|
114
|
+
#
|
115
|
+
# @param conn [Connection] Connection to be removed
|
116
|
+
def free_connection(conn)
|
117
|
+
@connection_queue.delete(conn)
|
118
|
+
end
|
119
|
+
|
120
|
+
# returns the count of the currently queued {BackendServer}s
|
121
|
+
#
|
122
|
+
# @return [int]
|
123
|
+
def backend_queue_count
|
124
|
+
@backend_queue.size
|
125
|
+
end
|
126
|
+
|
127
|
+
# returns the count of the registered{BackendServer}s
|
128
|
+
#
|
129
|
+
# @return [int]
|
130
|
+
def backend_count
|
131
|
+
@backend_list.size
|
132
|
+
end
|
133
|
+
|
134
|
+
# returns the count of the currently queued connections
|
135
|
+
#
|
136
|
+
# @return [int]
|
137
|
+
def connection_count
|
138
|
+
@connection_queue.size
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
# Matches request to all known backends
|
144
|
+
#
|
145
|
+
# @return [Boolean] true if it can be matched, false if request is not accepted at all
|
146
|
+
def match_any_backend?(request)
|
147
|
+
@backend_list.each_value do |v|
|
148
|
+
return true if v.accept?(request)
|
149
|
+
end
|
150
|
+
false
|
151
|
+
end
|
152
|
+
|
153
|
+
# Matches queued connections with a backend
|
154
|
+
#
|
155
|
+
# @return [Connection] matching connection
|
156
|
+
# @return [NilClass] nothing matched
|
157
|
+
def match_connections(backend)
|
158
|
+
@connection_queue.each do |conn|
|
159
|
+
if backend.accept?(conn.get_request)
|
160
|
+
return conn
|
161
|
+
end
|
162
|
+
end
|
163
|
+
nil
|
164
|
+
end
|
165
|
+
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module ExperellaProxy
|
2
|
+
|
3
|
+
# defined hop by hop header fields
|
4
|
+
HOP_HEADERS = ["Connection", "Keep-Alive", "Proxy-Authorization", "TE", "Trailer", "Transfer-Encoding", "Upgrade"]
|
5
|
+
|
6
|
+
# Provides getters for global variables
|
7
|
+
#
|
8
|
+
# All methods are private. The module needs to be included in every Class which needs it.
|
9
|
+
module Globals
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
# @!visibility public
|
14
|
+
|
15
|
+
# Get the global config
|
16
|
+
#
|
17
|
+
# @return [Configuration] config object
|
18
|
+
def config
|
19
|
+
ExperellaProxy.config
|
20
|
+
end
|
21
|
+
|
22
|
+
# Get the global logger
|
23
|
+
#
|
24
|
+
# @return [Logger] logger set in config object
|
25
|
+
def log
|
26
|
+
ExperellaProxy.config.logger
|
27
|
+
end
|
28
|
+
|
29
|
+
# Get the global connection manager
|
30
|
+
#
|
31
|
+
# @return [ConnectionManager] connection_manager object
|
32
|
+
def connection_manager
|
33
|
+
ExperellaProxy.connection_manager
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module ExperellaProxy
|
2
|
+
|
3
|
+
# Every standard HTTP code mapped to the appropriate message.
|
4
|
+
#
|
5
|
+
# @author Mongrel.
|
6
|
+
HTTP_STATUS_CODES = {
|
7
|
+
100 => 'Continue',
|
8
|
+
101 => 'Switching Protocols',
|
9
|
+
200 => 'OK',
|
10
|
+
201 => 'Created',
|
11
|
+
202 => 'Accepted',
|
12
|
+
203 => 'Non-Authoritative Information',
|
13
|
+
204 => 'No Content',
|
14
|
+
205 => 'Reset Content',
|
15
|
+
206 => 'Partial Content',
|
16
|
+
300 => 'Multiple Choices',
|
17
|
+
301 => 'Moved Permanently',
|
18
|
+
302 => 'Moved Temporarily',
|
19
|
+
303 => 'See Other',
|
20
|
+
304 => 'Not Modified',
|
21
|
+
305 => 'Use Proxy',
|
22
|
+
400 => 'Bad Request',
|
23
|
+
401 => 'Unauthorized',
|
24
|
+
402 => 'Payment Required',
|
25
|
+
403 => 'Forbidden',
|
26
|
+
404 => 'Not Found',
|
27
|
+
405 => 'Method Not Allowed',
|
28
|
+
406 => 'Not Acceptable',
|
29
|
+
407 => 'Proxy Authentication Required',
|
30
|
+
408 => 'Request Time-out',
|
31
|
+
409 => 'Conflict',
|
32
|
+
410 => 'Gone',
|
33
|
+
411 => 'Length Required',
|
34
|
+
412 => 'Precondition Failed',
|
35
|
+
413 => 'Request Entity Too Large',
|
36
|
+
414 => 'Request-URI Too Large',
|
37
|
+
415 => 'Unsupported Media Type',
|
38
|
+
500 => 'Internal Server Error',
|
39
|
+
501 => 'Not Implemented',
|
40
|
+
502 => 'Bad Gateway',
|
41
|
+
503 => 'Service Unavailable',
|
42
|
+
504 => 'Gateway Time-out',
|
43
|
+
505 => 'HTTP Version not supported'
|
44
|
+
}
|
45
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module ExperellaProxy
|
2
|
+
# The proxy
|
3
|
+
#
|
4
|
+
# Controls the EventMachine, initializes backends from config and starts proxy servers
|
5
|
+
class Proxy
|
6
|
+
extend ExperellaProxy::Globals
|
7
|
+
|
8
|
+
# Starts the Eventmachine, initializes backends in {ConnectionManager} and starts the servers
|
9
|
+
# defined in config the proxy should listen on
|
10
|
+
#
|
11
|
+
# @param options [Hash] option Hash passed to the {Connection}
|
12
|
+
# @param blk [Block] Block evaluated in each new {Connection}
|
13
|
+
def self.start(options, &blk)
|
14
|
+
|
15
|
+
#initalize backend servers from config
|
16
|
+
config.backends.each do |backend|
|
17
|
+
connection_manager.add_backend(BackendServer.new(backend[:host], backend[:port], backend))
|
18
|
+
log.info "Initializing backend #{backend[:name]} at #{backend[:host]}:#{backend[:port]} with concurrency\
|
19
|
+
#{backend[:concurrency]}"
|
20
|
+
log.info "Backend accepts: #{backend[:accepts].inspect}"
|
21
|
+
log.info "Backend mangles: #{backend[:mangle].inspect}"
|
22
|
+
end
|
23
|
+
|
24
|
+
#start eventmachine
|
25
|
+
EM.epoll
|
26
|
+
EM.run do
|
27
|
+
trap("TERM") { stop }
|
28
|
+
trap("INT") { stop }
|
29
|
+
|
30
|
+
if config.proxy.empty?
|
31
|
+
log.fatal "No proxy host:port address configured. Stopping experella-proxy."
|
32
|
+
return stop
|
33
|
+
else
|
34
|
+
config.proxy.each do |proxy|
|
35
|
+
opts = options
|
36
|
+
# pass proxy specific options
|
37
|
+
unless proxy[:options].nil?
|
38
|
+
opts = options.merge(proxy[:options])
|
39
|
+
end
|
40
|
+
log.info "Launching experella-proxy at #{proxy[:host]}:#{proxy[:port]} with #{config.timeout}s timeout..."
|
41
|
+
log.info "with options: #{opts.inspect}"
|
42
|
+
EventMachine::start_server(proxy[:host], proxy[:port],
|
43
|
+
Connection, opts) do |conn|
|
44
|
+
conn.instance_eval(&blk)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# Stops the Eventmachine and terminates all connections
|
53
|
+
#
|
54
|
+
def self.stop
|
55
|
+
if EM.reactor_running?
|
56
|
+
log.info("Terminating experella-proxy")
|
57
|
+
EventMachine::stop_event_loop
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module ExperellaProxy
|
2
|
+
#
|
3
|
+
# Request is used to store incoming (HTTP) requests and parsed data
|
4
|
+
#
|
5
|
+
# Every Request belongs to a client {Connection}
|
6
|
+
#
|
7
|
+
class Request
|
8
|
+
|
9
|
+
include ExperellaProxy::Globals
|
10
|
+
|
11
|
+
attr_accessor :keep_alive, :chunked
|
12
|
+
attr_reader :conn, :header, :uri, :response
|
13
|
+
|
14
|
+
# The constructor
|
15
|
+
#
|
16
|
+
# @param conn [Connection] Connection the request belongs to
|
17
|
+
def initialize(conn)
|
18
|
+
@conn = conn
|
19
|
+
@header = {}
|
20
|
+
@chunked = false # if true the parsed body will be chunked
|
21
|
+
@uri = {} #contains port, path and query information for faster backend selection
|
22
|
+
@keep_alive = true
|
23
|
+
@send_buffer = String.new
|
24
|
+
@response = Response.new(self)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Adds data to the request object
|
28
|
+
#
|
29
|
+
# data must be formatted as string
|
30
|
+
#
|
31
|
+
# @param str [String] data as string
|
32
|
+
def <<(str)
|
33
|
+
@send_buffer << str
|
34
|
+
end
|
35
|
+
|
36
|
+
# Adds a hash with uri information to {#uri}
|
37
|
+
#
|
38
|
+
# duplicate key values will be overwritten with hsh values
|
39
|
+
#
|
40
|
+
# @param hsh [Hash] hash with keys :port :path :query containing URI information
|
41
|
+
def add_uri(hsh)
|
42
|
+
@uri.update(hsh)
|
43
|
+
log.debug hsh
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns the data in send_buffer and empties the send_buffer
|
47
|
+
#
|
48
|
+
# @return [String] data to send
|
49
|
+
def flush
|
50
|
+
@send_buffer.slice!(0, @send_buffer.length)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns if the send_buffer is flushed? (empty)
|
54
|
+
#
|
55
|
+
# @return [Boolean]
|
56
|
+
def flushed?
|
57
|
+
@send_buffer.empty?
|
58
|
+
end
|
59
|
+
|
60
|
+
# Reconstructs modified http request in send_buffer
|
61
|
+
#
|
62
|
+
# Reconstructed request must be a valid request according to the HTTP Protocol
|
63
|
+
#
|
64
|
+
# First Header after Startline will always be "Host: ", after that order is determined by {#header}.each
|
65
|
+
#
|
66
|
+
def reconstruct_header
|
67
|
+
#split send_buffer into header and body part
|
68
|
+
buf = @send_buffer.split(/\r\n\r\n/, 2) unless flushed?
|
69
|
+
@send_buffer = ""
|
70
|
+
#start line
|
71
|
+
@send_buffer << @header[:http_method] + ' '
|
72
|
+
@send_buffer << @header[:request_url] + ' '
|
73
|
+
@send_buffer << "HTTP/1.1\r\n"
|
74
|
+
@send_buffer << "Host: " + @header[:Host] + "\r\n" #add Host first for better header readability
|
75
|
+
#header fields
|
76
|
+
@header.each do |key, value|
|
77
|
+
unless key == :http_method || key == :request_url || key == :http_version || key == :Host #exclude startline parameters
|
78
|
+
@send_buffer << key.to_s + ": "
|
79
|
+
if value.is_a?(Array)
|
80
|
+
@send_buffer << value.shift
|
81
|
+
until value.empty? do
|
82
|
+
@send_buffer << "," + value.shift
|
83
|
+
end
|
84
|
+
else
|
85
|
+
@send_buffer << value
|
86
|
+
end
|
87
|
+
@send_buffer << "\r\n"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
@send_buffer << "\r\n"
|
91
|
+
#reconstruction complete
|
92
|
+
@send_buffer << buf[1] unless buf.nil? #append buffered body
|
93
|
+
log.debug [:reconstructed_sendbuffer, @send_buffer]
|
94
|
+
end
|
95
|
+
|
96
|
+
# Adds a hash to {#header}
|
97
|
+
#
|
98
|
+
# symbolizes hsh keys, duplicate key values will be overwritten with hsh values
|
99
|
+
#
|
100
|
+
# @param hsh [Hash] hash with HTTP header Key:Value pairs
|
101
|
+
def update_header(hsh)
|
102
|
+
hsh = hsh.inject({}) { |memo, (k, v)| memo[k.to_sym] = v; memo }
|
103
|
+
@header.update(hsh)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
module ExperellaProxy
|
2
|
+
#
|
3
|
+
# Response is used to store incoming (HTTP) responses and parsed data
|
4
|
+
#
|
5
|
+
# Every Response belongs to a request {Request}
|
6
|
+
#
|
7
|
+
class Response
|
8
|
+
|
9
|
+
include ExperellaProxy::Globals
|
10
|
+
|
11
|
+
attr_reader :header
|
12
|
+
|
13
|
+
# The constructor
|
14
|
+
#
|
15
|
+
# @param request [Request] Request the response belongs to
|
16
|
+
def initialize(request)
|
17
|
+
@request = request
|
18
|
+
@conn = request.conn
|
19
|
+
@header = {}
|
20
|
+
@status_code = 500
|
21
|
+
@chunked = false # if true the parsed body will be chunked
|
22
|
+
@buffer = false # default is false, so incoming data will be streamed,
|
23
|
+
# used for http1.0 clients and transfer-encoding chunked backend responses
|
24
|
+
@send_buffer = String.new
|
25
|
+
@response_parser = Http::Parser.new
|
26
|
+
init_http_parser
|
27
|
+
end
|
28
|
+
|
29
|
+
# Adds data to the response object
|
30
|
+
#
|
31
|
+
# data must be formatted as string
|
32
|
+
#
|
33
|
+
# On Http::Parser::Error parsing gets interrupted and the connection closed
|
34
|
+
#
|
35
|
+
# @param str [String] data as string
|
36
|
+
def <<(str)
|
37
|
+
begin
|
38
|
+
@response_parser << str
|
39
|
+
rescue Http::Parser::Error
|
40
|
+
log.warn ["Parser error caused by invalid response data", "@#{@conn.signature}"]
|
41
|
+
# on error unbind response_parser object, so additional data doesn't get parsed anymore
|
42
|
+
#
|
43
|
+
# assigning a string to the parser variable, will cause incoming data to get buffered
|
44
|
+
# imho this is a better solution than adding a condition for this rare error case
|
45
|
+
@response_parser = ""
|
46
|
+
@conn.close
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns the data in send_buffer and empties the send_buffer
|
51
|
+
#
|
52
|
+
# @return [String] data to send
|
53
|
+
def flush
|
54
|
+
log.debug [:data_to_user, @send_buffer]
|
55
|
+
@send_buffer.slice!(0, @send_buffer.length)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns if the send_buffer is flushed? (empty)
|
59
|
+
#
|
60
|
+
# @return [Boolean]
|
61
|
+
def flushed?
|
62
|
+
@send_buffer.empty?
|
63
|
+
end
|
64
|
+
|
65
|
+
# Reconstructs modified http response in send_buffer
|
66
|
+
#
|
67
|
+
# Reconstructed response must be a valid response according to the HTTP Protocol
|
68
|
+
#
|
69
|
+
# Header order is determined by {#header}.each
|
70
|
+
#
|
71
|
+
def reconstruct_header
|
72
|
+
@send_buffer = ""
|
73
|
+
#start line
|
74
|
+
@send_buffer << "HTTP/1.1 "
|
75
|
+
@send_buffer << @status_code.to_s + ' '
|
76
|
+
@send_buffer << HTTP_STATUS_CODES[@status_code] + "\r\n"
|
77
|
+
#header fields
|
78
|
+
@header.each do |key, value|
|
79
|
+
@send_buffer << key.to_s + ": "
|
80
|
+
if value.is_a?(Array)
|
81
|
+
@send_buffer << value.shift
|
82
|
+
until value.empty? do
|
83
|
+
@send_buffer << "," + value.shift
|
84
|
+
end
|
85
|
+
else
|
86
|
+
@send_buffer << value
|
87
|
+
end
|
88
|
+
@send_buffer << "\r\n"
|
89
|
+
end
|
90
|
+
@send_buffer << "\r\n"
|
91
|
+
#reconstruction complete
|
92
|
+
log.debug [:response_reconstructed_header, @send_buffer]
|
93
|
+
end
|
94
|
+
|
95
|
+
# Adds a hash to {#header}
|
96
|
+
#
|
97
|
+
# symbolizes hsh keys, duplicate key values will be overwritten with hsh values
|
98
|
+
#
|
99
|
+
# @param hsh [Hash] hash with HTTP header Key:Value pairs
|
100
|
+
def update_header(hsh)
|
101
|
+
hsh = hsh.inject({}) { |memo, (k, v)| memo[k.to_sym] = v; memo }
|
102
|
+
@header.update(hsh)
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
# initializes the response http parser
|
108
|
+
def init_http_parser
|
109
|
+
#called when response headers are completely parsed (first \r\n\r\n triggers this)
|
110
|
+
@response_parser.on_headers_complete = proc do |h|
|
111
|
+
|
112
|
+
@status_code = @response_parser.status_code
|
113
|
+
|
114
|
+
if @request.keep_alive
|
115
|
+
@header[:Connection] = "Keep-Alive"
|
116
|
+
end
|
117
|
+
|
118
|
+
# handle the transfer-encoding
|
119
|
+
#
|
120
|
+
# if no transfer encoding and no content-length is given, terminate connection after backend unbind
|
121
|
+
#
|
122
|
+
# if no transfer encoding is given, but there is content-length, just keep the content-length and send the message
|
123
|
+
#
|
124
|
+
# if a transfer-encoding is given, continue with Transfer-Encoding chunked and remove false content-length
|
125
|
+
# header if present. Old Transfer-Encoding header will be removed with all other hop-by-hop headers
|
126
|
+
#
|
127
|
+
if h["Transfer-Encoding"].nil?
|
128
|
+
# if no transfer-encoding and no content-length is present, set Connection: close
|
129
|
+
if h["Content-Length"].nil?
|
130
|
+
@request.keep_alive = false
|
131
|
+
@header[:Connection] = "close"
|
132
|
+
end
|
133
|
+
#chunked encoded
|
134
|
+
else
|
135
|
+
# buffer response data if client uses http 1.0 until message complete
|
136
|
+
if @request.header[:http_version][0] == 1 && @request.header[:http_version][1] == 0
|
137
|
+
@buffer = true
|
138
|
+
else
|
139
|
+
h.delete("Content-Length")
|
140
|
+
@chunked = true unless @request.header[:http_method] == "HEAD"
|
141
|
+
@header[:"Transfer-Encoding"] = "chunked"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# remove all hop-by-hop header fields
|
146
|
+
unless h["Connection"].nil?
|
147
|
+
h["Connection"].each do |s|
|
148
|
+
h.delete(s)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
HOP_HEADERS.each do |s|
|
152
|
+
h.delete(s)
|
153
|
+
end
|
154
|
+
|
155
|
+
|
156
|
+
via = h.delete("Via")
|
157
|
+
if via.nil?
|
158
|
+
via = "1.1 experella"
|
159
|
+
else
|
160
|
+
via << "1.1 experella"
|
161
|
+
end
|
162
|
+
@header[:Via] = via
|
163
|
+
|
164
|
+
|
165
|
+
update_header(h)
|
166
|
+
unless @buffer
|
167
|
+
# called before any data is put into send_buffer
|
168
|
+
reconstruct_header
|
169
|
+
@conn.send_data flush
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
@response_parser.on_body = proc do |chunk|
|
174
|
+
if @chunked
|
175
|
+
# add hexadecimal chunk size
|
176
|
+
@send_buffer << chunk.size.to_s(16)
|
177
|
+
@send_buffer << "\r\n"
|
178
|
+
@send_buffer << chunk
|
179
|
+
@send_buffer << "\r\n"
|
180
|
+
else
|
181
|
+
@send_buffer << chunk
|
182
|
+
end
|
183
|
+
unless @buffer
|
184
|
+
@conn.send_data flush
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
@response_parser.on_message_complete = proc do
|
189
|
+
if @chunked
|
190
|
+
# send closing chunk
|
191
|
+
@send_buffer << "0\r\n\r\n"
|
192
|
+
@conn.send_data flush
|
193
|
+
elsif @buffer
|
194
|
+
@header[:"Content-Length"] = @send_buffer.size.to_s
|
195
|
+
body = flush
|
196
|
+
reconstruct_header
|
197
|
+
@send_buffer << body
|
198
|
+
@conn.send_data flush
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module ExperellaProxy
|
2
|
+
# The server starts the {Proxy} and provides callbacks/block hooks for client {Connection}s
|
3
|
+
class Server
|
4
|
+
include ExperellaProxy::Globals
|
5
|
+
|
6
|
+
# Constructor
|
7
|
+
#
|
8
|
+
# @param options [Hash] options Hash passed to the proxy
|
9
|
+
def initialize(options)
|
10
|
+
@options=options
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :options
|
14
|
+
|
15
|
+
# Runs the proxy server with given options
|
16
|
+
#
|
17
|
+
# Opens a block passed to every {Connection}
|
18
|
+
#
|
19
|
+
# You can add logic to
|
20
|
+
#
|
21
|
+
# {Connection#connected} in on_connect
|
22
|
+
# {Connection#receive_data} in on_data, must return data
|
23
|
+
# {Connection#relay_from_backend} in on_response, must return resp
|
24
|
+
# {Connection#unbind_backend} in on_finish
|
25
|
+
# {Connection#unbind} in on_unbind
|
26
|
+
#
|
27
|
+
def run
|
28
|
+
|
29
|
+
Proxy.start(options = {}) do |conn|
|
30
|
+
|
31
|
+
log.info msec + "new Connection @" + signature.to_s
|
32
|
+
|
33
|
+
# called on successful backend connection
|
34
|
+
# backend is the name of the connected server
|
35
|
+
conn.on_connect do |backend|
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
# modify / process request stream
|
40
|
+
# and return modified data
|
41
|
+
conn.on_data do |data|
|
42
|
+
data
|
43
|
+
end
|
44
|
+
|
45
|
+
# modify / process response stream
|
46
|
+
# and return modified response
|
47
|
+
conn.on_response do |backend, resp|
|
48
|
+
resp
|
49
|
+
end
|
50
|
+
|
51
|
+
# termination logic
|
52
|
+
conn.on_finish do |backend|
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
# called if client terminates connection
|
57
|
+
# or timeout occurs
|
58
|
+
conn.on_unbind do
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module ExperellaProxy
|
2
|
+
# ExperellaProxy Gemversion
|
3
|
+
# 0.0.6
|
4
|
+
# * updated homepage
|
5
|
+
# 0.0.5
|
6
|
+
# * added :host_port option to backend configuration
|
7
|
+
# 0.0.4
|
8
|
+
# * added lambda for accept filtering
|
9
|
+
#
|
10
|
+
# 0.0.3
|
11
|
+
# * added self-signed SSL certificate for TLS/HTTPS
|
12
|
+
# * added config template init functionality
|
13
|
+
#
|
14
|
+
VERSION = "0.0.6"
|
15
|
+
end
|