equity 0.5

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/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
+