kstor 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +7 -0
- data/bin/kstor +286 -0
- data/bin/kstor-srv +26 -0
- data/lib/kstor/config.rb +66 -0
- data/lib/kstor/controller/authentication.rb +79 -0
- data/lib/kstor/controller/secret.rb +201 -0
- data/lib/kstor/controller/users.rb +62 -0
- data/lib/kstor/controller.rb +80 -0
- data/lib/kstor/crypto/ascii_armor.rb +27 -0
- data/lib/kstor/crypto/keys.rb +116 -0
- data/lib/kstor/crypto.rb +240 -0
- data/lib/kstor/error.rb +85 -0
- data/lib/kstor/log.rb +56 -0
- data/lib/kstor/message.rb +132 -0
- data/lib/kstor/model.rb +437 -0
- data/lib/kstor/server.rb +51 -0
- data/lib/kstor/session.rb +80 -0
- data/lib/kstor/socket_server.rb +113 -0
- data/lib/kstor/sql_connection.rb +74 -0
- data/lib/kstor/store.rb +383 -0
- data/lib/kstor/systemd.rb +25 -0
- data/lib/kstor/version.rb +5 -0
- data/lib/kstor.rb +10 -0
- metadata +141 -0
@@ -0,0 +1,132 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module KStor
|
6
|
+
# Internal exception when a request is received with neither a session ID nor
|
7
|
+
# a login/password pair.
|
8
|
+
#
|
9
|
+
# We can't use a KStor::Error here because kstor/error.rb require()s
|
10
|
+
# kstor/message.rb .
|
11
|
+
class RequestMissesAuthData < RuntimeError
|
12
|
+
end
|
13
|
+
|
14
|
+
class UnparsableResponse < RuntimeError
|
15
|
+
end
|
16
|
+
|
17
|
+
# A user request or response.
|
18
|
+
class Message
|
19
|
+
attr_reader :type
|
20
|
+
attr_reader :args
|
21
|
+
|
22
|
+
def initialize(type, args)
|
23
|
+
@type = type
|
24
|
+
@args = args
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_h
|
28
|
+
{ 'type' => @type, 'args' => @args }
|
29
|
+
end
|
30
|
+
|
31
|
+
def serialize
|
32
|
+
to_h.to_json
|
33
|
+
end
|
34
|
+
|
35
|
+
# Parse a user request, freshly arrived from the network.
|
36
|
+
#
|
37
|
+
# @param str [String] serialized request
|
38
|
+
# @return [LoginRequest,SessionRequest] a request
|
39
|
+
# @raise [KStor::RequestMissesAuthData]
|
40
|
+
def self.parse_request(str)
|
41
|
+
data = JSON.parse(str)
|
42
|
+
if data.key?('login') && data.key?('password')
|
43
|
+
LoginRequest.new(
|
44
|
+
data['login'], data['password'],
|
45
|
+
data['type'], data['args']
|
46
|
+
)
|
47
|
+
elsif data.key?('session_id')
|
48
|
+
SessionRequest.new(
|
49
|
+
data['session_id'], data['type'], data['args']
|
50
|
+
)
|
51
|
+
else
|
52
|
+
raise RequestMissesAuthData
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# A user authentication request.
|
58
|
+
class LoginRequest < Message
|
59
|
+
attr_reader :login
|
60
|
+
attr_reader :password
|
61
|
+
|
62
|
+
def initialize(login, password, type, args)
|
63
|
+
@login = login
|
64
|
+
@password = password
|
65
|
+
super(type, args)
|
66
|
+
end
|
67
|
+
|
68
|
+
def inspect
|
69
|
+
fmt = [
|
70
|
+
'#<KStor::LoginRequest:%<id>x',
|
71
|
+
'@login=%<login>s',
|
72
|
+
'@password="******"',
|
73
|
+
'@args=%<args>s>'
|
74
|
+
].join(' ')
|
75
|
+
format(fmt, id: object_id, login: @login.inspect, args: @args.inspect)
|
76
|
+
end
|
77
|
+
|
78
|
+
def to_h
|
79
|
+
super.merge('login' => @login, 'password' => @password)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# A user request with a session ID.
|
84
|
+
class SessionRequest < Message
|
85
|
+
attr_reader :session_id
|
86
|
+
|
87
|
+
def initialize(session_id, type, args)
|
88
|
+
@session_id = session_id
|
89
|
+
super(type, args)
|
90
|
+
end
|
91
|
+
|
92
|
+
def inspect
|
93
|
+
fmt = [
|
94
|
+
'#<KStor::SessionRequest:%<id>x',
|
95
|
+
'@session_id=******',
|
96
|
+
'@args=%<args>s>'
|
97
|
+
].join(' ')
|
98
|
+
format(fmt, id: object_id, args: @args.inspect)
|
99
|
+
end
|
100
|
+
|
101
|
+
def to_h
|
102
|
+
super.merge('session_id' => @session_id)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Response to a user request.
|
107
|
+
class Response < Message
|
108
|
+
attr_accessor :session_id
|
109
|
+
|
110
|
+
def initialize(type, args)
|
111
|
+
@session_id = nil
|
112
|
+
super
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.parse(str)
|
116
|
+
data = JSON.parse(str)
|
117
|
+
resp = new(data['type'], data['args'])
|
118
|
+
resp.session_id = data['session_id']
|
119
|
+
resp
|
120
|
+
rescue JSON::ParserError => e
|
121
|
+
raise UnparsableResponse, e.message
|
122
|
+
end
|
123
|
+
|
124
|
+
def error?
|
125
|
+
@type == 'error'
|
126
|
+
end
|
127
|
+
|
128
|
+
def to_h
|
129
|
+
super.merge('session_id' => @session_id)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
data/lib/kstor/model.rb
ADDED
@@ -0,0 +1,437 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'securerandom'
|
5
|
+
|
6
|
+
require 'kstor/crypto'
|
7
|
+
|
8
|
+
module KStor
|
9
|
+
module Model
|
10
|
+
# @!macro [new] dsl_model_properties_rw
|
11
|
+
# @!attribute $1
|
12
|
+
# @return returns value of property $1
|
13
|
+
|
14
|
+
# Base class for model objects.
|
15
|
+
class Base
|
16
|
+
class << self
|
17
|
+
attr_reader :properties
|
18
|
+
|
19
|
+
# Define a named property
|
20
|
+
#
|
21
|
+
# @param name [Symbol] name of the property
|
22
|
+
# @param read_only [Boolean] false to define both a getter and a setter
|
23
|
+
def property(name, read_only: false)
|
24
|
+
@properties ||= []
|
25
|
+
@properties << name
|
26
|
+
define_method(name) do
|
27
|
+
@data[name]
|
28
|
+
end
|
29
|
+
return if read_only
|
30
|
+
|
31
|
+
define_method("#{name}=".to_sym) do |value|
|
32
|
+
@data[name] = value
|
33
|
+
@dirty = true
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Check if a property is defined.
|
38
|
+
#
|
39
|
+
# @param name [Symbol] name of the property
|
40
|
+
# @return [Boolean] true if it is defined
|
41
|
+
def property?(name)
|
42
|
+
@properties.include?(name)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Create a model object from hash values
|
47
|
+
#
|
48
|
+
# @param values [Hash] property values
|
49
|
+
# @return [KStor::Model::Base] a new model object
|
50
|
+
def initialize(**values)
|
51
|
+
@data = {}
|
52
|
+
values.each do |k, v|
|
53
|
+
@data[k] = v if self.class.property?(k)
|
54
|
+
end
|
55
|
+
@dirty = false
|
56
|
+
end
|
57
|
+
|
58
|
+
# Check if properties were modified since instanciation
|
59
|
+
#
|
60
|
+
# @return [Boolean] true if modified
|
61
|
+
def dirty?
|
62
|
+
@dirty
|
63
|
+
end
|
64
|
+
|
65
|
+
# Tell the object that dirty properties were persisted.
|
66
|
+
def clean
|
67
|
+
@dirty = false
|
68
|
+
end
|
69
|
+
|
70
|
+
# Represent model object as a Hash
|
71
|
+
#
|
72
|
+
# @return [Hash] a hash of model object properties
|
73
|
+
def to_h
|
74
|
+
@data.to_h { |k, v| [k.to_s, v.respond_to?(:to_h) ? v.to_h : v] }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# A group of users that can access the same set of secrets.
|
79
|
+
class Group < Base
|
80
|
+
# @!macro dsl_model_properties_rw
|
81
|
+
property :id
|
82
|
+
# @!macro dsl_model_properties_rw
|
83
|
+
property :name
|
84
|
+
# @!macro dsl_model_properties_rw
|
85
|
+
property :pubk
|
86
|
+
|
87
|
+
# Dump properties except pubk.
|
88
|
+
def to_h
|
89
|
+
h = super
|
90
|
+
h.delete('pubk')
|
91
|
+
|
92
|
+
h
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# An item in a user keychain: associates a group and it's private key,
|
97
|
+
# encrypted with the user's key pair.
|
98
|
+
#
|
99
|
+
# Initially encrypted, the {#privk} property will be nil until {#unlock}ed.
|
100
|
+
class KeychainItem < Base
|
101
|
+
# @!macro dsl_model_properties_rw
|
102
|
+
property :group_id
|
103
|
+
# @!macro dsl_model_properties_rw
|
104
|
+
property :group_pubk
|
105
|
+
# @!macro dsl_model_properties_rw
|
106
|
+
property :encrypted_privk
|
107
|
+
# @!macro dsl_model_properties_rw
|
108
|
+
property :privk
|
109
|
+
|
110
|
+
# Decrypt group private key.
|
111
|
+
#
|
112
|
+
# Calling this method will set the {#privk} property.
|
113
|
+
#
|
114
|
+
# @param group_pubk [PublicKey] public key to verify ciphertext signature
|
115
|
+
# @param user_privk [PrivateKey] private key of owner of keychain item
|
116
|
+
def unlock(group_pubk, user_privk)
|
117
|
+
self.privk = Crypto.decrypt_group_privk(
|
118
|
+
group_pubk, user_privk, encrypted_privk
|
119
|
+
)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Re-encrypt group private key.
|
123
|
+
#
|
124
|
+
# Calling this will overwrite the {#encrypted_privk} property.
|
125
|
+
#
|
126
|
+
# @param user_pubk [KStor::Crypto::PublicKey] public key of keychain item
|
127
|
+
# owner
|
128
|
+
def encrypt(user_pubk)
|
129
|
+
self.encrypted_privk = Crypto.encrypt_group_privk(
|
130
|
+
user_pubk, privk, privk
|
131
|
+
)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Forget about decrypted group private key.
|
135
|
+
#
|
136
|
+
# This will unset {#privk} property.
|
137
|
+
def lock
|
138
|
+
self.privk = nil
|
139
|
+
end
|
140
|
+
|
141
|
+
# Check if group private key was decrypted.
|
142
|
+
#
|
143
|
+
# @return [Boolean] false if decrypted
|
144
|
+
def locked?
|
145
|
+
privk.nil?
|
146
|
+
end
|
147
|
+
|
148
|
+
# Check if group private key was decrypted.
|
149
|
+
#
|
150
|
+
# @return [Boolean] true if decrypted
|
151
|
+
def unlocked?
|
152
|
+
!locked?
|
153
|
+
end
|
154
|
+
|
155
|
+
# Dump properties except {#encrypted_privk}.
|
156
|
+
def to_h
|
157
|
+
h = super
|
158
|
+
h.delete('encrypted_privk')
|
159
|
+
h
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# A person allowed to connect to the application.
|
164
|
+
class User < Base
|
165
|
+
# @!macro dsl_model_properties_rw
|
166
|
+
property :id
|
167
|
+
# @!macro dsl_model_properties_rw
|
168
|
+
property :login
|
169
|
+
# @!macro dsl_model_properties_rw
|
170
|
+
property :name
|
171
|
+
# @!macro dsl_model_properties_rw
|
172
|
+
property :status
|
173
|
+
# @!macro dsl_model_properties_rw
|
174
|
+
property :pubk
|
175
|
+
# @!macro dsl_model_properties_rw
|
176
|
+
property :kdf_params
|
177
|
+
# @!macro dsl_model_properties_rw
|
178
|
+
property :encrypted_privk
|
179
|
+
# @!macro dsl_model_properties_rw
|
180
|
+
property :privk
|
181
|
+
# @!macro dsl_model_properties_rw
|
182
|
+
property :keychain
|
183
|
+
|
184
|
+
# Derive secret key from password.
|
185
|
+
#
|
186
|
+
# If user has no keypair yet, initialize it.
|
187
|
+
#
|
188
|
+
# @param password [String] plaintext password
|
189
|
+
# @return [KStor::Crypto::SecretKey] derived secret key
|
190
|
+
def secret_key(password)
|
191
|
+
Log.debug("model: deriving secret key for user #{login}")
|
192
|
+
reset_password(password) unless initialized?
|
193
|
+
Crypto.key_derive(password, kdf_params)
|
194
|
+
end
|
195
|
+
|
196
|
+
# Decrypt user private key and keychain.
|
197
|
+
#
|
198
|
+
# This will set the {#privk} property and call {KeychainItem#unlock} on
|
199
|
+
# the keychain.
|
200
|
+
#
|
201
|
+
# @param secret_key [KStor::Crypto::SecretKey] secret key derived from
|
202
|
+
# password
|
203
|
+
# @see #secret_key
|
204
|
+
def unlock(secret_key)
|
205
|
+
return if unlocked?
|
206
|
+
|
207
|
+
Log.debug("model: unlock user #{login}")
|
208
|
+
self.privk = Crypto.decrypt_user_privk(secret_key, encrypted_privk)
|
209
|
+
keychain.each_value { |it| it.unlock(it.group_pubk, privk) }
|
210
|
+
end
|
211
|
+
|
212
|
+
# Re-encrypt user private key and keychain.
|
213
|
+
#
|
214
|
+
# This will overwrite the {#encrypted_privk} property and call
|
215
|
+
# {KeychainItem#encrypt} on the keychain.
|
216
|
+
#
|
217
|
+
# @param secret_key [KStor::Crypto::SecretKey] secret key derived from
|
218
|
+
# password
|
219
|
+
# @see #secret_key
|
220
|
+
def encrypt(secret_key)
|
221
|
+
Log.debug("model: lock user data for #{login}")
|
222
|
+
self.encrypted_privk = Crypto.encrypt_user_privk(
|
223
|
+
secret_key, privk
|
224
|
+
)
|
225
|
+
keychain.each_value { |it| it.encrypt(pubk) }
|
226
|
+
end
|
227
|
+
|
228
|
+
# Forget about the user's decrypted private key and the group private
|
229
|
+
# keys in the keychain.
|
230
|
+
#
|
231
|
+
# This will unset the {#privk} property and call {KeychainItem#lock} on
|
232
|
+
# the keychain.
|
233
|
+
def lock
|
234
|
+
return if locked?
|
235
|
+
|
236
|
+
self.privk = nil
|
237
|
+
keychain.each_value(&:lock)
|
238
|
+
end
|
239
|
+
|
240
|
+
# Check if some sensitive data was decrypted.
|
241
|
+
#
|
242
|
+
# @return [Boolean] true if private key or keychain was decrypted
|
243
|
+
def locked?
|
244
|
+
privk.nil? && keychain.all? { |_, it| it.locked? }
|
245
|
+
end
|
246
|
+
|
247
|
+
# Check if no sensitive data was decrypted.
|
248
|
+
#
|
249
|
+
# @return [Boolean] true if neither private key nor any keychain iyem was
|
250
|
+
# decrypted.
|
251
|
+
def unlocked?
|
252
|
+
!privk.nil? || keychain.any? { |_, it| it.unlocked? }
|
253
|
+
end
|
254
|
+
|
255
|
+
# Generate a new key pair and throw away all keychain
|
256
|
+
# items.
|
257
|
+
#
|
258
|
+
# @param password [String] new user password
|
259
|
+
def reset_password(password)
|
260
|
+
Log.info("model: resetting password for user #{login}")
|
261
|
+
reset_key_pair
|
262
|
+
secret_key = Crypto.key_derive(password)
|
263
|
+
self.kdf_params = secret_key.kdf_params
|
264
|
+
encrypt(secret_key)
|
265
|
+
self.keychain = {}
|
266
|
+
# FIXME: delete keychain items from database!
|
267
|
+
# they won't be useable (decryption key is lost) but will provoke
|
268
|
+
# errors.
|
269
|
+
end
|
270
|
+
|
271
|
+
# Re-encrypt private key and keychain with a new secret key derived from
|
272
|
+
# the new password.
|
273
|
+
#
|
274
|
+
# @param password [String] old password
|
275
|
+
# @param new_password [String] new password
|
276
|
+
def change_password(password, new_password)
|
277
|
+
Log.info("model: changing password for user #{login}")
|
278
|
+
old_secret_key = secret_key(password)
|
279
|
+
unlock(old_secret_key)
|
280
|
+
new_secret_key = secret_key(new_password)
|
281
|
+
encrypt(new_secret_key)
|
282
|
+
end
|
283
|
+
|
284
|
+
# Dump properties except {#encrypted_privk} and {#pubk}.
|
285
|
+
def to_h
|
286
|
+
h = super
|
287
|
+
h.delete('encrypted_privk')
|
288
|
+
h.delete('pubk')
|
289
|
+
h['keychain'] = keychain.transform_values(&:to_h) if keychain
|
290
|
+
|
291
|
+
h
|
292
|
+
end
|
293
|
+
|
294
|
+
private
|
295
|
+
|
296
|
+
def initialized?
|
297
|
+
kdf_params && pubk && encrypted_privk
|
298
|
+
end
|
299
|
+
|
300
|
+
def reset_key_pair
|
301
|
+
Log.info("model: generating new key pair for user #{login}")
|
302
|
+
self.pubk, self.privk = Crypto.generate_key_pair
|
303
|
+
self.keychain = {}
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
# Metadata for a secret.
|
308
|
+
#
|
309
|
+
# This is not a "real" model object: just a helper class to convert
|
310
|
+
# metadata to and from database.
|
311
|
+
class SecretMeta
|
312
|
+
# Secret is defined for this application
|
313
|
+
attr_accessor :app
|
314
|
+
# Secret is defined for this database
|
315
|
+
attr_accessor :database
|
316
|
+
# Secret is defined for this login
|
317
|
+
attr_accessor :login
|
318
|
+
# Secret is related to this server
|
319
|
+
attr_accessor :server
|
320
|
+
# Secret should be used at this URL
|
321
|
+
attr_accessor :url
|
322
|
+
|
323
|
+
def initialize(values)
|
324
|
+
@app = values['app']
|
325
|
+
@database = values['database']
|
326
|
+
@login = values['login']
|
327
|
+
@server = values['server']
|
328
|
+
@url = values['url']
|
329
|
+
end
|
330
|
+
|
331
|
+
def to_h
|
332
|
+
{ 'app' => @app, 'database' => @database, 'login' => @login,
|
333
|
+
'server' => @server, 'url' => @url }.compact
|
334
|
+
end
|
335
|
+
|
336
|
+
def serialize
|
337
|
+
Crypto::ArmoredHash.from_hash(to_h)
|
338
|
+
end
|
339
|
+
|
340
|
+
def merge(other)
|
341
|
+
values = to_h.merge(other.to_h)
|
342
|
+
values.reject! { |_, v| v.empty? }
|
343
|
+
self.class.new(values)
|
344
|
+
end
|
345
|
+
|
346
|
+
def self.load(armored_hash)
|
347
|
+
new(armored_hash.to_hash)
|
348
|
+
end
|
349
|
+
|
350
|
+
def match?(meta)
|
351
|
+
self_h = to_h
|
352
|
+
other_h = meta.to_h
|
353
|
+
other_h.each do |k, wildcard|
|
354
|
+
val = self_h[k]
|
355
|
+
next if val.nil?
|
356
|
+
next if wildcard.nil?
|
357
|
+
|
358
|
+
key_matched = File.fnmatch?(
|
359
|
+
wildcard, val, File::FNM_CASEFOLD | File::FNM_DOTMATCH
|
360
|
+
)
|
361
|
+
return false unless key_matched
|
362
|
+
end
|
363
|
+
true
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
367
|
+
# A secret, with metadata and a value that are kept encrypted on disk.
|
368
|
+
class Secret < Base
|
369
|
+
# @!macro dsl_model_properties_rw
|
370
|
+
property :id
|
371
|
+
# @!macro dsl_model_properties_rw
|
372
|
+
property :value_author_id
|
373
|
+
# @!macro dsl_model_properties_rw
|
374
|
+
property :meta_author_id
|
375
|
+
# @!macro dsl_model_properties_rw
|
376
|
+
property :group_id
|
377
|
+
# @!macro dsl_model_properties_rw
|
378
|
+
property :ciphertext
|
379
|
+
# @!macro dsl_model_properties_rw
|
380
|
+
property :plaintext
|
381
|
+
# @!macro dsl_model_properties_rw
|
382
|
+
property :encrypted_metadata
|
383
|
+
# @!macro dsl_model_properties_rw
|
384
|
+
property :metadata, read_only: true
|
385
|
+
|
386
|
+
def metadata=(armored_hash)
|
387
|
+
@data[:metadata] = armored_hash ? SecretMeta.load(armored_hash) : nil
|
388
|
+
end
|
389
|
+
|
390
|
+
# Decrypt secret value.
|
391
|
+
#
|
392
|
+
# This will set the {#plaintext} property.
|
393
|
+
#
|
394
|
+
# @param author_pubk [KStor::Crypto::PublicKey] key to verify signature
|
395
|
+
# by the user that last set the value
|
396
|
+
# @param group_privk [KStor::Crypto::PrivateKey] private key of a group
|
397
|
+
# that can decrypt this secret value.
|
398
|
+
def unlock(author_pubk, group_privk)
|
399
|
+
self.plaintext = Crypto.decrypt_secret_value(
|
400
|
+
author_pubk, group_privk, ciphertext
|
401
|
+
)
|
402
|
+
end
|
403
|
+
|
404
|
+
# Decrypt secret metadata.
|
405
|
+
#
|
406
|
+
# This will set the {#metadata} property.
|
407
|
+
#
|
408
|
+
# @param author_pubk [KStor::Crypto::PublicKey] key to verify signature
|
409
|
+
# by the user that last set metadata
|
410
|
+
# @param group_privk [KStor::Crypto::PrivateKey] private key of a group
|
411
|
+
# that can decrypt this secret metadata.
|
412
|
+
def unlock_metadata(author_pubk, group_privk)
|
413
|
+
self.metadata = Crypto.decrypt_secret_metadata(
|
414
|
+
author_pubk, group_privk, encrypted_metadata
|
415
|
+
)
|
416
|
+
end
|
417
|
+
|
418
|
+
# Forget about the decrypted value and metadata.
|
419
|
+
def lock
|
420
|
+
self.metadata = nil
|
421
|
+
self.plaintext = nil
|
422
|
+
end
|
423
|
+
|
424
|
+
# Dump properties except {#ciphertext}, {#encrypted_metadata},
|
425
|
+
# {#value_author_id} and {#meta_author_id}.
|
426
|
+
def to_h
|
427
|
+
h = super
|
428
|
+
h.delete('ciphertext')
|
429
|
+
h.delete('encrypted_metadata')
|
430
|
+
h.delete('value_author_id')
|
431
|
+
h.delete('meta_author_id')
|
432
|
+
|
433
|
+
h
|
434
|
+
end
|
435
|
+
end
|
436
|
+
end
|
437
|
+
end
|
data/lib/kstor/server.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'kstor/socket_server'
|
4
|
+
require 'kstor/controller'
|
5
|
+
require 'kstor/message'
|
6
|
+
require 'kstor/log'
|
7
|
+
require 'kstor/error'
|
8
|
+
|
9
|
+
module KStor
|
10
|
+
# Error: invalid request.
|
11
|
+
class InvalidMessage < Error
|
12
|
+
error_code 'MSG/INVALID'
|
13
|
+
error_message 'JSON error: %s'
|
14
|
+
end
|
15
|
+
|
16
|
+
# Listen for clients and respond to their requests.
|
17
|
+
class Server < SocketServer
|
18
|
+
def initialize(controller:, **args)
|
19
|
+
@controller = controller
|
20
|
+
super(**args)
|
21
|
+
end
|
22
|
+
|
23
|
+
def work(client)
|
24
|
+
client_data, = client.recvfrom(4096)
|
25
|
+
Log.debug("server: read #{client_data.bytesize} bytes from client")
|
26
|
+
server_data = handle_client_data(client_data)
|
27
|
+
Log.debug("server: sending #{server_data.bytesize} bytes of response " \
|
28
|
+
'to client')
|
29
|
+
client.send(server_data, 0)
|
30
|
+
rescue Errno::EPIPE
|
31
|
+
Log.info('server: client unexpectedly broke connection')
|
32
|
+
ensure
|
33
|
+
client.close
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def handle_client_data(data)
|
39
|
+
req = Message.parse_request(data)
|
40
|
+
resp = @controller.handle_request(req)
|
41
|
+
Log.debug("server: response = #{resp.inspect}")
|
42
|
+
resp.serialize
|
43
|
+
rescue JSON::ParserError => e
|
44
|
+
err = Error.for_code('MSG/INVALID', e.message)
|
45
|
+
Log.info("server: #{err}")
|
46
|
+
err.response.serialize
|
47
|
+
rescue RequestMissesAuthData
|
48
|
+
raise Error.for_code('MSG/INVALID', 'Missing authentication data')
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'mutex_m'
|
4
|
+
|
5
|
+
module KStor
|
6
|
+
# A user session in memory.
|
7
|
+
class Session
|
8
|
+
attr_reader :id
|
9
|
+
attr_reader :user
|
10
|
+
attr_reader :secret_key
|
11
|
+
attr_reader :created_at
|
12
|
+
attr_reader :updated_at
|
13
|
+
|
14
|
+
def initialize(sid, user, secret_key)
|
15
|
+
@id = sid
|
16
|
+
@user = user
|
17
|
+
@secret_key = secret_key
|
18
|
+
@created_at = Time.now
|
19
|
+
@updated_at = Time.now
|
20
|
+
end
|
21
|
+
|
22
|
+
def update
|
23
|
+
@updated_at = Time.now
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.create(user, secret_key)
|
28
|
+
sid = SecureRandom.urlsafe_base64(16)
|
29
|
+
new(sid, user, secret_key)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Collection of user sessions (in memory)
|
34
|
+
#
|
35
|
+
# FIXME make it thread safe!
|
36
|
+
class SessionStore
|
37
|
+
def initialize(idle_timeout, life_timeout)
|
38
|
+
@idle_timeout = idle_timeout
|
39
|
+
@life_timeout = life_timeout
|
40
|
+
@sessions = {}
|
41
|
+
@sessions.extend(Mutex_m)
|
42
|
+
end
|
43
|
+
|
44
|
+
def <<(session)
|
45
|
+
@sessions.synchronize do
|
46
|
+
@sessions[session.id] = session
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def [](sid)
|
51
|
+
@sessions.synchronize do
|
52
|
+
s = @sessions[sid.to_s]
|
53
|
+
return nil if s.nil?
|
54
|
+
|
55
|
+
if invalid?(s)
|
56
|
+
@sessions.delete(s.id)
|
57
|
+
return nil
|
58
|
+
end
|
59
|
+
|
60
|
+
s.update
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def purge
|
65
|
+
now = Time.now
|
66
|
+
@sessions.synchronize do
|
67
|
+
@sessions.delete_if { |_, s| invalid?(s, now) }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def invalid?(session, now = Time.now)
|
74
|
+
return true if session.created_at + @life_timeout < now
|
75
|
+
return true if session.updated_at + @idle_timeout < now
|
76
|
+
|
77
|
+
false
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|