equity 0.5

Sign up to get free protection for your applications and to get access to all the features.
data/bin/equity ADDED
@@ -0,0 +1,294 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # equity 0.5 - queueing software load balancer with transmission inspection
4
+ # and simple HTTP header rewriting
5
+ #
6
+ # Usage: equity [-dp] [-h "Header: Value"] <listen-port> [<node-address>:]<node-port> ...
7
+ # Run equity with no arguments for detailed usage information.
8
+ #
9
+
10
+ require 'socket'
11
+ require 'equity/node'
12
+ require 'equity/manager'
13
+ require 'getoptlong'
14
+
15
+ # Returns a string that can be used to identify a particular node in debugging
16
+ # messages. At the moment this returns the address and port of the client being
17
+ # served by the node.
18
+ def client_id(node)
19
+ if node.connected?
20
+ "#{node.client_address}:#{node.client_port}"
21
+ else
22
+ "not connected"
23
+ end
24
+ end
25
+
26
+ # Prints a debug message if debugging is enabled.
27
+ def debug(socket, message)
28
+ return unless $debugging
29
+ if socket
30
+ if node = Equity::Node.with_socket(socket)
31
+ client_id = client_id(node)
32
+ else
33
+ client_id = nil
34
+ begin
35
+ nameinfo = Socket.getnameinfo(socket.getpeername,
36
+ Socket::NI_NUMERICHOST | Socket::NI_NUMERICSERV)
37
+ client_id = "#{nameinfo[0]}:#{nameinfo[1]}"
38
+ rescue
39
+ client_id = "???:???"
40
+ end
41
+ end
42
+ print "[#{client_id}] "
43
+ end
44
+ puts message
45
+ end
46
+
47
+ # Prints data in hex and ASCII if packet dumping is enabled.
48
+ BYTES_PER_PACKET_LINE = 16
49
+ SAFE_PACKET_RX = (32..126).to_a.collect! {|n| n.chr}.join("").sub!(/[\[\]]/, '\\\1')
50
+ def dump_packet(data, src, dst)
51
+ return unless $dump_packets
52
+
53
+ node = Equity::Node.with_socket(src) || return
54
+ to_server = (src == node.sockets.first)
55
+ arrow = (to_server ? "--->" : "<---")
56
+ print "[#{client_id(node)}] #{arrow} #{node.address}:#{node.port}\n\n"
57
+
58
+ data = data.dup
59
+ until data.empty?
60
+ bytes = data.slice!(0, BYTES_PER_PACKET_LINE)
61
+ hex = bytes.unpack("H*").first + (" " * (BYTES_PER_PACKET_LINE - bytes.length))
62
+ hex1 = hex.slice!(0, BYTES_PER_PACKET_LINE)
63
+ hex2 = hex
64
+ hex1.gsub!(/(....)/, '\1 ')
65
+ hex2.gsub!(/(....)/, '\1 ')
66
+ bytes.gsub!(/[^#{SAFE_PACKET_RX}]/, ".")
67
+ puts " #{hex1} #{hex2} #{bytes}"
68
+ end
69
+ print "\n"
70
+ end
71
+
72
+ # Handles rewriting of HTTP request headers. This method isn't very smart about
73
+ # how it detects HTTP requests, but should work for most situations.
74
+ def rewrite_headers!(data, src)
75
+ return if $header_rewrites.empty?
76
+ return unless http_slash_idx = data.index(" HTTP/")
77
+ node = Equity::Node.with_socket(src) || return
78
+ return unless (src == node.sockets.first)
79
+
80
+ debug(src, "rewriting HTTP request headers")
81
+
82
+ before_headers = data.slice!(0, data.index("\r\n", http_slash_idx))
83
+ headers = data.slice!(0, data.index("\r\n\r\n") + 4)
84
+ after_headers = data
85
+
86
+ $header_rewrites.each do |header, value|
87
+ if headers.sub!(/\r\n#{header}: (.+?)\r\n/m, "\r\n#{header}: #{value}\r\n")
88
+ debug(src, " #{header}: #{$1} => #{value}")
89
+ end
90
+ end
91
+
92
+ headers.chop!
93
+ $header_additions.each do |header, value|
94
+ next if headers =~ /\r\n#{header}: /m
95
+ headers += "#{header}: #{value}\r\n"
96
+ debug(src, " #{header}: => #{value}")
97
+ end
98
+ headers += "\r\n"
99
+
100
+ data.replace(before_headers + headers + after_headers)
101
+ rescue
102
+ # If an exception occurs, ignore it. Rewriting the headers is far less
103
+ # important than keeping the program alive.
104
+ end
105
+
106
+
107
+ # Process arguments.
108
+ $debugging = !!ARGV.delete('-d')
109
+ $dump_packets = !!ARGV.delete('-p')
110
+ $header_rewrites = {}
111
+ $header_additions = {}
112
+
113
+ options = GetoptLong.new(
114
+ ["-d", "--debug", GetoptLong::NO_ARGUMENT],
115
+ ["-p", "--packets", GetoptLong::NO_ARGUMENT],
116
+ ["-h", "--header", GetoptLong::REQUIRED_ARGUMENT],
117
+ ["-H", "--add-header", GetoptLong::REQUIRED_ARGUMENT]
118
+ )
119
+ options.each do |option, argument|
120
+ case option
121
+ when "-d"
122
+ $debugging = true
123
+ when "-p"
124
+ $dump_packets = true
125
+ when "-h", "-H"
126
+ header = argument.split(/: */, 2)
127
+ unless header.length == 2
128
+ STDERR.puts "I don't understand this header rewrite:"
129
+ STDERR.puts " #{argument}"
130
+ STDERR.puts "Run equity with no arguments for help."
131
+ exit(64) # EX_USAGE
132
+ end
133
+ $header_rewrites[header.first] = header.last
134
+ $header_additions[header.first] = header.last if option == "-H"
135
+ end
136
+ end
137
+
138
+ if ARGV.length < 2
139
+ STDERR.puts 'equity 0.5'
140
+ STDERR.puts 'Usage: equity [<options>] <listen-port> [<node-address>:]<node-port> ...'
141
+ STDERR.print "\n"
142
+ STDERR.puts 'Node addresses default to localhost. Node ports can be single port numbers, or'
143
+ STDERR.puts 'you can specify a range like so: 10001-10010'
144
+ STDERR.print "\n"
145
+ STDERR.puts 'The following options can be used:'
146
+ STDERR.puts '-d Debug mode. Stays in the foreground and prints status messages.'
147
+ STDERR.puts '-p Packet dumper. Stays in the foreground and displays the raw data being'
148
+ STDERR.puts ' transferred.'
149
+ STDERR.puts '-h "<header>: <value>" Rewrite an HTTP request header before retransmitting.'
150
+ STDERR.puts ' Can be used more than once to rewrite several headers. The space after the'
151
+ STDERR.puts ' colon is optional.'
152
+ STDERR.puts '-H Same as -h, but the header will be added to every HTTP request (as opposed'
153
+ STDERR.puts ' to only being rewritten if it was in the original request).'
154
+ STDERR.print "\n"
155
+ exit(64) # EX_USAGE
156
+ end
157
+
158
+ # Instantiate nodes.
159
+ $listen_port = ARGV.shift.to_i
160
+
161
+ $nodes = []
162
+ ARGV.each do |node_spec|
163
+ address, port = node_spec.split(':', 2)
164
+ if port.nil?
165
+ port = address
166
+ address = 'localhost'
167
+ end
168
+ if port =~ /^(\d+)-(\d+)$/
169
+ port_range = ($1.to_i)..($2.to_i)
170
+ port_range.each do |p|
171
+ $nodes << Equity::Node.new(address, p)
172
+ end
173
+ else
174
+ $nodes << Equity::Node.new(address, port)
175
+ end
176
+ end
177
+
178
+ # Daemonize.
179
+ unless $debugging || $dump_packets
180
+ fork && exit
181
+ Process.setsid
182
+ trap 'SIGHUP', 'IGNORE'
183
+ fork && exit
184
+ Dir.chdir '/tmp'
185
+ File.umask 0000
186
+ ObjectSpace.each_object(IO) do |io|
187
+ unless [STDIN, STDOUT, STDERR].include?(io)
188
+ io.close rescue nil
189
+ end
190
+ end
191
+ STDIN.reopen "/dev/null"
192
+ STDOUT.reopen "/dev/null", "a"
193
+ STDERR.reopen STDOUT
194
+ end
195
+
196
+ # Print debugging info for header rewrites.
197
+ $header_rewrites.each do |header, value|
198
+ if $header_additions.keys.include?(header)
199
+ debug(nil, "Will add HTTP request header `#{header}' with value `#{value}'")
200
+ else
201
+ debug(nil, "Will rewrite value of HTTP request header `#{header}' to `#{value}'")
202
+ end
203
+ end
204
+
205
+ # Set up SIGINT handler to print node counters.
206
+ trap 'SIGINT', Proc.new {
207
+ debug(nil, "\nNode Counters")
208
+ $nodes.each do |node|
209
+ debug(nil, "#{node} - #{node.counter} connections")
210
+ end
211
+ exit
212
+ }
213
+
214
+ # Start up control server.
215
+ $control_server = Equity::Manager.new($nodes, $listen_port, $debugging)
216
+
217
+ # Start up server.
218
+ $client_queue = []
219
+
220
+ $server = TCPServer.new(nil, $listen_port)
221
+ $server.listen(5)
222
+
223
+ debug(nil, "Ready on port #{$listen_port}")
224
+
225
+ # Loop forever.
226
+ while true
227
+ # Wait for one or more sockets to be readable.
228
+ sockets = [$server, $nodes.collect {|n| n.sockets}]
229
+ sockets.flatten!
230
+ selected = select(sockets, nil, nil, 5)
231
+ if selected
232
+ selected[0].each do |socket|
233
+ if socket == $server
234
+ # Incoming connection. Accept it and queue it.
235
+ client = $server.accept
236
+ $client_queue << client
237
+ debug(client, 'connection accepted')
238
+ else
239
+ # Data received. Transfer it to the socket's mate.
240
+ node = $nodes.find {|n| n.owns_socket?(socket)}
241
+ next if node.nil?
242
+ begin
243
+ data = socket.recvfrom(65536)[0]
244
+ raise Errno::EPIPE if data.empty?
245
+ mate = socket.mate
246
+ rewrite_headers!(data, socket)
247
+ dump_packet(data, socket, mate)
248
+ mate.write(data)
249
+ rescue Errno::EPIPE, Errno::ECONNRESET
250
+ # Connection closed. Disconnect this node.
251
+ debug(socket, 'disconnected')
252
+ node.disconnect
253
+ end
254
+ end
255
+ end
256
+ end
257
+
258
+ # Dequeue as many clients as possible, showing favor to nodes that have
259
+ # handled the fewest clients.
260
+ while !$client_queue.empty?
261
+ client = $client_queue.shift
262
+ found_a_node = false
263
+ failure_count = 0
264
+ sorted_notes = $nodes.sort {|a,b| a.counter <=> b.counter}
265
+ sorted_notes.each do |node|
266
+ next if node.connected?
267
+ begin
268
+ node.connect(client)
269
+ rescue Exception => e
270
+ debug(client, "connection to #{node} failed: #{e.message}")
271
+ failure_count += 1
272
+ next
273
+ end
274
+ debug(client, "assigned to #{node}")
275
+ found_a_node = true
276
+ break
277
+ end
278
+
279
+ $client_queue.unshift(client) unless found_a_node
280
+
281
+ if failure_count == $nodes.length
282
+ # All nodes failed. Disconnect all queued clients.
283
+ debug(nil, 'All nodes failed')
284
+ $client_queue.each {|client| client.close}
285
+ $client_queue.replace []
286
+ end
287
+
288
+ break unless found_a_node
289
+ end
290
+
291
+ # Check for control packets.
292
+ $control_server.process_commands
293
+ exit if $nodes.all? {|node| node.shutdown? && !node.connected?}
294
+ end
data/bin/equityctl ADDED
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # equityctl - management client for equity
4
+ #
5
+ # Usage: equityctl [<host>:]<port> <command>
6
+ #
7
+ # The host defaults to localhost.
8
+ #
9
+ # Command Description
10
+ # status Displays each node's address, port, connection status, and
11
+ # connection counter.
12
+ #
13
+ # Equity uses UDP packets to send control information, so all of UDP's caveats
14
+ # apply to equityctl.
15
+ #
16
+
17
+ require 'equity/controller'
18
+ require 'getoptlong'
19
+
20
+ # Process options.
21
+ options = GetoptLong.new(
22
+ ["--genkey", GetoptLong::NO_ARGUMENT]
23
+ )
24
+ options.each do |option, argument|
25
+ case option
26
+ when "--genkey"
27
+ $genkey = true
28
+ end
29
+ end
30
+
31
+ if ($genkey && !ARGV.empty?) || (!$genkey && ARGV.length != 2)
32
+ STDERR.puts 'Usage: equityctl [<host>:]<port> <command>'
33
+ STDERR.print "\n"
34
+ STDERR.puts 'The host defaults to localhost.'
35
+ STDERR.print "\n"
36
+ STDERR.puts 'Command Description'
37
+ STDERR.puts 'status Displays each node\'s address, port, connection status,'
38
+ STDERR.puts ' connection counter, and failure counter.'
39
+ STDERR.puts 'shutdown Close connections immediately and shut down.'
40
+ STDERR.puts 'waitdown Wait for connections to close, then shut down.'
41
+ STDERR.print "\n\n"
42
+ STDERR.puts 'Usage: equityctl --genkey'
43
+ STDERR.print "\n"
44
+ STDERR.puts 'With the --genkey option, equityctl generates a public/private key pair for'
45
+ STDERR.puts 'authenticating itself to equity.'
46
+ exit(64) # EX_USAGE
47
+ end
48
+
49
+ # If --genkey was specified, generate a key pair and exit.
50
+ if $genkey
51
+ puts "Generating key..."
52
+ begin
53
+ key = Equity::Controller::Key.generate
54
+ rescue Exception => e
55
+ STDERR.puts e.message
56
+ exit(1)
57
+ end
58
+ puts "Done! For reference, your key is stored here:"
59
+ key.paths.each {|path| puts " #{path}"}
60
+ # Tell the user where they need to put their public key in order for it to be
61
+ # useful.
62
+ puts "\nTo be authorized to use equityctl with your own equity processes, your public"
63
+ puts "key must be appended to:"
64
+ puts " " + Equity::Controller::Key::PERSONAL_AUTHORIZED_KEYS_PATH
65
+ puts "\nTo be authorized to use equityctl with other people's equity processes, your"
66
+ puts "public key must be appended to:"
67
+ puts " " + Equity::Controller::Key::SYSTEM_AUTHORIZED_KEYS_PATH
68
+ exit
69
+ end
70
+
71
+ # Extract address, port, and command from arguments.
72
+ address, port = ARGV.shift.split(':', 2)
73
+ if port.nil?
74
+ port = address
75
+ address = 'localhost'
76
+ end
77
+
78
+ command = ARGV.shift
79
+
80
+ # Send command.
81
+ class NoResponseError < Exception; end
82
+
83
+ controller = Equity::Controller.new(address, port.to_i)
84
+ begin
85
+ case command
86
+ when 'status'
87
+ nodes = controller.node_status || raise(NoResponseError)
88
+ nodes.each do |node|
89
+ puts "#{node} #{node.connected? ? 'C' : '-'} #{node.counter} #{node.failure_counter}"
90
+ end
91
+ when 'shutdown'
92
+ controller.shutdown || raise(NoResponseError)
93
+ when 'waitdown'
94
+ controller.waitdown || raise(NoResponseError)
95
+ else
96
+ STDERR.puts "Unrecognized command: #{command}"
97
+ STDERR.puts 'Run equityctl with no arguments for a list of commands.'
98
+ exit(64)
99
+ end
100
+ rescue NoResponseError
101
+ STDERR.puts "No response from #{address}:#{port}"
102
+ exit(1)
103
+ end
@@ -0,0 +1,71 @@
1
+ require 'socket'
2
+ require 'dl/import'
3
+ require 'equity/manager'
4
+ require 'equity/node'
5
+ require 'equity/controller/key'
6
+
7
+ module Equity
8
+ class Controller
9
+ def initialize(host, port, key = Key.new)
10
+ @host = host.dup
11
+ @port = port.to_i
12
+ @socket = UDPSocket.new
13
+ @key = key
14
+ end
15
+
16
+ def node_status
17
+ reply = send(Manager::CMD_NODE_STATUS)
18
+ return nil unless reply
19
+
20
+ nodes = []
21
+ count = reply.slice!(0, 1)[0]
22
+ count.times do
23
+ length, connected, counter, port = reply.slice!(0, 9).unpack('nCNn')
24
+ address = reply.slice!(0, length - 9)
25
+ node = Node::Static.new(address, port)
26
+ node.connected = (connected != 0)
27
+ node.counter = counter
28
+ nodes << node
29
+ end
30
+ nodes
31
+ end
32
+
33
+ def shutdown
34
+ reply = send(Manager::CMD_SHUTDOWN)
35
+ !!reply
36
+ end
37
+
38
+ def waitdown
39
+ reply = send(Manager::CMD_WAITDOWN)
40
+ !!reply
41
+ end
42
+
43
+ module Alarm
44
+ extend DL::Importable
45
+ if RUBY_PLATFORM =~ /darwin/
46
+ so_ext = 'dylib'
47
+ else
48
+ so_ext = 'so'
49
+ end
50
+ dlload "libc.#{so_ext}"
51
+ extern "unsigned int alarm(unsigned int)"
52
+ end
53
+
54
+ private
55
+
56
+ def send(message)
57
+ message = @key.sign(message)
58
+ @socket.send(message, 0, @host, @port)
59
+ begin
60
+ trap('ALRM') { raise Errno::EINTR }
61
+ Alarm.alarm(15)
62
+ reply, address = @socket.recvfrom(1024)
63
+ rescue Errno::EINTR
64
+ reply = nil
65
+ end
66
+ Alarm.alarm(0)
67
+ reply
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,177 @@
1
+ require 'openssl'
2
+ require 'fileutils'
3
+ require 'base64'
4
+
5
+ module Equity
6
+ class Controller
7
+ class Key
8
+
9
+ # Length, in bits, of generated keys.
10
+ LENGTH = 2048
11
+
12
+ # Default file system path where the user's key is stored. This path is
13
+ # the private half of the key. The public key can be found by appending
14
+ # ".pub" to this path.
15
+ DEFAULT_IDENTITY_PATH = File.join(ENV['HOME'], ".equity", "id_dsa")
16
+
17
+ # Array of paths that contain the lists of public keys that identify
18
+ # people who are authorized to control equity.
19
+ PERSONAL_AUTHORIZED_KEYS_PATH = File.join(ENV['HOME'], ".equity", "authorized_keys")
20
+ SYSTEM_AUTHORIZED_KEYS_PATH = "/etc/equity/authorized_keys"
21
+
22
+ # Byte sequence that separates the data and the signature in signed data.
23
+ SIGNATURE_SEPARATOR = 0.chr
24
+
25
+ # Generates a new DSA key pair and stores it in a pair of files at the
26
+ # default identity path. Returns the generated key.
27
+ def self.generate
28
+ # Generate a DSA key pair.
29
+ dsa = OpenSSL::PKey::DSA.generate(LENGTH)
30
+ # Write the private key to disk.
31
+ FileUtils.mkdir_p(File.dirname(DEFAULT_IDENTITY_PATH))
32
+ File.open(DEFAULT_IDENTITY_PATH, "w") do |io|
33
+ io.chmod(0600)
34
+ io.write(dsa.to_pem)
35
+ end
36
+ # Write the public key to disk.
37
+ File.open(DEFAULT_IDENTITY_PATH + ".pub", "w") do |io|
38
+ io.write(dsa.public_key.to_pem)
39
+ end
40
+ # Return a Key object with the newly generated key.
41
+ new
42
+ end
43
+
44
+ # Loads a key from disk or a string. If the loaded key includes the
45
+ # private half of the key and the key has been loaded from disk, the
46
+ # file's permissions are verified to ensure that only the owner can
47
+ # access it. Raises a SecurityError if the file's permissions are unsafe.
48
+ def initialize(path_or_pem = DEFAULT_IDENTITY_PATH)
49
+ # Try to interpret path_or_pem as a PEM string first.
50
+ begin
51
+ @dsa = OpenSSL::PKey::DSA.new(path_or_pem)
52
+ rescue
53
+ # That didn't work. It must be a path.
54
+ @path = path_or_pem
55
+ @dsa = OpenSSL::PKey::DSA.new(IO.read(@path))
56
+ # If this is a private key, make sure nobody else can read it.
57
+ if @dsa.private?
58
+ mode = File.stat(@path).mode
59
+ unless mode & 077 == 0
60
+ raise SecurityError, "unsafe permissions on #{@path} - must not allow any access by group or world"
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ # Returns an array of paths to all files associated with this key.
67
+ def paths
68
+ return [] if @path.nil?
69
+ if @dsa.private?
70
+ [@path, @path + ".pub"]
71
+ else
72
+ [@path + ".pub"]
73
+ end
74
+ end
75
+
76
+ # Appends a cryptographic signature to +data+.
77
+ def sign(data)
78
+ sig = @dsa.sign(OpenSSL::Digest::DSS1.new, data)
79
+ a1sig = OpenSSL::ASN1.decode( sig )
80
+
81
+ sig_r = a1sig.value[0].value.to_s(2)
82
+ sig_s = a1sig.value[1].value.to_s(2)
83
+
84
+ if sig_r.length > 20 || sig_s.length > 20
85
+ raise OpenSSL::PKey::DSAError, "bad sig size"
86
+ end
87
+
88
+ sig_r = "\0" * ( 20 - sig_r.length ) + sig_r if sig_r.length < 20
89
+ sig_s = "\0" * ( 20 - sig_s.length ) + sig_s if sig_s.length < 20
90
+
91
+ return [
92
+ data,
93
+ Base64.encode64(sig_r + sig_s),
94
+ @dsa.public_key.to_pem
95
+ ].join(SIGNATURE_SEPARATOR)
96
+ end
97
+
98
+ # Verifies the cryptographic signature appended to +data_with_sig+. If
99
+ # the signature is legitimate, the data is returned. Otherwise this
100
+ # method returns +false+.
101
+ def self.verify(data_with_sig)
102
+ data, sig, public_pem = data_with_sig.split(/#{SIGNATURE_SEPARATOR}/)
103
+ sig = Base64.decode64(sig)
104
+ dsa = OpenSSL::PKey::DSA.new(public_pem)
105
+ sig_r = sig[0,20].unpack("H*")[0].to_i(16)
106
+ sig_s = sig[20,20].unpack("H*")[0].to_i(16)
107
+ a1sig = OpenSSL::ASN1::Sequence([
108
+ OpenSSL::ASN1::Integer(sig_r),
109
+ OpenSSL::ASN1::Integer(sig_s)
110
+ ])
111
+ if dsa.verify(OpenSSL::Digest::DSS1.new, a1sig.to_der, data)
112
+ data
113
+ else
114
+ false
115
+ end
116
+ end
117
+
118
+ # Returns true if this key is authorized to control equity.
119
+ def authorized?
120
+ self.class.authorized_keys.include?(self)
121
+ end
122
+
123
+ # Returns an array of keys which are authorized to control equity. Raises
124
+ # a SecurityError if either of the authorized key files are writable by
125
+ # group or world, or if a private key is found in the files.
126
+ def self.authorized_keys
127
+ # Return cached keys if they've already been loaded.
128
+ return @authorized_keys if @authorized_keys
129
+ # Read the keys and verify file permissions.
130
+ pem = ""
131
+ if File.exist?(PERSONAL_AUTHORIZED_KEYS_PATH)
132
+ mode = File.stat(PERSONAL_AUTHORIZED_KEYS_PATH).mode
133
+ unless mode & 022 == 0
134
+ raise SecurityError, "unsafe permissions on #{PERSONAL_AUTHORIZED_KEYS_PATH} - must not be writable by group or world"
135
+ end
136
+ pem = IO.read(PERSONAL_AUTHORIZED_KEYS_PATH) + "\n"
137
+ end
138
+ if File.exist?(SYSTEM_AUTHORIZED_KEYS_PATH)
139
+ mode = File.stat(SYSTEM_AUTHORIZED_KEYS_PATH).mode
140
+ unless mode & 022 == 0
141
+ raise SecurityError, "unsafe permissions on #{SYSTEM_AUTHORIZED_KEYS_PATH} - must not be writable by group or world"
142
+ end
143
+ pem += IO.read(SYSTEM_AUTHORIZED_KEYS_PATH)
144
+ end
145
+ # Instantiate the keys and verify that they're all public.
146
+ pems = pem.split(/-----END DSA PUBLIC KEY-----/)
147
+ pems.pop
148
+ pems.collect! do |pem|
149
+ key = new(pem + "-----END DSA PUBLIC KEY-----")
150
+ if key.private?
151
+ raise SecurityError, "private key found in authorized keys - only public keys should be authorized"
152
+ end
153
+ key
154
+ end
155
+ # Cache the loaded keys.
156
+ @authorized_keys = pems
157
+ end
158
+
159
+ # Returns true if this key includes the private half of the key.
160
+ def private?
161
+ @dsa.private?
162
+ end
163
+
164
+ # Two keys are equal if they have the same public key.
165
+ def ==(other)
166
+ @dsa.public_key.to_s == other.instance_eval {@dsa.public_key.to_s}
167
+ end
168
+
169
+ # Returns the public half of the key that signed +data_with_sig+.
170
+ def self.signature_key(data_with_sig)
171
+ data, sig, public_pem = data_with_sig.split(/#{SIGNATURE_SEPARATOR}/)
172
+ new(public_pem)
173
+ end
174
+
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,97 @@
1
+ require "equity/controller/key"
2
+
3
+ module Equity
4
+ class Manager
5
+
6
+ CMD_NODE_STATUS = "S"
7
+ CMD_SHUTDOWN = "K"
8
+ CMD_WAITDOWN = "W"
9
+
10
+ CMD_NAMES = {
11
+ CMD_NODE_STATUS => "status",
12
+ CMD_SHUTDOWN => "shutdown",
13
+ CMD_WAITDOWN => "waitdown"
14
+ }
15
+
16
+ def initialize(nodes, port, debugging = false)
17
+ @nodes = nodes
18
+ @socket = UDPSocket.new
19
+ @socket.bind(nil, port)
20
+ @debugging = debugging
21
+ end
22
+
23
+ def process_commands
24
+ # Read commands from the network.
25
+ begin
26
+ data, address = @socket.recvfrom_nonblock(4096)
27
+ rescue Errno::EAGAIN
28
+ end
29
+ return unless data
30
+ command = data.split(/#{Controller::Key::SIGNATURE_SEPARATOR}/).first
31
+ debug("received manager command: #{command_name(command)}", address)
32
+ # Verify the signature and ensure that the key used to sign the command
33
+ # is authorized.
34
+ unless verified_data = Controller::Key.verify(data)
35
+ debug("signature verification failed - ignoring command", address)
36
+ return
37
+ end
38
+ unless Controller::Key.signature_key(data).authorized?
39
+ debug("command signed by unauthorized key - ignoring command", address)
40
+ return
41
+ end
42
+ data = verified_data
43
+ # Process the command.
44
+ debug("signature verified - processing command", address)
45
+ case data
46
+ when CMD_NODE_STATUS
47
+ reply = node_status_message
48
+ when CMD_SHUTDOWN
49
+ UDPSocket.new.send(generic_ok_message, 0, address[2], address[1])
50
+ exit
51
+ when CMD_WAITDOWN
52
+ reply = generic_ok_message
53
+ waitdown
54
+ else
55
+ debug("command not understood (#{data.unpack('H*')})", address)
56
+ return
57
+ end
58
+ UDPSocket.new.send(reply, 0, address[2], address[1])
59
+ end
60
+
61
+ private
62
+
63
+ def node_status_message
64
+ message = [@nodes.length].pack('C')
65
+ @nodes.each do |node|
66
+ message += [
67
+ 9 + node.address.length,
68
+ (node.connected? ? 1 : 0),
69
+ node.counter,
70
+ node.port
71
+ ].pack('nCNn')
72
+ message += node.address
73
+ end
74
+ message
75
+ end
76
+
77
+ def generic_ok_message
78
+ "OK"
79
+ end
80
+
81
+ def waitdown
82
+ @nodes.each {|node| node.stop_accepting!}
83
+ end
84
+
85
+ def debug(message, sockaddr)
86
+ return unless @debugging
87
+ nameinfo = Socket.getnameinfo(sockaddr,
88
+ Socket::NI_NUMERICHOST | Socket::NI_NUMERICSERV)
89
+ puts "[#{nameinfo[0]}:#{nameinfo[1]}] #{message}"
90
+ end
91
+
92
+ def command_name(command)
93
+ CMD_NAMES[command] || "unknown"
94
+ end
95
+
96
+ end
97
+ end
@@ -0,0 +1,113 @@
1
+ require 'equity/socket_pairing'
2
+
3
+ module Equity
4
+ # Represents a cluster node.
5
+ class Node
6
+ attr_reader :counter
7
+ attr_reader :failure_counter
8
+ attr_reader :address
9
+ attr_reader :port
10
+ attr_reader :client_address
11
+ attr_reader :client_port
12
+
13
+ def initialize(address, port)
14
+ @client = nil
15
+ @counter = 0
16
+ @failure_counter = 0
17
+ @address = address.dup
18
+ @port = port.to_i
19
+ @active = true
20
+ end
21
+
22
+ def connect(client)
23
+ raise(RuntimeError, 'node is already connected') if connected?
24
+ raise(RuntimeError, 'node has been shut down') if shutdown?
25
+ begin
26
+ @client = client
27
+ @server = TCPSocket.new(@address, @port)
28
+ @server.extend SocketPairing
29
+ @server.pair_with(client)
30
+ @counter += 1
31
+
32
+ begin
33
+ nameinfo = Socket.getnameinfo(@client.getpeername,
34
+ Socket::NI_NUMERICHOST | Socket::NI_NUMERICSERV)
35
+ @client_address, @client_port = nameinfo
36
+ rescue
37
+ @client_address, @client_port = "???", "???"
38
+ end
39
+
40
+ @server
41
+ rescue Exception => e
42
+ @client = nil
43
+ @server = nil
44
+ @failure_counter += 1
45
+ raise e
46
+ end
47
+ end
48
+
49
+ def disconnect
50
+ @client.close
51
+ @server.close
52
+ @client = nil
53
+ @server = nil
54
+ @client_address = nil
55
+ @client_port = nil
56
+ end
57
+
58
+ def connected?
59
+ !!@client
60
+ end
61
+
62
+ def sockets
63
+ connected? ? [@client, @server] : []
64
+ end
65
+
66
+ def owns_socket?(socket)
67
+ connected? && ((socket == @client) || (socket == @server))
68
+ end
69
+
70
+ def to_s
71
+ "#{@address}:#{@port}"
72
+ end
73
+
74
+ def self.with_socket(socket)
75
+ ObjectSpace.each_object(self) do |node|
76
+ return node if node.owns_socket?(socket)
77
+ end
78
+ nil
79
+ end
80
+
81
+ def stop_accepting!
82
+ @active = false
83
+ end
84
+
85
+ def shutdown?
86
+ !@active
87
+ end
88
+
89
+ class Static < Node
90
+ attr_writer :counter
91
+
92
+ def connected=(bool)
93
+ @client = bool
94
+ end
95
+
96
+ def connect(client)
97
+ raise(RuntimeError, 'static nodes cannot be connected')
98
+ end
99
+
100
+ def disconnect
101
+ raise(RuntimeError, 'static nodes cannot be disconnected')
102
+ end
103
+
104
+ def sockets
105
+ []
106
+ end
107
+
108
+ def owns_socket?(socket)
109
+ false
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,15 @@
1
+ module Equity
2
+ # Enables pairing of sockets, so a client socket can be linked to a server
3
+ # socket.
4
+ module SocketPairing
5
+ attr_reader :mate
6
+
7
+ def pair_with(other_socket, one_way = false)
8
+ @mate = other_socket
9
+ unless one_way
10
+ other_socket.extend SocketPairing
11
+ other_socket.pair_with(self, true)
12
+ end
13
+ end
14
+ end
15
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: equity
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 5
8
+ version: "0.5"
9
+ platform: ruby
10
+ authors:
11
+ - Dana Contreras
12
+ autorequire:
13
+ bindir: bin
14
+ cert_chain: []
15
+
16
+ date: 2010-03-27 00:00:00 -04:00
17
+ default_executable:
18
+ dependencies: []
19
+
20
+ description:
21
+ email:
22
+ executables:
23
+ - equity
24
+ - equityctl
25
+ extensions: []
26
+
27
+ extra_rdoc_files: []
28
+
29
+ files:
30
+ - bin/equity
31
+ - bin/equityctl
32
+ - lib/equity/controller.rb
33
+ - lib/equity/controller/key.rb
34
+ - lib/equity/manager.rb
35
+ - lib/equity/node.rb
36
+ - lib/equity/socket_pairing.rb
37
+ has_rdoc: true
38
+ homepage: http://github.com/DanaDanger/equity
39
+ licenses: []
40
+
41
+ post_install_message:
42
+ rdoc_options: []
43
+
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ segments:
51
+ - 0
52
+ version: "0"
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ segments:
58
+ - 0
59
+ version: "0"
60
+ requirements: []
61
+
62
+ rubyforge_project:
63
+ rubygems_version: 1.3.6
64
+ signing_key:
65
+ specification_version: 3
66
+ summary: Queueing software load balancer.
67
+ test_files: []
68
+