DanaDanger-equity 0.4 → 0.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|