kstor 0.4.0
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.
- 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
|