DanaDanger-equity 0.4 → 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 +5 -5
- data/bin/equityctl +44 -2
- data/lib/equity/controller.rb +9 -1
- data/lib/equity/controller/key.rb +177 -0
- data/lib/equity/manager.rb +46 -4
- metadata +2 -1
data/bin/equity
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
#
|
|
3
|
-
# equity 0.
|
|
3
|
+
# equity 0.5 - queueing software load balancer with transmission inspection
|
|
4
4
|
# and simple HTTP header rewriting
|
|
5
5
|
#
|
|
6
6
|
# Usage: equity [-dp] [-h "Header: Value"] <listen-port> [<node-address>:]<node-port> ...
|
|
@@ -25,7 +25,7 @@ end
|
|
|
25
25
|
|
|
26
26
|
# Prints a debug message if debugging is enabled.
|
|
27
27
|
def debug(socket, message)
|
|
28
|
-
return unless $debugging
|
|
28
|
+
return unless $debugging
|
|
29
29
|
if socket
|
|
30
30
|
if node = Equity::Node.with_socket(socket)
|
|
31
31
|
client_id = client_id(node)
|
|
@@ -124,7 +124,7 @@ options.each do |option, argument|
|
|
|
124
124
|
end
|
|
125
125
|
|
|
126
126
|
if ARGV.length < 2
|
|
127
|
-
STDERR.puts 'equity 0.
|
|
127
|
+
STDERR.puts 'equity 0.5'
|
|
128
128
|
STDERR.puts 'Usage: equity [<options>] <listen-port> [<node-address>:]<node-port> ...'
|
|
129
129
|
STDERR.print "\n"
|
|
130
130
|
STDERR.puts 'Node addresses default to localhost.'
|
|
@@ -185,7 +185,7 @@ trap 'SIGINT', Proc.new {
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
# Start up control server.
|
|
188
|
-
$control_server = Equity::Manager.new($nodes, $listen_port)
|
|
188
|
+
$control_server = Equity::Manager.new($nodes, $listen_port, $debugging)
|
|
189
189
|
|
|
190
190
|
# Start up server.
|
|
191
191
|
$client_queue = []
|
|
@@ -221,8 +221,8 @@ while true
|
|
|
221
221
|
mate.write(data)
|
|
222
222
|
rescue Errno::EPIPE, Errno::ECONNRESET
|
|
223
223
|
# Connection closed. Disconnect this node.
|
|
224
|
-
node.disconnect
|
|
225
224
|
debug(socket, 'disconnected')
|
|
225
|
+
node.disconnect
|
|
226
226
|
end
|
|
227
227
|
end
|
|
228
228
|
end
|
data/bin/equityctl
CHANGED
|
@@ -15,9 +15,20 @@
|
|
|
15
15
|
#
|
|
16
16
|
|
|
17
17
|
require 'equity/controller'
|
|
18
|
+
require 'getoptlong'
|
|
18
19
|
|
|
19
|
-
# Process
|
|
20
|
-
|
|
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)
|
|
21
32
|
STDERR.puts 'Usage: equityctl [<host>:]<port> <command>'
|
|
22
33
|
STDERR.print "\n"
|
|
23
34
|
STDERR.puts 'The host defaults to localhost.'
|
|
@@ -25,9 +36,38 @@ if ARGV.length != 2
|
|
|
25
36
|
STDERR.puts 'Command Description'
|
|
26
37
|
STDERR.puts 'status Displays each node\'s address, port, connection status,'
|
|
27
38
|
STDERR.puts ' connection counter, and failure counter.'
|
|
39
|
+
STDERR.puts 'shutdown Close connections immediately and shut down.'
|
|
40
|
+
STDERR.print "\n\n"
|
|
41
|
+
STDERR.puts 'Usage: equityctl --genkey'
|
|
42
|
+
STDERR.print "\n"
|
|
43
|
+
STDERR.puts 'With the --genkey option, equityctl generates a public/private key pair for'
|
|
44
|
+
STDERR.puts 'authenticating itself to equity.'
|
|
28
45
|
exit(64) # EX_USAGE
|
|
29
46
|
end
|
|
30
47
|
|
|
48
|
+
# If --genkey was specified, generate a key pair and exit.
|
|
49
|
+
if $genkey
|
|
50
|
+
puts "Generating key..."
|
|
51
|
+
begin
|
|
52
|
+
key = Equity::Controller::Key.generate
|
|
53
|
+
rescue Exception => e
|
|
54
|
+
STDERR.puts e.message
|
|
55
|
+
exit(1)
|
|
56
|
+
end
|
|
57
|
+
puts "Done! For reference, your key is stored here:"
|
|
58
|
+
key.paths.each {|path| puts " #{path}"}
|
|
59
|
+
# Tell the user where they need to put their public key in order for it to be
|
|
60
|
+
# useful.
|
|
61
|
+
puts "\nTo be authorized to use equityctl with your own equity processes, your public"
|
|
62
|
+
puts "key must be appended to:"
|
|
63
|
+
puts " " + Equity::Controller::Key::PERSONAL_AUTHORIZED_KEYS_PATH
|
|
64
|
+
puts "\nTo be authorized to use equityctl with other people's equity processes, your"
|
|
65
|
+
puts "public key must be appended to:"
|
|
66
|
+
puts " " + Equity::Controller::Key::SYSTEM_AUTHORIZED_KEYS_PATH
|
|
67
|
+
exit
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Extract address, port, and command from arguments.
|
|
31
71
|
address, port = ARGV.shift.split(':', 2)
|
|
32
72
|
if port.nil?
|
|
33
73
|
port = address
|
|
@@ -47,6 +87,8 @@ begin
|
|
|
47
87
|
nodes.each do |node|
|
|
48
88
|
puts "#{node} #{node.connected? ? 'C' : '-'} #{node.counter} #{node.failure_counter}"
|
|
49
89
|
end
|
|
90
|
+
when 'shutdown'
|
|
91
|
+
controller.shutdown || raise(NoResponseError)
|
|
50
92
|
else
|
|
51
93
|
STDERR.puts "Unrecognized command: #{command}"
|
|
52
94
|
STDERR.puts 'Run equityctl with no arguments for a list of commands.'
|
data/lib/equity/controller.rb
CHANGED
|
@@ -2,13 +2,15 @@ require 'socket'
|
|
|
2
2
|
require 'dl/import'
|
|
3
3
|
require 'equity/manager'
|
|
4
4
|
require 'equity/node'
|
|
5
|
+
require 'equity/controller/key'
|
|
5
6
|
|
|
6
7
|
module Equity
|
|
7
8
|
class Controller
|
|
8
|
-
def initialize(host, port)
|
|
9
|
+
def initialize(host, port, key = Key.new)
|
|
9
10
|
@host = host.dup
|
|
10
11
|
@port = port.to_i
|
|
11
12
|
@socket = UDPSocket.new
|
|
13
|
+
@key = key
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
def node_status
|
|
@@ -28,6 +30,11 @@ module Equity
|
|
|
28
30
|
nodes
|
|
29
31
|
end
|
|
30
32
|
|
|
33
|
+
def shutdown
|
|
34
|
+
reply = send(Manager::CMD_SHUTDOWN)
|
|
35
|
+
!!reply
|
|
36
|
+
end
|
|
37
|
+
|
|
31
38
|
module Alarm
|
|
32
39
|
extend DL::Importable
|
|
33
40
|
if RUBY_PLATFORM =~ /darwin/
|
|
@@ -42,6 +49,7 @@ module Equity
|
|
|
42
49
|
private
|
|
43
50
|
|
|
44
51
|
def send(message)
|
|
52
|
+
message = @key.sign(message)
|
|
45
53
|
@socket.send(message, 0, @host, @port)
|
|
46
54
|
begin
|
|
47
55
|
trap('ALRM') { raise Errno::EINTR }
|
|
@@ -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
|
data/lib/equity/manager.rb
CHANGED
|
@@ -1,24 +1,51 @@
|
|
|
1
|
+
require "equity/controller/key"
|
|
2
|
+
|
|
1
3
|
module Equity
|
|
2
4
|
class Manager
|
|
3
5
|
|
|
4
|
-
CMD_NODE_STATUS = "
|
|
6
|
+
CMD_NODE_STATUS = "S"
|
|
7
|
+
CMD_SHUTDOWN = "K"
|
|
5
8
|
|
|
6
|
-
|
|
9
|
+
CMD_NAMES = {
|
|
10
|
+
CMD_NODE_STATUS => "status",
|
|
11
|
+
CMD_SHUTDOWN => "shutdown"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
def initialize(nodes, port, debugging = false)
|
|
7
15
|
@nodes = nodes
|
|
8
16
|
@socket = UDPSocket.new
|
|
9
17
|
@socket.bind(nil, port)
|
|
18
|
+
@debugging = debugging
|
|
10
19
|
end
|
|
11
20
|
|
|
12
21
|
def process_commands
|
|
22
|
+
# Read commands from the network.
|
|
13
23
|
begin
|
|
14
|
-
data, address = @socket.recvfrom_nonblock(
|
|
24
|
+
data, address = @socket.recvfrom_nonblock(4096)
|
|
15
25
|
rescue Errno::EAGAIN
|
|
16
26
|
end
|
|
17
27
|
return unless data
|
|
18
|
-
|
|
28
|
+
command = data.split(/#{Controller::Key::SIGNATURE_SEPARATOR}/).first
|
|
29
|
+
debug("received manager command: #{command_name(command)}", address)
|
|
30
|
+
# Verify the signature and ensure that the key used to sign the command
|
|
31
|
+
# is authorized.
|
|
32
|
+
unless verified_data = Controller::Key.verify(data)
|
|
33
|
+
debug("signature verification failed - ignoring command", address)
|
|
34
|
+
return
|
|
35
|
+
end
|
|
36
|
+
unless Controller::Key.signature_key(data).authorized?
|
|
37
|
+
debug("command signed by unauthorized key - ignoring command", address)
|
|
38
|
+
return
|
|
39
|
+
end
|
|
40
|
+
data = verified_data
|
|
41
|
+
# Process the command.
|
|
42
|
+
debug("signature verified - processing command", address)
|
|
19
43
|
case data
|
|
20
44
|
when CMD_NODE_STATUS
|
|
21
45
|
reply = node_status_message
|
|
46
|
+
when CMD_SHUTDOWN
|
|
47
|
+
UDPSocket.new.send(generic_ok_message, 0, address[2], address[1])
|
|
48
|
+
exit
|
|
22
49
|
else
|
|
23
50
|
return
|
|
24
51
|
end
|
|
@@ -41,5 +68,20 @@ module Equity
|
|
|
41
68
|
message
|
|
42
69
|
end
|
|
43
70
|
|
|
71
|
+
def generic_ok_message
|
|
72
|
+
"OK"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def debug(message, sockaddr)
|
|
76
|
+
return unless @debugging
|
|
77
|
+
nameinfo = Socket.getnameinfo(sockaddr,
|
|
78
|
+
Socket::NI_NUMERICHOST | Socket::NI_NUMERICSERV)
|
|
79
|
+
puts "[#{nameinfo[0]}:#{nameinfo[1]}] #{message}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def command_name(command)
|
|
83
|
+
CMD_NAMES[command] || "unknown"
|
|
84
|
+
end
|
|
85
|
+
|
|
44
86
|
end
|
|
45
87
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: DanaDanger-equity
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: "0.
|
|
4
|
+
version: "0.5"
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- DanaDanger
|
|
@@ -26,6 +26,7 @@ files:
|
|
|
26
26
|
- bin/equity
|
|
27
27
|
- bin/equityctl
|
|
28
28
|
- lib/equity/controller.rb
|
|
29
|
+
- lib/equity/controller/key.rb
|
|
29
30
|
- lib/equity/manager.rb
|
|
30
31
|
- lib/equity/node.rb
|
|
31
32
|
- lib/equity/socket_pairing.rb
|