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