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 +294 -0
- data/bin/equityctl +103 -0
- data/lib/equity/controller.rb +71 -0
- data/lib/equity/controller/key.rb +177 -0
- data/lib/equity/manager.rb +97 -0
- data/lib/equity/node.rb +113 -0
- data/lib/equity/socket_pairing.rb +15 -0
- metadata +68 -0
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
|
data/lib/equity/node.rb
ADDED
@@ -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
|
+
|