nova 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +19 -0
- data/README.md +29 -0
- data/bin/nova +8 -0
- data/lib/generator/template/new_install/galaxy/some_star.rb +3 -0
- data/lib/generator/template/new_install/supernova.yml +16 -0
- data/lib/nova.rb +49 -0
- data/lib/nova/cli.rb +62 -0
- data/lib/nova/commands/server.rb +71 -0
- data/lib/nova/common.rb +17 -0
- data/lib/nova/common/event_handler.rb +165 -0
- data/lib/nova/common/event_handler/event.rb +147 -0
- data/lib/nova/common/features.rb +93 -0
- data/lib/nova/common/features/feature.rb +65 -0
- data/lib/nova/common/metadata.rb +65 -0
- data/lib/nova/common/metadata/data.rb +171 -0
- data/lib/nova/common/star_management.rb +164 -0
- data/lib/nova/constructor.rb +84 -0
- data/lib/nova/exceptions.rb +16 -0
- data/lib/nova/project.rb +199 -0
- data/lib/nova/remote.rb +10 -0
- data/lib/nova/remote/fake.rb +51 -0
- data/lib/nova/remote/fake/commands.rb +44 -0
- data/lib/nova/remote/fake/file_system.rb +76 -0
- data/lib/nova/remote/fake/operating_system.rb +52 -0
- data/lib/nova/remote/fake/platform.rb +89 -0
- data/lib/nova/star.rb +25 -0
- data/lib/nova/starbound.rb +14 -0
- data/lib/nova/starbound/client.rb +59 -0
- data/lib/nova/starbound/encryptor.rb +134 -0
- data/lib/nova/starbound/encryptors.rb +13 -0
- data/lib/nova/starbound/encryptors/openssl.rb +122 -0
- data/lib/nova/starbound/encryptors/plaintext.rb +64 -0
- data/lib/nova/starbound/encryptors/rbnacl.rb +67 -0
- data/lib/nova/starbound/protocol.rb +81 -0
- data/lib/nova/starbound/protocol/encryption.rb +48 -0
- data/lib/nova/starbound/protocol/exceptions.rb +38 -0
- data/lib/nova/starbound/protocol/messages.rb +116 -0
- data/lib/nova/starbound/protocol/packet.rb +267 -0
- data/lib/nova/starbound/protocol/socket.rb +231 -0
- data/lib/nova/starbound/server.rb +182 -0
- data/lib/nova/version.rb +5 -0
- data/spec/constructor_spec.rb +20 -0
- data/spec/local_spec.rb +26 -0
- data/spec/nova_spec.rb +7 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/star/some_type.rb +27 -0
- data/spec/star_spec.rb +107 -0
- data/spec/starbound/encryptor_spec.rb +33 -0
- data/spec/starbound/openssl_encryptor_spec.rb +80 -0
- data/spec/starbound/packet_spec.rb +61 -0
- data/spec/starbound/plaintext_encryptor_spec.rb +27 -0
- data/spec/starbound/protocol_spec.rb +163 -0
- data/spec/starbound/rbnacl_encryptor_spec.rb +70 -0
- metadata +166 -0
@@ -0,0 +1,182 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module Nova
|
4
|
+
module Starbound
|
5
|
+
|
6
|
+
# A Starbound server.
|
7
|
+
#
|
8
|
+
# @todo Default proc.
|
9
|
+
class Server
|
10
|
+
|
11
|
+
# The options passed to the server on initialization.
|
12
|
+
#
|
13
|
+
# @return [Hash]
|
14
|
+
attr_reader :options
|
15
|
+
|
16
|
+
# The default options.
|
17
|
+
DEFAULT_OPTIONS = {
|
18
|
+
:type => :tcp,
|
19
|
+
:host => "127.0.0.1",
|
20
|
+
:port => 2010,
|
21
|
+
:path => "/tmp/sock"
|
22
|
+
}
|
23
|
+
|
24
|
+
# Whether or not to run. Once this is set to false, the server
|
25
|
+
# stops listening for clients.
|
26
|
+
#
|
27
|
+
# @return [Boolean]
|
28
|
+
attr_accessor :run
|
29
|
+
|
30
|
+
# The list of active threads that are running.
|
31
|
+
#
|
32
|
+
# @return [Array<Thread>]
|
33
|
+
attr_reader :thread_list
|
34
|
+
|
35
|
+
# Initialize the server with the given options. If the options
|
36
|
+
# has a +:protocol+ key, it is removed from the options and set
|
37
|
+
# as the protocol options.
|
38
|
+
#
|
39
|
+
# @param options [Hash]
|
40
|
+
# @option options [Symbol] :type the type of server. Can be any
|
41
|
+
# of +:tcp+ or +:unix+. Defaults to +:tcp+.
|
42
|
+
# @option options [String] :host the host to bind to. Defaults
|
43
|
+
# to +"127.0.0.1"+. Only used if +:type+ is +:tcp+.
|
44
|
+
# @option options [Numeric] :port the port to bind to. Defaults
|
45
|
+
# to +2010+. Only used if +:type+ is +:tcp+.
|
46
|
+
# @option options [String] :path the path to the unix socket.
|
47
|
+
# Only used if +:type+ is +:unix+.
|
48
|
+
def initialize(options = {})
|
49
|
+
@options = DEFAULT_OPTIONS.merge options
|
50
|
+
@protocol_options = (options.delete(:protocol) || {}).dup
|
51
|
+
@run = true
|
52
|
+
@thread_list = []
|
53
|
+
end
|
54
|
+
|
55
|
+
# How to handle the client. Accepts a block, which is used in
|
56
|
+
# {#listen} when a client connects. If no block is given, the
|
57
|
+
# a block is returned.
|
58
|
+
#
|
59
|
+
# @yieldparam protocol [Protocol] the protocol instance that is
|
60
|
+
# used to handle the client. By the time it is yielded, the
|
61
|
+
# client has already done a handshake with the server.
|
62
|
+
# @return [Proc]
|
63
|
+
def with_client(&block)
|
64
|
+
if block_given?
|
65
|
+
@with_client = block
|
66
|
+
else
|
67
|
+
@with_client ||= proc {}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Reads a ruby file in the instance of this server, so that it
|
72
|
+
# can build a {#with_client} block for it.
|
73
|
+
#
|
74
|
+
# @param file [String] the file to read from.
|
75
|
+
def read_client_file(file)
|
76
|
+
return unless file && File.exists?(file)
|
77
|
+
|
78
|
+
instance_eval File.open(file, "r").read, file, 1
|
79
|
+
end
|
80
|
+
|
81
|
+
# Listen for clients. Calls {#handle_client} when a client is
|
82
|
+
# found.
|
83
|
+
#
|
84
|
+
# @return [void]
|
85
|
+
def listen
|
86
|
+
Nova.logger.info { "Server started." }
|
87
|
+
while run
|
88
|
+
next unless IO.select [server], nil, nil, 1
|
89
|
+
thread_list << Thread.start(server.accept) do |client|
|
90
|
+
Nova.logger.info { "Client accepted." }
|
91
|
+
|
92
|
+
handle_client client
|
93
|
+
|
94
|
+
thread_list.delete(Thread.current)
|
95
|
+
Nova.logger.info { "Client disconnected." }
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Shuts down the server.
|
101
|
+
#
|
102
|
+
# @return [void]
|
103
|
+
def shutdown
|
104
|
+
puts "shutting down"
|
105
|
+
@run = false
|
106
|
+
thread_list.each do |thread|
|
107
|
+
thread.raise ExitError
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns the server type that this server should run as,
|
112
|
+
# already instantized.
|
113
|
+
#
|
114
|
+
# @return [Object]
|
115
|
+
def server
|
116
|
+
@_server ||= case options.fetch(:type, :tcp)
|
117
|
+
when :tcp
|
118
|
+
TCPServer.new options.fetch(:host, "127.0.0.1"),
|
119
|
+
options.fetch(:port, 2010)
|
120
|
+
when :unix
|
121
|
+
UNIXServer.new options.fetch(:path)
|
122
|
+
when :pipe
|
123
|
+
FakeServer.new options.fetch(:pipe)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
# Handles a client after they connect. Creates a {Protocol},
|
130
|
+
# initiates the handshake, and calls {#with_client} to handle
|
131
|
+
# the protocol afterwards.
|
132
|
+
#
|
133
|
+
# @param client [IO] the client to handle.
|
134
|
+
# @return [void]
|
135
|
+
def handle_client(client)
|
136
|
+
protocol = Protocol.new(
|
137
|
+
@protocol_options.merge(:type => :server))
|
138
|
+
|
139
|
+
protocol.socket = client
|
140
|
+
protocol.handshake
|
141
|
+
|
142
|
+
with_client.call(protocol)
|
143
|
+
|
144
|
+
protocol.close(false)
|
145
|
+
|
146
|
+
rescue ExitError => e
|
147
|
+
Nova.logger.error { "Closing while client is connected. Notifying..." }
|
148
|
+
if protocol
|
149
|
+
protocol.close(:shutdown)
|
150
|
+
else
|
151
|
+
client.close unless client.closed?
|
152
|
+
end
|
153
|
+
rescue RemoteCloseError
|
154
|
+
rescue ProtocolError => e
|
155
|
+
Nova.logger.error { "Client failed: #{e.message} #{e.backtrace.join("\n")}" }
|
156
|
+
client.close unless client.closed?
|
157
|
+
ensure
|
158
|
+
protocol.close(false)
|
159
|
+
end
|
160
|
+
|
161
|
+
# A fake server that wraps a pipe.
|
162
|
+
class FakeServer
|
163
|
+
|
164
|
+
# Initialize the fake server with the pipe.
|
165
|
+
def initialize(pipe)
|
166
|
+
@pipe = pipe
|
167
|
+
end
|
168
|
+
|
169
|
+
# Does nothing.
|
170
|
+
def listen(_); end
|
171
|
+
|
172
|
+
# Returns the pipe.
|
173
|
+
#
|
174
|
+
# @return [Object]
|
175
|
+
def accept
|
176
|
+
@pipe
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
data/lib/nova/version.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
describe Nova::Constructor do
|
2
|
+
|
3
|
+
it "should create stars from types" do
|
4
|
+
d = described_class.new(non_existant_star: :some_name) {}
|
5
|
+
|
6
|
+
expect {
|
7
|
+
d.modify_or_create
|
8
|
+
}.to raise_error(Nova::NoStarError)
|
9
|
+
|
10
|
+
d = described_class.new(star: :some_name) do
|
11
|
+
def some_method; 5; end
|
12
|
+
end
|
13
|
+
|
14
|
+
klass = d.modify_or_create
|
15
|
+
expect(klass.as).to be :some_name
|
16
|
+
expect(klass.instance_methods).to include(:some_method)
|
17
|
+
expect(klass.new.some_method).to be 5
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
data/spec/local_spec.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
=begin
|
2
|
+
|
3
|
+
require 'nova/remote/local'
|
4
|
+
|
5
|
+
describe Nova::Remote::Local do
|
6
|
+
|
7
|
+
before :each do
|
8
|
+
Nova::Star.remote = Nova::Remote::Local
|
9
|
+
end
|
10
|
+
|
11
|
+
context "platforms" do
|
12
|
+
|
13
|
+
it "should determine platforms" do
|
14
|
+
expect(Nova::Star.new.platform).to have_at_least(3).items
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context "commands" do
|
19
|
+
it "should execute the command" do
|
20
|
+
star = Nova::Star.new
|
21
|
+
expect(star.line("echo", "hello").pass.stdout).to eq("hello\n")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
=end
|
data/spec/nova_spec.rb
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'coveralls'
|
2
|
+
|
3
|
+
Coveralls.wear!
|
4
|
+
|
5
|
+
module NovaHelper
|
6
|
+
|
7
|
+
def self.build_packet(type = 0, body = "hello world", data = { :packet_id => 1, :nonce => "" })
|
8
|
+
Nova::Starbound::Protocol::Packet.build(type, body, data)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.packet_from_socket(sock)
|
12
|
+
Nova::Starbound::Protocol::Packet.from_socket(sock)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.build_response(type, body, pack)
|
16
|
+
Nova::Starbound::Protocol::Packet.build_response(type, body, pack, :nonce => "")
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class SomeType < Nova::Star
|
2
|
+
star_type :some_type
|
3
|
+
|
4
|
+
metadata do
|
5
|
+
require_options :hello
|
6
|
+
end
|
7
|
+
|
8
|
+
feature :some_feature do
|
9
|
+
on :some_event do; end
|
10
|
+
|
11
|
+
on :enable do
|
12
|
+
tag :enable
|
13
|
+
|
14
|
+
3
|
15
|
+
end
|
16
|
+
|
17
|
+
on :disable do
|
18
|
+
tag :disable
|
19
|
+
|
20
|
+
4
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
on :foo do; 1; end
|
25
|
+
|
26
|
+
on :bar, requires: :an_option do; 2; end
|
27
|
+
end
|
data/spec/star_spec.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
require_relative 'star/some_type'
|
2
|
+
|
3
|
+
describe Nova::Star do
|
4
|
+
|
5
|
+
before :each do
|
6
|
+
Nova::Star.remote = Nova::Remote::Fake
|
7
|
+
end
|
8
|
+
|
9
|
+
context "management" do
|
10
|
+
it "handles types" do
|
11
|
+
expect(Nova::Star.types).to eq(star: Nova::Star, some_type: SomeType)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "has pretty inspect" do
|
15
|
+
expect(Nova::Star.inspect).to eq("Nova::Star")
|
16
|
+
expect(SomeType.inspect).to eq("Nova::Star/SomeType")
|
17
|
+
end
|
18
|
+
|
19
|
+
it "has a default remote" do
|
20
|
+
expect(SomeType.remote).to be(Nova::Remote::Fake)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context "options manager" do
|
25
|
+
it "validates options" do
|
26
|
+
expect {
|
27
|
+
star = SomeType.new
|
28
|
+
star.options = {}
|
29
|
+
}.to raise_error(Nova::InvalidOptionsError)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context "event manager" do
|
34
|
+
it "manages events" do
|
35
|
+
event = Nova::Common::EventHandler::Event
|
36
|
+
[SomeType, SomeType.new].each do |type|
|
37
|
+
expect(type.events).to be_instance_of Set
|
38
|
+
expect(type.has_event?(:foo)).to be_instance_of event
|
39
|
+
expect(type.has_event?(:not_foo)).to be_nil
|
40
|
+
expect(type.has_event?(:bar)).to be_instance_of event
|
41
|
+
|
42
|
+
expect(type.has_event_with_options?(:foo)).to be_instance_of event
|
43
|
+
expect(type.has_event_with_options?(:bar)).to be_nil
|
44
|
+
expect(type.has_event_with_options?(:bar, an_option: true)).to be_instance_of event
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
it "runs events" do
|
49
|
+
star = SomeType.new
|
50
|
+
context = double(:context)
|
51
|
+
star.bind! context
|
52
|
+
|
53
|
+
expect(star.run!(:foo)).to be 1
|
54
|
+
|
55
|
+
expect {
|
56
|
+
star.run!(:bar)
|
57
|
+
}.to raise_error(Nova::NoEventError)
|
58
|
+
expect(star.run(:bar)).to be_instance_of Nova::NoEventError
|
59
|
+
expect(star.run!(:bar, an_option: true)).to be 2
|
60
|
+
|
61
|
+
expect {
|
62
|
+
star.run! :not_an_event
|
63
|
+
}.to raise_error(Nova::NoEventError)
|
64
|
+
expect(star.run(:not_an_event)).to be_instance_of Nova::NoEventError
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context "features" do
|
69
|
+
it "supports defined features" do
|
70
|
+
expect(SomeType.supports?(:some_feature)).to be true
|
71
|
+
expect(SomeType.supports?(:another_feature)).to be false
|
72
|
+
end
|
73
|
+
|
74
|
+
it "gives fake features" do
|
75
|
+
star = SomeType.new
|
76
|
+
expect(star.feature(:some_feature)).to_not be_fake
|
77
|
+
expect(star.feature(:another_feature)).to be_fake
|
78
|
+
|
79
|
+
expect(star.supports?(:some_feature)).to be true
|
80
|
+
expect(star.supports?(:another_feature)).to be false
|
81
|
+
end
|
82
|
+
|
83
|
+
it "runs enable and disable events" do
|
84
|
+
star = SomeType.new
|
85
|
+
context = stub(:context)
|
86
|
+
context.should_receive(:tag).with(:enable)
|
87
|
+
expect(star.feature(:some_feature).bind(context).enable!).to be 3
|
88
|
+
|
89
|
+
context = stub(:context)
|
90
|
+
context.should_receive(:tag).with(:disable)
|
91
|
+
expect(star.feature(:some_feature).bind(context).disable!).to be 4
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
context "platforms" do
|
96
|
+
it "displays correct platforms" do
|
97
|
+
expect(SomeType.new.remote.platform.types).to eq([])
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
context "commands" do
|
102
|
+
it "gives a command line" do
|
103
|
+
star = SomeType.new
|
104
|
+
expect(star.remote.command.line("something", "arguments")).to be_instance_of Command::Runner
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
describe Nova::Starbound::Encryptor do
|
2
|
+
|
3
|
+
it "is not available" do
|
4
|
+
expect(described_class).to_not be_available
|
5
|
+
|
6
|
+
expect {
|
7
|
+
subject
|
8
|
+
}.to raise_error NotImplementedError
|
9
|
+
end
|
10
|
+
|
11
|
+
it "is not plaintext" do
|
12
|
+
expect(described_class).to_not be_plaintext
|
13
|
+
end
|
14
|
+
|
15
|
+
it "sorts the encryptors" do
|
16
|
+
expect(described_class.sorted_encryptors).to have(3).items
|
17
|
+
expect(described_class.sorted_encryptors.first.preference).to be > described_class.sorted_encryptors.last.preference
|
18
|
+
end
|
19
|
+
|
20
|
+
context "encrypting" do
|
21
|
+
before(:each) { described_class.stub(:available?).and_return(true) }
|
22
|
+
|
23
|
+
it "raises errors" do
|
24
|
+
[:encrypt, :decrypt, :private_key!, :public_key,
|
25
|
+
:other_public_key=].each do |m|
|
26
|
+
|
27
|
+
expect(subject).to respond_to m
|
28
|
+
|
29
|
+
expect { subject.send(m) }.to raise_error NotImplementedError
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
describe Nova::Starbound::Encryptors::OpenSSL do
|
2
|
+
|
3
|
+
it "is available" do
|
4
|
+
expect(described_class).to be_available
|
5
|
+
end
|
6
|
+
|
7
|
+
context "handling keys" do
|
8
|
+
subject { described_class.new }
|
9
|
+
its(:private_key!) { should be_instance_of ::OpenSSL::PKey::RSA }
|
10
|
+
|
11
|
+
it "has a public key" do
|
12
|
+
subject.private_key! # initialize the private key
|
13
|
+
expect(subject.public_key).to be_a String
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
before :each do
|
18
|
+
@packet = NovaHelper.build_packet
|
19
|
+
end
|
20
|
+
|
21
|
+
it "encrypts a packet successfully" do
|
22
|
+
|
23
|
+
subject.private_key!
|
24
|
+
public_key = subject.public_key
|
25
|
+
|
26
|
+
secret = ::OpenSSL::Random.random_bytes(4096 / 16)
|
27
|
+
encrypted_secret = ::OpenSSL::PKey::RSA.new(public_key).public_encrypt(secret)
|
28
|
+
|
29
|
+
subject.other_public_key = encrypted_secret
|
30
|
+
|
31
|
+
expect(subject.options[:shared_secret]).to eq secret
|
32
|
+
encrypted = nil
|
33
|
+
|
34
|
+
expect {
|
35
|
+
encrypted = subject.encrypt(@packet)
|
36
|
+
}.to_not raise_error
|
37
|
+
|
38
|
+
expect(encrypted).to be_instance_of Nova::Starbound::Protocol::Packet
|
39
|
+
expect(encrypted.body.bytesize).to eq encrypted[:size]
|
40
|
+
end
|
41
|
+
|
42
|
+
it "decrypts a packet successfully" do
|
43
|
+
|
44
|
+
subject.private_key!
|
45
|
+
|
46
|
+
public_key = ::OpenSSL::PKey::RSA.new(4096)
|
47
|
+
subject.other_public_key = public_key.public_key.to_der
|
48
|
+
encrypted_secret = subject.public_key
|
49
|
+
|
50
|
+
secret = public_key.private_decrypt(encrypted_secret)
|
51
|
+
expect(secret).to eq subject.options[:shared_secret]
|
52
|
+
|
53
|
+
encrypted = subject.encrypt(@packet)
|
54
|
+
decrypted = nil
|
55
|
+
|
56
|
+
expect {
|
57
|
+
decrypted = subject.decrypt(encrypted)
|
58
|
+
}.to_not raise_error
|
59
|
+
|
60
|
+
expect(decrypted.body).to eq @packet.body
|
61
|
+
end
|
62
|
+
|
63
|
+
it "raises an error on non-matching hashes" do
|
64
|
+
subject.private_key!
|
65
|
+
public_key = subject.public_key
|
66
|
+
|
67
|
+
secret = ::OpenSSL::Random.random_bytes(4096 / 16)
|
68
|
+
encrypted_secret = ::OpenSSL::PKey::RSA.new(public_key).public_encrypt(secret)
|
69
|
+
|
70
|
+
subject.other_public_key = encrypted_secret
|
71
|
+
|
72
|
+
encrypted = subject.encrypt(@packet)
|
73
|
+
encrypted.body.replace("\x00" * encrypted.size)
|
74
|
+
|
75
|
+
expect {
|
76
|
+
subject.decrypt(encrypted)
|
77
|
+
}.to raise_error(Nova::Starbound::EncryptorError)
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|