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,557 @@
1
+ require 'uri'
2
+
3
+ module ExperellaProxy
4
+
5
+ # The proxies TCP Connection to the client
6
+ #
7
+ # Responsible for parsing and buffering the clients http requests,
8
+ # connecting to the backend server, sending data to the backend server and returning responses to the client.
9
+ #
10
+ # See EventMachine::Connection documentation for more information
11
+ #
12
+ # @see http://eventmachine.rubyforge.org/EventMachine/Connection.html EventMachine::Connection
13
+ class Connection < EventMachine::Connection
14
+ include ExperellaProxy::Globals
15
+
16
+ # Used to pass an optional block to the connection which will be executed when the {#connected} event occurs
17
+ #
18
+ # @example
19
+ # # called on successful backend connection
20
+ # # backend is the name of the connected server
21
+ # conn.on_connect do |backend|
22
+ #
23
+ # end
24
+ #
25
+ # @param blk [Block] a block to be executed
26
+ def on_connect(&blk)
27
+ @on_connect = blk
28
+ end
29
+
30
+ # Used to pass an optional block to the connection which will be executed when the {#receive_data} event occurs
31
+ #
32
+ # @example
33
+ # # modify / process request stream
34
+ # # and return modified data
35
+ # conn.on_data do |data|
36
+ # data
37
+ # end
38
+ #
39
+ # @param blk [Block] a block to be executed
40
+ # @return [String] the modified data
41
+ def on_data(&blk)
42
+ @on_data = blk
43
+ end
44
+
45
+ # Used to pass an optional block to the connection which will be executed when the {#relay_from_backend} event occurs
46
+ #
47
+ # @example
48
+ # # modify / process response stream
49
+ # # and return modified response
50
+ # # backend is the name of the connected server
51
+ # conn.on_response do |backend, resp|
52
+ # resp
53
+ # end
54
+ #
55
+ # @param blk [Block] a block to be executed
56
+ # @return [String] the modified response
57
+ def on_response(&blk)
58
+ @on_response = blk
59
+ end
60
+
61
+ # Used to pass an optional block to the connection which will be executed when the {#unbind_backend} event occurs
62
+ #
63
+ # @example
64
+ # # termination logic
65
+ # # backend is the name of the connected server
66
+ # conn.on_finish do |backend|
67
+ #
68
+ # end
69
+ #
70
+ # @param blk [Block] a block to be executed
71
+ def on_finish(&blk)
72
+ @on_finish = blk
73
+ end
74
+
75
+ # Used to pass an optional block to the connection which will be executed when the {#unbind} event occurs
76
+ #
77
+ # @example
78
+ # # called if client terminates connection
79
+ # # or timeout occurs
80
+ # conn.on_unbind do
81
+ #
82
+ # end
83
+ #
84
+ # @param blk [Block] a block to be executed
85
+ def on_unbind(&blk)
86
+ @on_unbind = blk
87
+ end
88
+
89
+ # calls EventMachine close_connection_after_writing method with 1 tick delay
90
+ # waits 1 tick to make sure reactor i/o does not have unnecessary loop delay
91
+ def close
92
+ @unbound = true
93
+ EM.next_tick(method(:close_connection_after_writing))
94
+ log.debug [msec, :unbind_client_after_writing, @signature.to_s]
95
+ end
96
+
97
+ # Connects self to a BackendServer object
98
+ #
99
+ # Any request mangling configured in {BackendServer#mangle} will be done here
100
+ #
101
+ # Method provides additional support for BackendServer's named "web".
102
+ # Host and Port will be determined through the Request instead of BackendServer settings.
103
+ #
104
+ # @param backend [BackendServer] the BackendServer object
105
+ def connect_backendserver(backend)
106
+ @backend = backend
107
+ connection_manager.free_connection(self)
108
+ #mangle http header if backend wants to
109
+ unless @backend.mangle.nil?
110
+ @backend.mangle.each do |k, v|
111
+ if v.respond_to?(:call)
112
+ get_request.update_header({k => v.call(get_request.header[k])})
113
+ else
114
+ get_request.update_header({k => v})
115
+ end
116
+ end
117
+ end
118
+
119
+ # reconstruct the request header
120
+ get_request.reconstruct_header
121
+
122
+ #special web support for unknown hosts
123
+ if @backend.name.eql?("web")
124
+ xport = get_request.header[:Host].match(/:[0-9]+/)
125
+ if xport.nil? || xport.to_s.empty?
126
+ xport = "80"
127
+ else
128
+ xport = xport.to_s.gsub(/:/, "")
129
+ end
130
+ xhost = get_request.header[:Host].gsub(":#{xport}", "")
131
+ server(@backend.name, :host => xhost, :port => xport)
132
+ else
133
+ server(@backend.name, :host => @backend.host, :port => @backend.port)
134
+ end
135
+ end
136
+
137
+ # Called by backend connections when the remote TCP connection attempt completes successfully.
138
+ #
139
+ # {#on_connect} block will be executed here
140
+ #
141
+ # This method triggers the {#relay_to_server} method
142
+ #
143
+ # @param name [String] name of the Server used for logging
144
+ def connected(name)
145
+ log.debug [msec, :connected]
146
+ @on_connect.call(name) if @on_connect
147
+ log.info msec + 'on_connect'.ljust(12) + " @" + @signature.to_s + ' ' + name
148
+ relay_to_server
149
+ end
150
+
151
+ # Used for accessing the connections first request
152
+ #
153
+ # Buffered requests must not be handled before first in done
154
+ #
155
+ # @return [Request] the Request to be handled
156
+ def get_request
157
+ @requests.first
158
+ end
159
+
160
+ # Called by the EventMachine loop whenever data has been received by the network connection.
161
+ # It is never called by user code. {#receive_data} is called with a single parameter,
162
+ # a String containing the network protocol data, which may of course be binary.
163
+ #
164
+ # Data gets passed to the specified {#on_data} block first
165
+ # Then data gets passed to the parser and the {#relay_to_server} method gets fired
166
+ #
167
+ # On Http::Parser::Error a 400 Bad Request error send to the client and the Connection will be closed
168
+ #
169
+ # @param data [String] Opaque incoming data
170
+ def receive_data(data)
171
+ log.debug [msec, :connection, data]
172
+ data = @on_data.call(data) if @on_data
173
+ begin
174
+ @request_parser << data
175
+ rescue Http::Parser::Error
176
+ log.warn [msec, "Parser error caused by invalid request data", "@#{@signature}"]
177
+ # on error unbind request_parser object, so additional data doesn't get parsed anymore
178
+ #
179
+ # assigning a string to the parser variable, will cause incoming data to get buffered
180
+ # imho this is a better solution than adding a condition for this rare error case
181
+ @request_parser = ""
182
+ send_data "HTTP/1.1 400 Bad Request\r\nVia: 1.1 experella\r\nConnection: close\r\n\r\n"
183
+ close
184
+ end
185
+
186
+ log.info msec + 'on_data'.ljust(12) + " @" + @signature.to_s
187
+ log.debug data
188
+ relay_to_server
189
+ end
190
+
191
+ # Called by {Backend} connections.
192
+ # Relays data from backend server to the client
193
+ #
194
+ # {#on_response} block will be executed here
195
+ #
196
+ # @param name [String] name of the Server used for logging
197
+ # @param data [String] opaque response data
198
+ def relay_from_backend(name, data)
199
+ log.info msec + 'on_response'.ljust(12) + " @" + @signature.to_s + " from #{name}"
200
+ log.debug msec + "#{name.inspect} " + data
201
+ @got_response = true
202
+ data = @on_response.call(name, data) if @on_response
203
+ get_request.response << data
204
+ end
205
+
206
+ # Initialize a {Backend} connection
207
+ #
208
+ # Can connect to host:port server address
209
+ #
210
+ # @param name [String] name of the Server used for logging
211
+ # @param opts [Hash] Hash containing connection parameters
212
+ def server(name, opts)
213
+ srv = EventMachine::bind_connect(opts[:bind_host], opts[:bind_port], opts[:host], opts[:port], Backend) do |c|
214
+ c.name = name
215
+ c.plexer = self
216
+ end
217
+
218
+ @server = srv
219
+ end
220
+
221
+ #
222
+ # ip, port of the connected client
223
+ #
224
+ def peer
225
+ @peer ||= begin
226
+ peername = get_peername
227
+ peername ? Socket.unpack_sockaddr_in(peername).reverse : nil
228
+ end
229
+ end
230
+
231
+ # Called by the event loop immediately after the network connection has been established,
232
+ # and before resumption of the network loop.
233
+ # This method is generally not called by user code, but is called automatically
234
+ # by the event loop. The base-class implementation is a no-op.
235
+ # This is a very good place to initialize instance variables that will
236
+ # be used throughout the lifetime of the network connection.
237
+ #
238
+ # This is currently used to initiate start_tls on @options[:tls] enabled
239
+ #
240
+ #
241
+ # @see #connection_completed
242
+ # @see #unbind
243
+ # @see #send_data
244
+ # @see #receive_data
245
+ def post_init
246
+ if @options[:tls]
247
+ log.info [msec, "starting tls handshake @#{@signature}"]
248
+ start_tls(:private_key_file => @options[:private_key_file], :cert_chain_file => @options[:cert_chain_file], :verify_peer => false)
249
+ end
250
+ end
251
+
252
+ #
253
+ # ip, port of the local server connect
254
+ #
255
+ def sock
256
+ @sock ||= begin
257
+ sockname = get_sockname
258
+ sockname ? Socket.unpack_sockaddr_in(sockname).reverse : nil
259
+ end
260
+ end
261
+
262
+
263
+ # Called by EventMachine when the SSL/TLS handshake has been completed, as a result of calling start_tls to
264
+ # initiate SSL/TLS on the connection.
265
+ #
266
+ # This callback exists because {#post_init} and connection_completed are not reliable for indicating when an
267
+ # SSL/TLS connection is ready to have its certificate queried for.
268
+ def ssl_handshake_completed
269
+ log.info [msec, "ssl_handshake_completed successful"]
270
+ end
271
+
272
+ # Called by backend connections whenever their connection is closed.
273
+ # The close can occur because the code intentionally closes it
274
+ # (using #close_connection and #close_connection_after_writing), because
275
+ # the remote peer closed the connection, or because of a network error.
276
+ #
277
+ # Therefor connection errors, reconnections and queues need to be handled here
278
+ #
279
+ # {#on_finish} block will be executed here
280
+ #
281
+ # @param name [String] name of the Server used for logging
282
+ def unbind_backend(name)
283
+
284
+ log.info msec + 'on_finish'.ljust(12) + " @" + @signature.to_s + " for #{name}" + " responded? " + @got_response.to_s
285
+ if @on_finish
286
+ @on_finish.call(name)
287
+ end
288
+
289
+ @server = nil
290
+
291
+ #if backend responded or client unbound connection (timeout probably triggers this too)
292
+ if @got_response || @unbound
293
+ log.info msec + "Request done! @" + @signature.to_s
294
+ log.debug [msec, :backend_unbound, @requests.size, "keep alive? " + get_request.keep_alive.to_s]
295
+ unless get_request.keep_alive
296
+ close
297
+ log.debug [msec, :connection_closed, "close non persistent connection after writing"]
298
+ end
299
+ @requests.shift #pop first element,request is done
300
+ @got_response = false #reset response flag
301
+
302
+ #free backend server and connect to next conn if matching conn exists
303
+ unless @backend.nil?
304
+ connect_next
305
+ end
306
+
307
+ #check if queued requests find a matching backend
308
+ unless @requests.empty? || @unbound
309
+ #try to dispatch first request to backend
310
+ dispatch_request
311
+ end
312
+ else
313
+ #handle no backend response here
314
+ log.error msec + "Error, backend didnt respond"
315
+ error_page = "HTTP/1.1 503 Service unavailable\r\nContent-Length: #{config.error_pages[503].length}\r\nContent-Type: text/html;charset=utf-8\r\nConnection: close\r\n\r\n"
316
+ unless get_request.header[:http_method].eql? "HEAD"
317
+ error_page << config.error_pages[503]
318
+ end
319
+ log.error [msec, :error_to_client, error_page]
320
+ send_data error_page
321
+
322
+ close
323
+ end
324
+ end
325
+
326
+ # Called by the EventMachine loop whenever the client connection is closed.
327
+ # The close can occur because the code intentionally closes it
328
+ # (using #close_connection and #close_connection_after_writing), because
329
+ # the remote peer closed the connection, or because of a network error.
330
+ #
331
+ # This is used to clean up associations made to the connection object while it was open.
332
+ #
333
+ # {#on_unbind} block will be executed here
334
+ #
335
+ def unbind
336
+ @unbound = true
337
+ @on_unbind.call if @on_unbind
338
+
339
+ log.info [msec, "Client connection unbound! @" + @signature.to_s]
340
+ #lazy evaluated. if first is true, second would cause a nil-pointer!
341
+ unless @requests.empty? || get_request.flushed?
342
+ log.debug [msec, @requests.inspect]
343
+ end
344
+ #delete conn from queue if still queued
345
+ connection_manager.free_connection(self)
346
+
347
+ # reconnect backend to new connection if this has not happened already
348
+ unless @backend.nil?
349
+ connect_next
350
+ end
351
+ # terminate any unfinished backend connections
352
+ unless @server.nil?
353
+ @server.close_connection_after_writing
354
+ end
355
+ end
356
+
357
+
358
+ private
359
+
360
+ # @private constructor, gets called by EventMachine::Connection's overwritten new method
361
+ # Initializes http parser and timeout_timer
362
+ #
363
+ # @param options [Hash] options Hash passed to the connection
364
+ def initialize(options)
365
+ @options = options
366
+ @backend = nil
367
+ @server = nil
368
+ @requests = [] #contains request objects
369
+ @unbound = false
370
+ @got_response = false
371
+ @request_parser = Http::Parser.new
372
+ init_http_parser
373
+ timeout_timer
374
+ @start = Time.now
375
+ end
376
+
377
+ # checks if the free backend matches any queued connection
378
+ # if there is a match, fire connection event to that connection
379
+ #
380
+ # @note DeHerr: imho ugly, initiating anothers connection backend from a connection feels just wrong
381
+ # did this for testability. 27.11.2013
382
+ #
383
+ def connect_next
384
+ #free backend server and connect to next conn if matching conn exists
385
+ next_conn = connection_manager.free_backend(@backend)
386
+ unless next_conn.nil?
387
+ next_conn.connect_backendserver(@backend)
388
+ end
389
+ @backend = nil
390
+ end
391
+
392
+ # Tries to dispatch the connections first request to a BackendServer object. Usually this should not be called
393
+ # more than once per request.
394
+ #
395
+ # connects to the backend if a BackendServer object is available
396
+ #
397
+ # logs if the connection got queued
398
+ #
399
+ # sends a 404 Error to client if no registered BackendServer matches the request
400
+ def dispatch_request
401
+ backend = connection_manager.backend_available?(get_request)
402
+
403
+ if backend.is_a?(BackendServer)
404
+ log.debug [msec + "Backend found"]
405
+ connect_backendserver(backend)
406
+ elsif backend == :queued
407
+ log.debug [msec + "pushed on queue: no backend available"]
408
+ else
409
+ log.error msec + "Error, send client error message and unbind! No backend will match"
410
+ error_page = "HTTP/1.1 404 Not Found\r\nContent-Length: #{config.error_pages[404].length}\r\nContent-Type: text/html;charset=utf-8\r\nConnection: close\r\n\r\n"
411
+ unless get_request.header[:http_method].eql? "HEAD"
412
+ error_page << config.error_pages[404]
413
+ end
414
+ log.error [msec, :error_to_client, error_page]
415
+ send_data error_page
416
+ close
417
+ end
418
+ end
419
+
420
+ # initializes http parser callbacks and blocks
421
+ def init_http_parser
422
+
423
+ @request_parser.on_message_begin = proc do
424
+ @requests.push(Request.new(self))
425
+ # this log also triggers if client sends new keep-alive request before backend was unbound
426
+ log.info [msec + "pipelined request"] if @requests.length > 1
427
+ log.debug [msec, :message_begin]
428
+ end
429
+
430
+ #called when request headers are completely parsed (first \r\n\r\n triggers this)
431
+ @request_parser.on_headers_complete = proc do |h|
432
+ log.info [msec, "new Request @#{@signature} Host: " + h["Host"] + " Request Path: " + @request_parser.request_url]
433
+ request = @requests.last
434
+
435
+ # cache if client wants persistent connection
436
+ if @request_parser.http_version[0] == 1 && @request_parser.http_version[1] == 0
437
+ request.keep_alive = false unless h["Connection"].to_s.downcase.eql? "keep-alive"
438
+ else
439
+ request.keep_alive = false if h["Connection"].to_s.downcase.include? "close"
440
+ end
441
+ request.update_header({:Connection => "close"}) #update Connection header to close for backends
442
+
443
+ # if there is a transfer-encoding, stream the message as Transfer-Encoding: chunked to backends
444
+ unless h["Transfer-Encoding"].nil?
445
+ h.delete("Content-Length")
446
+ request.chunked = true
447
+ request.update_header({:"Transfer-Encoding" => "chunked"})
448
+ end
449
+
450
+ # remove all hop-by-hop header fields
451
+ unless h["Connection"].nil?
452
+ h["Connection"].each do |s|
453
+ h.delete(s)
454
+ end
455
+ end
456
+ HOP_HEADERS.each do |s|
457
+ h.delete(s)
458
+ end
459
+
460
+
461
+ via = h.delete("Via")
462
+ if via.nil?
463
+ via = "1.1 experella"
464
+ else
465
+ via << "1.1 experella"
466
+ end
467
+ request.update_header({:Via => via})
468
+
469
+ request.update_header(h)
470
+ request.update_header({:http_version => @request_parser.http_version})
471
+ request.update_header({:http_method => @request_parser.http_method}) # for requests
472
+ request.update_header({:request_url => @request_parser.request_url})
473
+ if @request_parser.request_url.include? "http://"
474
+ u = URI.parse(@request_parser.request_url)
475
+ request.update_header(:Host => u.host)
476
+ log.debug [msec, "Host set to absolut host from request_url", u.host]
477
+ else
478
+ u = URI.parse("http://" + h["Host"] + @request_parser.request_url)
479
+ end
480
+
481
+ request.add_uri(:port => u.port, :path => u.path, :query => u.query)
482
+
483
+
484
+ # try to connect request to backend
485
+ # but only try to connect if this (.last) equals (.first), true at length == 1
486
+ # according to http-protocol requests must always be handled in order.
487
+ if @requests.length == 1
488
+ dispatch_request
489
+ end
490
+
491
+ log.debug [msec, :on_header_complete, @requests.size]
492
+ end
493
+
494
+ @request_parser.on_body = proc do |chunk|
495
+ request = @requests.last
496
+ if request.chunked
497
+ # add hexadecimal chunk size
498
+ request << chunk.size.to_s(16)
499
+ request << "\r\n"
500
+ request << chunk
501
+ request << "\r\n"
502
+ else
503
+ request << chunk
504
+ end
505
+ end
506
+
507
+ @request_parser.on_message_complete = proc do
508
+ request = @requests.last
509
+ if request.chunked
510
+ # add closing chunk
511
+ request << "0\r\n\r\n"
512
+ end
513
+ end
514
+
515
+ end
516
+
517
+ # This method sends the first requests send_buffer to the backend server, if
518
+ # any backend is set and there is request data to dispatch
519
+ #
520
+ # Request header will be reconstructed here before dispatch
521
+ #
522
+ # If the backend server is not yet connected, data is already buffered to be sent when the connection gets established
523
+ #
524
+ def relay_to_server
525
+ log.debug [:relay_to_server, "@" + @signature.to_s, @backend, @requests.empty?, get_request.flushed?, @server.nil?]
526
+ if @backend && !@requests.empty? && !get_request.flushed? && !@server.nil?
527
+ # save some memory here if logger isn't set on debug
528
+ if log.debug?
529
+ data = get_request.flush
530
+ @server.send_data data
531
+ log.debug [msec, :data_to_server_send, "@" + @signature.to_s, data]
532
+ else
533
+ @server.send_data get_request.flush
534
+ end
535
+ end
536
+ end
537
+
538
+ #returns milliseconds since connection startup as string
539
+ def msec
540
+ (((Time.now.tv_sec - @start.tv_sec) * 1000) + ((Time.now.tv_usec - @start.tv_usec) / 1000.0)).to_s + "ms: "
541
+ end
542
+
543
+ # starts the timeout timer and closes connection if timeout was exceeded
544
+ def timeout_timer
545
+ timer = EventMachine::PeriodicTimer.new(1) do
546
+ if get_idle_time.nil?
547
+ timer.cancel
548
+ elsif get_idle_time > config.timeout
549
+ log.info [msec, :unbind_client, :timeout, "@" + @signature.to_s]
550
+ timer.cancel
551
+ close
552
+ end
553
+ end
554
+ end
555
+
556
+ end
557
+ end