bane 0.3.0 → 0.4.0

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 (61) hide show
  1. checksums.yaml +15 -0
  2. data/README.md +16 -6
  3. data/Rakefile +1 -1
  4. data/TODO +5 -8
  5. data/bin/bane +4 -12
  6. data/examples/multiple_behaviors.rb +7 -6
  7. data/examples/readme_example.rb +2 -1
  8. data/examples/single_behavior.rb +2 -1
  9. data/lib/bane.rb +18 -5
  10. data/lib/bane/arguments_parser.rb +73 -0
  11. data/lib/bane/behavior_maker.rb +39 -0
  12. data/lib/bane/behaviors/responders/close_after_pause.rb +21 -0
  13. data/lib/bane/behaviors/responders/close_immediately.rb +14 -0
  14. data/lib/bane/behaviors/responders/deluge_response.rb +27 -0
  15. data/lib/bane/behaviors/responders/echo_response.rb +16 -0
  16. data/lib/bane/behaviors/responders/exported.rb +9 -0
  17. data/lib/bane/behaviors/responders/fixed_response.rb +25 -0
  18. data/lib/bane/behaviors/responders/for_each_line.rb +18 -0
  19. data/lib/bane/behaviors/responders/http_refuse_all_credentials.rb +31 -0
  20. data/lib/bane/behaviors/responders/never_respond.rb +27 -0
  21. data/lib/bane/behaviors/responders/newline_response.rb +18 -0
  22. data/lib/bane/behaviors/responders/random_response.rb +24 -0
  23. data/lib/bane/behaviors/responders/slow_response.rb +32 -0
  24. data/lib/bane/behaviors/servers/exported.rb +18 -0
  25. data/lib/bane/behaviors/servers/responder_server.rb +50 -0
  26. data/lib/bane/behaviors/servers/timeout_in_listen_queue.rb +51 -0
  27. data/lib/bane/command_line_configuration.rb +22 -79
  28. data/lib/bane/extensions.rb +7 -0
  29. data/test/bane/acceptance_test.rb +65 -0
  30. data/test/bane/arguments_parser_test.rb +76 -0
  31. data/test/bane/bane_test.rb +12 -0
  32. data/test/bane/behaviors/responders/close_after_pause_test.rb +30 -0
  33. data/test/bane/behaviors/responders/close_immediately_test.rb +14 -0
  34. data/test/bane/behaviors/responders/deluge_response_test.rb +20 -0
  35. data/test/bane/behaviors/responders/echo_response_test.rb +16 -0
  36. data/test/bane/behaviors/responders/fixed_response_test.rb +14 -0
  37. data/test/bane/behaviors/responders/for_each_line_test.rb +29 -0
  38. data/test/bane/behaviors/responders/http_refuse_all_credentials_test.rb +18 -0
  39. data/test/bane/behaviors/responders/never_respond_test.rb +31 -0
  40. data/test/bane/behaviors/responders/newline_response_test.rb +14 -0
  41. data/test/bane/behaviors/responders/random_response_test.rb +15 -0
  42. data/test/bane/behaviors/responders/slow_response_test.rb +21 -0
  43. data/test/bane/behaviors/servers/responder_server_test.rb +77 -0
  44. data/test/bane/behaviors/servers/timeout_in_listen_queue_test.rb +23 -0
  45. data/test/bane/command_line_configuration_test.rb +54 -72
  46. data/test/bane/extensions_test.rb +17 -0
  47. data/test/bane/fake_connection_test.rb +1 -1
  48. data/test/bane/launchable_role_tests.rb +20 -0
  49. data/test/bane/naive_http_response_test.rb +1 -1
  50. data/test/test_helper.rb +42 -1
  51. metadata +143 -99
  52. data/lib/bane/behavior_server.rb +0 -47
  53. data/lib/bane/behaviors.rb +0 -171
  54. data/lib/bane/compatibility.rb +0 -36
  55. data/lib/bane/configuration_parser.rb +0 -89
  56. data/lib/bane/service_registry.rb +0 -21
  57. data/test/bane/behavior_server_test.rb +0 -59
  58. data/test/bane/behaviors_test.rb +0 -135
  59. data/test/bane/configuration_parser_test.rb +0 -111
  60. data/test/bane/integration_test.rb +0 -92
  61. data/test/bane/service_registry_test.rb +0 -20
@@ -0,0 +1,18 @@
1
+ module Bane
2
+ module Behaviors
3
+ module Responders
4
+
5
+ # This module can be used to wrap another behavior with
6
+ # a "while(io.gets)" loop, which reads a line from the input and
7
+ # then performs the given behavior.
8
+ module ForEachLine
9
+ def serve(io)
10
+ while (io.gets)
11
+ super(io)
12
+ end
13
+ end
14
+ end
15
+
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,31 @@
1
+ module Bane
2
+ module Behaviors
3
+ module Responders
4
+
5
+ # Sends an HTTP 401 response (Unauthorized) for every request. This
6
+ # attempts to mimic an HTTP server by reading a line (the request)
7
+ # and then sending the response. This behavior responds to all
8
+ # incoming request URLs on the running port.
9
+ class HttpRefuseAllCredentials
10
+ UNAUTHORIZED_RESPONSE_BODY = <<EOF
11
+ <!DOCTYPE html>
12
+ <html>
13
+ <head>
14
+ <title>Bane Server</title>
15
+ </head>
16
+ <body>
17
+ <h1>Unauthorized</h1>
18
+ </body>
19
+ </html>
20
+ EOF
21
+
22
+ def serve(io)
23
+ io.gets # Read the request before responding
24
+ response = NaiveHttpResponse.new(401, "Unauthorized", "text/html", UNAUTHORIZED_RESPONSE_BODY)
25
+ io.write(response.to_s)
26
+ end
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,27 @@
1
+ module Bane
2
+ module Behaviors
3
+ module Responders
4
+
5
+ # Accepts a connection and never sends a byte of data. The connection is
6
+ # left open indefinitely.
7
+ class NeverRespond
8
+ READ_TIMEOUT_IN_SECONDS = 2
9
+ MAXIMUM_BYTES_TO_READ = 4096
10
+
11
+ def serve(io)
12
+ loop do
13
+ begin
14
+ io.read_nonblock(MAXIMUM_BYTES_TO_READ)
15
+ rescue Errno::EAGAIN
16
+ IO.select([io], nil, nil, READ_TIMEOUT_IN_SECONDS)
17
+ retry # Ignore the result of IO select since we retry reads regardless of if there's data to be read or not
18
+ rescue EOFError
19
+ break
20
+ end
21
+ end
22
+ end
23
+
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,18 @@
1
+ module Bane
2
+ module Behaviors
3
+ module Responders
4
+
5
+ # Sends a newline character as the only response
6
+ class NewlineResponse
7
+ def serve(io)
8
+ io.write "\n"
9
+ end
10
+ end
11
+
12
+ class NewlineResponseForEachLine < NewlineResponse
13
+ include ForEachLine
14
+ end
15
+
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,24 @@
1
+ module Bane
2
+ module Behaviors
3
+ module Responders
4
+
5
+ # Sends a random response.
6
+ class RandomResponse
7
+ def serve(io)
8
+ io.write random_string
9
+ end
10
+
11
+ private
12
+ def random_string
13
+ (1..rand(26)+1).map { |i| ('a'..'z').to_a[rand(26)] }.join
14
+ end
15
+
16
+ end
17
+
18
+ class RandomResponseForEachLine < RandomResponse
19
+ include ForEachLine
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,32 @@
1
+ module Bane
2
+ module Behaviors
3
+ module Responders
4
+
5
+ # Sends a fixed response character-by-character, pausing between each character.
6
+ #
7
+ # Options:
8
+ # - message: The response to send. Default: "Hello, world!"
9
+ # - pause_duration: The number of seconds to pause between each character. Default: 10 seconds
10
+ class SlowResponse
11
+ def initialize(options = {})
12
+ @options = {message: "Hello, world!", pause_duration: 10}.merge(options)
13
+ end
14
+
15
+ def serve(io)
16
+ message = @options[:message]
17
+ pause_duration = @options[:pause_duration]
18
+
19
+ message.each_char do |char|
20
+ io.write char
21
+ sleep pause_duration
22
+ end
23
+ end
24
+ end
25
+
26
+ class SlowResponseForEachLine < SlowResponse
27
+ include ForEachLine
28
+ end
29
+
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,18 @@
1
+ module Bane
2
+ module Behaviors
3
+ module Servers
4
+
5
+ EXPORTED = [TimeoutInListenQueue]
6
+
7
+ # Listen only on localhost
8
+ LOCALHOST = '127.0.0.1'
9
+
10
+ # Deprecated - use LOCALHOST - Listen only on localhost
11
+ DEFAULT_HOST = LOCALHOST
12
+
13
+ # Listen on all interfaces
14
+ ALL_INTERFACES = '0.0.0.0'
15
+
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,50 @@
1
+ require 'gserver'
2
+
3
+ module Bane
4
+ module Behaviors
5
+ module Servers
6
+
7
+ class ResponderServer < GServer
8
+
9
+ def initialize(port, behavior, host = Servers::LOCALHOST)
10
+ super(port, host)
11
+ @behavior = behavior
12
+ self.audit = true
13
+ end
14
+
15
+ def serve(io)
16
+ @behavior.serve(io)
17
+ end
18
+
19
+ def to_s
20
+ "<Bane::BehaviorServer: port=#{@port}, behavior=#{@behavior.class}>"
21
+ end
22
+
23
+ protected
24
+
25
+ alias_method :original_log, :log
26
+
27
+ def log(message)
28
+ original_log("#{@behavior.class.unqualified_name} #{@host}:#{@port} #{message}")
29
+ end
30
+
31
+ def connecting(client)
32
+ addr = client.peeraddr
33
+ log("client:#{addr[1]} #{addr[2]}<#{addr[3]}> connect")
34
+ end
35
+
36
+ def disconnecting(client_port)
37
+ log("client:#{client_port} disconnect")
38
+ end
39
+
40
+ def starting
41
+ log('start')
42
+ end
43
+
44
+ def stopping
45
+ log('stop')
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,51 @@
1
+ require 'socket'
2
+
3
+ module Bane
4
+ module Behaviors
5
+ module Servers
6
+
7
+ class TimeoutInListenQueue
8
+
9
+ def initialize(port, host = Servers::LOCALHOST)
10
+ @port = port
11
+ @host = host
12
+ self.stdlog= $stderr
13
+ end
14
+
15
+ def start
16
+ @server = Socket.new(:INET, :STREAM)
17
+ address = Socket.sockaddr_in(port, host)
18
+ @server.bind(address) # Note that we never call listen
19
+
20
+ log 'started'
21
+ end
22
+
23
+ def join
24
+ sleep
25
+ end
26
+
27
+ def stop
28
+ @server.close
29
+ log 'stopped'
30
+ end
31
+
32
+ def stdlog=(logger)
33
+ @logger = logger
34
+ end
35
+
36
+ def self.make(port, host)
37
+ new(port, host)
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :host, :port, :logger
43
+
44
+ def log(message)
45
+ logger.puts "[#{Time.new.ctime}] #{self.class.unqualified_name} #{host}:#{port} #{message}"
46
+ end
47
+ end
48
+
49
+ end
50
+ end
51
+ end
@@ -1,96 +1,39 @@
1
- require 'optparse'
2
-
3
1
  module Bane
4
- class CommandLineConfiguration
5
- def initialize()
6
- @options = { :host => BehaviorServer::DEFAULT_HOST }
7
- @option_parser = init_option_parser
8
- end
9
-
10
- def parse(args)
11
- parse_options(@options, args)
12
-
13
- return [] if (args.empty?)
14
-
15
- port = parse_port(args[0])
16
- behaviors = parse_behaviors(args.drop(1))
17
-
18
- behaviors = ServiceRegistry.all_servers if behaviors.empty?
19
- LinearPortMappedBehaviorConfiguration.new(port, behaviors, @options[:host]).servers
20
- end
21
-
22
- def usage
23
- @option_parser.help
24
- end
25
-
26
- private
27
-
28
- def init_option_parser
29
- OptionParser.new do |opts|
30
- opts.banner = "Usage: bane [options] port [behaviors]"
31
- opts.separator ""
32
- opts.on("-l", "--listen-on-localhost",
33
- "Listen on localhost, (#{BehaviorServer::DEFAULT_HOST}). [default]") do
34
- @options[:host] = BehaviorServer::DEFAULT_HOST
35
- end
36
- opts.on("-a", "--listen-on-all-hosts", "Listen on all interfaces, (#{BehaviorServer::ALL_INTERFACES})") do
37
- @options[:host] = BehaviorServer::ALL_INTERFACES
38
- end
39
- opts.separator ""
40
- opts.separator "All behaviors:"
41
- opts.separator ServiceRegistry.all_server_names.map { |title| " - #{title}" }.join("\n")
42
2
 
43
- end
44
- end
45
-
46
- def parse_options(options, args)
47
- @option_parser.parse!(args)
48
- rescue OptionParser::InvalidOption => io
49
- raise ConfigurationError, io.message
50
- end
3
+ def self.find_makeables
4
+ Hash[Bane::Behaviors::Responders::EXPORTED.map { |responder| [responder.unqualified_name, ResponderMaker.new(responder)] }]
5
+ .merge(Hash[Bane::Behaviors::Servers::EXPORTED.map { |server| [server.unqualified_name, server] }])
6
+ end
51
7
 
52
- def parse_port(port)
53
- Integer(port)
54
- rescue ArgumentError => ae
55
- raise ConfigurationError, "Invalid port number: #{port}"
56
- end
8
+ class CommandLineConfiguration
57
9
 
58
- def parse_behaviors(behavior_names)
59
- behavior_names.map { |behavior| find(behavior) }
60
- rescue UnknownBehaviorError => ube
61
- raise ConfigurationError, ube.message
10
+ def initialize(makeables)
11
+ @behavior_maker = BehaviorMaker.new(makeables)
12
+ @arguments_parser = ArgumentsParser.new(makeables.keys)
62
13
  end
63
14
 
64
- def find(behavior)
65
- raise UnknownBehaviorError.new(behavior) unless Behaviors.const_defined?(behavior)
66
- Behaviors.const_get(behavior)
15
+ def process(args, &error_policy)
16
+ arguments = @arguments_parser.parse(args)
17
+ create(arguments.behaviors, arguments.port, arguments.host)
18
+ rescue ConfigurationError => ce
19
+ error_policy.call([ce.message, @arguments_parser.usage].join("\n"))
67
20
  end
68
21
 
69
- class LinearPortMappedBehaviorConfiguration
70
- def initialize(port, behaviors, host)
71
- @starting_port = port
72
- @behaviors = behaviors
73
- @host = host
74
- end
22
+ private
75
23
 
76
- def servers
77
- configurations = []
78
- @behaviors.each_with_index do |behavior, index|
79
- configurations << BehaviorServer.new(@starting_port + index, behavior.new, @host)
80
- end
81
- configurations
24
+ def create(behaviors, port, host)
25
+ if behaviors.any?
26
+ @behavior_maker.create(behaviors, port, host)
27
+ else
28
+ @behavior_maker.create_all(port, host)
82
29
  end
83
-
30
+ rescue UnknownBehaviorError => ube
31
+ raise ConfigurationError, ube.message
84
32
  end
85
33
 
86
34
  end
87
35
 
88
- class ConfigurationError < RuntimeError; end
89
-
90
- class UnknownBehaviorError < RuntimeError
91
- def initialize(name)
92
- super "Unknown behavior: #{name}"
93
- end
36
+ class ConfigurationError < RuntimeError;
94
37
  end
95
38
 
96
39
  end
@@ -0,0 +1,7 @@
1
+ unless Class.respond_to?(:unqualified_name)
2
+ class Class
3
+ def unqualified_name
4
+ self.name.split("::").last
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,65 @@
1
+ require_relative '../test_helper'
2
+ require 'mocha/setup'
3
+
4
+ class BaneAcceptanceTest < Test::Unit::TestCase
5
+
6
+ include ServerTestHelpers
7
+
8
+ TEST_PORT = 4000
9
+
10
+ def test_uses_specified_port_and_server
11
+ run_server_with(TEST_PORT, Bane::Behaviors::Responders::FixedResponse) do
12
+ with_response_from TEST_PORT do |response|
13
+ assert !response.empty?, "Should have had a non-empty response"
14
+ end
15
+ end
16
+ end
17
+
18
+ def test_serves_http_requests
19
+ run_server_with(TEST_PORT, Bane::Behaviors::Responders::HttpRefuseAllCredentials) do
20
+ assert_match /401/, status_returned_from("http://localhost:#{TEST_PORT}/some/url")
21
+ end
22
+ end
23
+
24
+ def test_supports_command_line_interface
25
+ run_server_with_cli_arguments(["--listen-on-localhost", TEST_PORT, "FixedResponse"]) do
26
+ with_response_from TEST_PORT do |response|
27
+ assert !response.empty?, "Should have had a non-empty response"
28
+ end
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def run_server_with(port, behavior, &block)
35
+ behavior = Bane::Behaviors::Servers::ResponderServer.new(port, behavior.new)
36
+ launcher = Bane::Launcher.new([behavior], quiet_logger)
37
+ launch_and_stop_safely(launcher, &block)
38
+ sleep 0.1 # Until we can fix the GServer stopping race condition (Issue #7)
39
+ end
40
+
41
+ def run_server_with_cli_arguments(arguments, &block)
42
+ config = Bane::CommandLineConfiguration.new(Bane.find_makeables)
43
+ launcher = Bane::Launcher.new(config.process(arguments), quiet_logger) { |error| raise error }
44
+ launch_and_stop_safely(launcher, &block)
45
+ sleep 0.1 # Until we can fix the GServer stopping race condition (Issue #7)
46
+ end
47
+
48
+ def quiet_logger
49
+ StringIO.new
50
+ end
51
+
52
+ def status_returned_from(uri)
53
+ Net::HTTP.get_response(URI(uri)).code
54
+ end
55
+
56
+ def with_response_from(port)
57
+ begin
58
+ connection = TCPSocket.new "localhost", port
59
+ yield connection.read
60
+ ensure
61
+ connection.close if connection
62
+ end
63
+ end
64
+
65
+ end