rims 0.2.2 → 0.2.3

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.
@@ -2,17 +2,15 @@
2
2
 
3
3
  module RIMS
4
4
  class Error < StandardError
5
- def self.suppress_2nd_error_at_resource_closing(logger: nil)
6
- if ($!) then
7
- begin
8
- yield
9
- rescue # not mask the first error
10
- logger.error($!) if logger
11
- nil
12
- end
13
- else
14
- yield
5
+ def self.trace_error_chain(exception)
6
+ return enum_for(:trace_error_chain, exception) unless block_given?
7
+
8
+ while (exception)
9
+ yield(exception)
10
+ exception = exception.cause
15
11
  end
12
+
13
+ nil
16
14
  end
17
15
  end
18
16
  end
@@ -68,6 +68,10 @@ module RIMS
68
68
  def get_plug_in(name)
69
69
  PLUG_IN[name] or raise KeyError, "not found a key-value store plug-in: #{name}"
70
70
  end
71
+
72
+ def plug_in_names
73
+ PLUG_IN.keys
74
+ end
71
75
  end
72
76
 
73
77
  def initialize
@@ -136,7 +136,10 @@ module RIMS
136
136
  retry
137
137
  rescue
138
138
  logger.error('unexpected error at a detached thread and give up to retry write-lock timeout error.')
139
- logger.error($!)
139
+ Error.trace_error_chain($!) do |exception|
140
+ logger.error(exception)
141
+ end
142
+
140
143
  $!
141
144
  end
142
145
  }
@@ -31,13 +31,19 @@ module RIMS
31
31
  end
32
32
  module_function :compile_wildcard
33
33
 
34
+ IO_DATA_DUMP = false # true for debug
35
+
34
36
  def io_data_log(str)
35
37
  s = '<'
36
38
  s << str.encoding.to_s
37
39
  if (str.ascii_only?) then
38
40
  s << ':ascii_only'
39
41
  end
40
- s << '> ' << str.inspect
42
+ if (IO_DATA_DUMP) then
43
+ s << '> ' << str.inspect
44
+ else
45
+ s << '> ' << str.bytesize.to_s << ' octets'
46
+ end
41
47
  end
42
48
  module_function :io_data_log
43
49
 
@@ -37,7 +37,9 @@ module RIMS
37
37
  atom_list = request_reader.read_command
38
38
  rescue
39
39
  logger.error('invalid client command.')
40
- logger.error($!)
40
+ Error.trace_error_chain($!) do |exception|
41
+ logger.error(exception)
42
+ end
41
43
  response_write.call([ "* BAD client command syntax error\r\n" ])
42
44
  next
43
45
  end
@@ -46,7 +48,16 @@ module RIMS
46
48
 
47
49
  tag, command, *opt_args = atom_list
48
50
  logger.info("client command: #{tag} #{command}")
49
- logger.debug("client command parameter: #{opt_args.inspect}") if logger.debug?
51
+ if (logger.debug?) then
52
+ case (command.upcase)
53
+ when 'LOGIN'
54
+ log_opt_args = opt_args.dup
55
+ log_opt_args[-1] = '********'
56
+ else
57
+ log_opt_args = opt_args
58
+ end
59
+ logger.debug("client command parameter: #{log_opt_args.inspect}")
60
+ end
50
61
 
51
62
  begin
52
63
  case (command.upcase)
@@ -126,7 +137,9 @@ module RIMS
126
137
  end
127
138
  rescue
128
139
  logger.error('unexpected error.')
129
- logger.error($!)
140
+ Error.trace_error_chain($!) do |exception|
141
+ logger.error(exception)
142
+ end
130
143
  response_write.call([ "#{tag} BAD unexpected error\r\n" ])
131
144
  end
132
145
 
@@ -139,7 +152,7 @@ module RIMS
139
152
 
140
153
  nil
141
154
  ensure
142
- Error.suppress_2nd_error_at_resource_closing(logger: logger) { decoder.cleanup }
155
+ decoder.cleanup
143
156
  end
144
157
 
145
158
  def initialize(auth, logger)
@@ -158,7 +171,9 @@ module RIMS
158
171
  rescue
159
172
  raise if ($!.class.name =~ /AssertionFailedError/)
160
173
  @logger.error('internal server error.')
161
- @logger.error($!)
174
+ Error.trace_error_chain($!) do |exception|
175
+ @logger.error(exception)
176
+ end
162
177
  res << "#{tag} BAD internal server error\r\n"
163
178
  end
164
179
  }
@@ -183,7 +198,9 @@ module RIMS
183
198
  rescue
184
199
  raise if ($!.class.name =~ /AssertionFailedError/)
185
200
  @logger.error('internal server error.')
186
- @logger.error($!)
201
+ Error.trace_error_chain($!) do |exception|
202
+ @logger.error(exception)
203
+ end
187
204
  yield([ "#{tag} BAD internal server error\r\n" ])
188
205
  end
189
206
  end
@@ -245,7 +262,7 @@ module RIMS
245
262
 
246
263
  class InitialDecoder < Decoder
247
264
  def initialize(mail_store_pool, auth, logger,
248
- mail_delivery_user: Server::DEFAULT[:mail_delivery_user],
265
+ mail_delivery_user: Service::DEFAULT_CONFIG.mail_delivery_user,
249
266
  write_lock_timeout_seconds: ReadWriteLock::DEFAULT_TIMEOUT_SECONDS,
250
267
  **next_decoder_optional)
251
268
  super(auth, logger)
@@ -190,7 +190,7 @@ module RIMS
190
190
 
191
191
  def read_client_response_data_plain(inline_client_response_data_base64)
192
192
  if (inline_client_response_data_base64) then
193
- @logger.debug("authenticate command: inline client response data: #{inline_client_response_data_base64}") if @logger.debug?
193
+ @logger.debug("authenticate command: inline client response data: #{Protocol.io_data_log(inline_client_response_data_base64)}") if @logger.debug?
194
194
  Protocol.decode_base64(inline_client_response_data_base64)
195
195
  else
196
196
  read_client_response_data
@@ -0,0 +1,953 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'etc'
4
+ require 'json'
5
+ require 'logger'
6
+ require 'logger/joint'
7
+ require 'pathname'
8
+ require 'riser'
9
+ require 'socket'
10
+ require 'yaml'
11
+
12
+ module RIMS
13
+ class Service
14
+ class Configuration
15
+ class << self
16
+ def stringify_symbol(collection)
17
+ case (collection)
18
+ when Hash
19
+ Hash[collection.map{|key, value| [ stringify_symbol(key), stringify_symbol(value) ] }]
20
+ when Array
21
+ collection.map{|i| stringify_symbol(i) }
22
+ else
23
+ case (value = collection)
24
+ when Symbol
25
+ value.to_s
26
+ else
27
+ value
28
+ end
29
+ end
30
+ end
31
+
32
+ def update(dest, other)
33
+ case (dest)
34
+ when Hash
35
+ unless (other.is_a? Hash) then
36
+ raise ArgumentError, 'hash can only be updated with hash.'
37
+ end
38
+ for key, value in other
39
+ dest[key] = update(dest[key], value)
40
+ end
41
+ dest
42
+ when Array
43
+ if (other.is_a? Array) then
44
+ dest.concat(other)
45
+ else
46
+ other
47
+ end
48
+ else
49
+ other
50
+ end
51
+ end
52
+
53
+ def get_configuration(collection, base_dir=nil)
54
+ if ((collection.key? 'configuration') && (collection.key? 'configuration_file')) then
55
+ raise KeyError, 'configuration conflict: configuration, configuraion_file'
56
+ end
57
+
58
+ if (collection.key? 'configuration') then
59
+ collection['configuration']
60
+ elsif (collection.key? 'configuration_file') then
61
+ configuration_file_path = Pathname(collection['configuration_file'])
62
+ if (configuration_file_path.relative?) then
63
+ base_dir or raise ArgumentError, 'need for base_dir.'
64
+ configuration_file_path = base_dir + configuration_file_path # expect base_dir to be Pathname
65
+ end
66
+ YAML.load_file(configuration_file_path.to_s)
67
+ else
68
+ {}
69
+ end
70
+ end
71
+ end
72
+
73
+ def initialize
74
+ @config = {}
75
+ end
76
+
77
+ def load(config, load_path=nil)
78
+ stringified_config = self.class.stringify_symbol(config)
79
+ if (stringified_config.key? 'base_dir') then
80
+ base_dir = Pathname(stringified_config['base_dir'])
81
+ if (load_path && base_dir.relative?) then
82
+ stringified_config['base_dir'] = Pathname(load_path) + base_dir
83
+ else
84
+ stringified_config['base_dir'] = base_dir
85
+ end
86
+ elsif (load_path) then
87
+ stringified_config['base_dir'] = Pathname(load_path)
88
+ end
89
+ self.class.update(@config, stringified_config)
90
+
91
+ self
92
+ end
93
+
94
+ # configuration example.
95
+ # required_features:
96
+ # - rims/qdbm
97
+ # - rims/passwd/ldap
98
+ # logging:
99
+ # file:
100
+ # path: rims.log
101
+ # shift_age: 10
102
+ # shift_size: 1048576
103
+ # level: debug
104
+ # datetime_format: %Y-%m-%d %H:%M:%S
105
+ # shift_period_suffix: %Y%m%d
106
+ # stdout:
107
+ # level: info
108
+ # datetime_format: %Y-%m-%d %H:%M:%S
109
+ # protocol:
110
+ # # default is not output.
111
+ # # to output, set the log level to info or less.
112
+ # path: protocol.log
113
+ # shift_age: 10
114
+ # shift_size: 1048576
115
+ # level: info
116
+ # datetime_format: %Y-%m-%d %H:%M:%S
117
+ # shift_period_suffix: %Y%m%d
118
+ # daemon:
119
+ # daemonize: true
120
+ # debug: false
121
+ # status_file: rims.pid
122
+ # server_polling_interval_seconds: 3
123
+ # server_privileged_user: nobody
124
+ # server_privileged_group: nogroup
125
+ # server:
126
+ # listen_address:
127
+ # # see `Riser::SocketAddress.parse' for address format
128
+ # type: tcp
129
+ # host: 0.0.0.0
130
+ # port: 143
131
+ # backlog: 64
132
+ # accept_polling_timeout_seconds: 0.1
133
+ # thread_num: 20
134
+ # thread_queue_size: 20
135
+ # thread_queue_polling_timeout_seconds: 0.1
136
+ # send_buffer_limit_size: 16384
137
+ # openssl:
138
+ # use_ssl: true
139
+ # ssl_context: |
140
+ # # this entry is evaluated in an anonymous ruby ​​module
141
+ # # including OpenSSL to initialize the SSLContext used
142
+ # # for TLS connection.
143
+ # # SSLContext object is stored at `_'.
144
+ # # Pathname object is stored at `base_dir'.
145
+ # _.cert = X509::Certificate.new((base_dir / "tls_cert" / "server_default.cert").read)
146
+ # _.key = PKey.read((base_dir / "tls_secret" / "server.priv_key").read)
147
+ # sni_tbl = {
148
+ # "imap.example.com" => SSLContext.new.tap{|c| c.key = _.key; c.cert = X509::Certificate.new((base_dir / "tls_cert" / "server_imap.cert").read) },
149
+ # "imap2.example.com" => SSLContext.new.tap{|c| c.key = _.key; c.cert = X509::Certificate.new((base_dir / "tls_cert" / "server_imap2.cert").read) },
150
+ # "imap3.example.com" => SSLContext.new.tap{|c| c.key = _.key; c.cert = X509::Certificate.new((base_dir / "tls_cert" / "server_imap3.cert").read) }
151
+ # }
152
+ # _.servername_cb = lambda{|ssl_socket, hostname| sni_tbl[hostname.downcase] }
153
+ # lock:
154
+ # read_lock_timeout_seconds: 30
155
+ # write_lock_timeout_seconds: 30
156
+ # cleanup_write_lock_timeout_seconds: 1
157
+ # storage:
158
+ # meta_key_value_store:
159
+ # type: qdbm_depot
160
+ # configuration:
161
+ # bnum 1200000
162
+ # use_checksum: true
163
+ # text_key_value_store:
164
+ # type: qdbm_curia
165
+ # configuration_file: text_kvs_config.yml
166
+ # use_checksum: true
167
+ # authentication:
168
+ # hostname: imap.example.com
169
+ # password_sources:
170
+ # - type: plain
171
+ # configuration:
172
+ # - user: alice
173
+ # pass: open sesame
174
+ # - user: bob
175
+ # pass: Z1ON0101
176
+ # - type: hash
177
+ # configuration_file: passwd_hash.yml
178
+ # - type: ldap
179
+ # configuration:
180
+ # ldap_uri: ldap://ldap.example.com/ou=user,o=example,dc=nodomain?uid?one?(memberOf=cn=imap,ou=group,o=example,dc=nodomain)
181
+ # authorization:
182
+ # mail_delivery_user: "#postman"
183
+ #
184
+ # backward compatibility for required_features.
185
+ # load_libraries:
186
+ # - rims/qdbm
187
+ # - rims/passwd/ldap
188
+ #
189
+ # backward compatibility for logging.
190
+ # log_file: rims.log
191
+ # log_level: debug
192
+ # log_shift_age: 10
193
+ # log_shift_size: 1048576
194
+ # log_stdout: info
195
+ #
196
+ # backward compatibility for daemon.
197
+ # process_privilege_user: nobody
198
+ # process_privilege_group: nogroup
199
+ #
200
+ # backward compatibility for server.
201
+ # imap_host: 0.0.0.0
202
+ # imap_port: 143
203
+ # ip_addr: 0.0.0.0
204
+ # ip_port: 143
205
+ # send_buffer_limit: 16384
206
+ #
207
+ # backward compatibility for lock.
208
+ # read_lock_timeout_seconds: 30
209
+ # write_lock_timeout_seconds: 30
210
+ # cleanup_write_lock_timeout_seconds: 1
211
+ #
212
+ # backward compatibility for storage.
213
+ # key_value_store_type: gdbm
214
+ # use_key_value_store_checksum: true
215
+ # meta_key_value_store:
216
+ # plug_in: qdbm_depot
217
+ # configuration:
218
+ # bnum 1200000
219
+ # use_checksum: true
220
+ # text_key_value_store:
221
+ # plug_in: qdbm_curia
222
+ # configuration_file: text_kvs_config.yml
223
+ # use_checksum: true
224
+ #
225
+ # backward compatibility for authentication.
226
+ # hostname: imap.example.com
227
+ # username: alice
228
+ # password: open sesame
229
+ # user_list:
230
+ # - user: alice
231
+ # pass: open sesame
232
+ # - user: bob
233
+ # pass: Z1ON0101
234
+ # authentication:
235
+ # - plug_in: plain
236
+ # configuration:
237
+ # - user: alice
238
+ # pass: open sesame
239
+ # - user: bob
240
+ # pass: Z1ON0101
241
+ # - plug_in: hash
242
+ # configuration_file: passwd_hash.yml
243
+ # - plug_in: ldap
244
+ # configuration:
245
+ # ldap_uri: ldap://ldap.example.com/ou=user,o=example,dc=nodomain?uid?one?(memberOf=cn=imap,ou=group,o=example,dc=nodomain)
246
+ #
247
+ # backward compatibility for authorization.
248
+ # mail_delivery_user: "#postman"
249
+ #
250
+ def load_yaml(path)
251
+ load(YAML.load_file(path), File.dirname(path))
252
+ self
253
+ end
254
+
255
+ def require_features
256
+ # built-in plug-in
257
+ require 'rims/gdbm_kvs'
258
+ require 'rims/passwd'
259
+
260
+ if (feature_list = get_required_features) then
261
+ for feature in feature_list
262
+ require(feature)
263
+ end
264
+ end
265
+
266
+ nil
267
+ end
268
+
269
+ def get_required_features
270
+ @config.dig('required_features') ||
271
+ @config.dig('load_libraries') || # for backward compatibility
272
+ []
273
+ end
274
+
275
+ def base_dir
276
+ @config['base_dir'] or raise KeyError, 'not defined base_dir.'
277
+ end
278
+
279
+ def get_configuration(collection)
280
+ if (@config.key? 'base_dir') then # to avoid an error with DEFAULT_CONFIG
281
+ self.class.get_configuration(collection, base_dir)
282
+ else
283
+ self.class.get_configuration(collection)
284
+ end
285
+ end
286
+ private :get_configuration
287
+
288
+ # return parameters for Logger.new
289
+ def make_file_logger_params
290
+ log_path = Pathname(@config.dig('logging', 'file', 'path') ||
291
+ @config.dig('log_file') || # for backward compatibility
292
+ 'rims.log')
293
+ if (log_path.relative?) then
294
+ if (@config.key? 'base_dir') then # to avoid an error with DEFAULT_CONFIG
295
+ log_path = base_dir + log_path
296
+ end
297
+ end
298
+ logger_params = [ log_path.to_s ]
299
+
300
+ shift_age = @config.dig('logging', 'file', 'shift_age') ||
301
+ @config.dig('log_shift_age') # for backward compatibility
302
+ shift_size = @config.dig('logging', 'file', 'shift_size') ||
303
+ @config.dig('log_shift_size') # for backward compatibility
304
+ if (shift_size) then
305
+ logger_params << (shift_age || 0)
306
+ logger_params << shift_size
307
+ elsif (shift_age) then
308
+ logger_params << shift_age
309
+ end
310
+
311
+ kw_args = {}
312
+ kw_args[:level] = @config.dig('logging', 'file', 'level') ||
313
+ @config.dig('log_level') || # for backward compatibility
314
+ 'info'
315
+ kw_args[:progname] = 'rims'
316
+ if (datetime_format = @config.dig('logging', 'file', 'datetime_format')) then
317
+ kw_args[:datetime_format] = datetime_format
318
+ end
319
+ if (shift_period_suffix = @config.dig('logging', 'file', 'shift_period_suffix')) then
320
+ kw_args[:shift_period_suffix] = shift_period_suffix
321
+ end
322
+ logger_params << kw_args
323
+
324
+ logger_params
325
+ end
326
+
327
+ # return parameters for Logger.new
328
+ def make_stdout_logger_params
329
+ logger_params = [ STDOUT ]
330
+
331
+ kw_args = {}
332
+ kw_args[:level] = @config.dig('logging', 'stdout', 'level') ||
333
+ @config.dig('log_stdout') || # for backward compatibility
334
+ 'info'
335
+ kw_args[:progname] = 'rims'
336
+ if (datetime_format = @config.dig('logging', 'stdout', 'datetime_format')) then
337
+ kw_args[:datetime_format] = datetime_format
338
+ end
339
+ logger_params << kw_args
340
+
341
+ logger_params
342
+ end
343
+
344
+ # return parameters for Logger.new
345
+ def make_protocol_logger_params
346
+ log_path = Pathname(@config.dig('logging', 'protocol', 'path') || 'protocol.log')
347
+ if (log_path.relative?) then
348
+ if (@config.key? 'base_dir') then # to avoid an error with DEFAULT_CONFIG
349
+ log_path = base_dir + log_path
350
+ end
351
+ end
352
+ logger_params = [ log_path.to_s ]
353
+
354
+ shift_age = @config.dig('logging', 'protocol', 'shift_age')
355
+ shift_size = @config.dig('logging', 'protocol', 'shift_size')
356
+ if (shift_size) then
357
+ logger_params << (shift_age || 0)
358
+ logger_params << shift_size
359
+ elsif (shift_age) then
360
+ logger_params << shift_age
361
+ end
362
+
363
+ kw_args = {}
364
+ kw_args[:level] = @config.dig('logging', 'protocol', 'level') || 'unknown'
365
+ kw_args[:progname] = 'rims'
366
+ if (datetime_format = @config.dig('logging', 'protocol', 'datetime_format')) then
367
+ kw_args[:datetime_format] = datetime_format
368
+ end
369
+ if (shift_period_suffix = @config.dig('logging', 'protocol', 'shift_period_suffix')) then
370
+ kw_args[:shift_period_suffix] = shift_period_suffix
371
+ end
372
+ logger_params << kw_args
373
+
374
+ logger_params
375
+ end
376
+
377
+ def daemonize?
378
+ daemon_config = @config['daemon'] || {}
379
+ if (daemon_config.key? 'daemonize') then
380
+ daemon_config['daemonize']
381
+ else
382
+ true
383
+ end
384
+ end
385
+
386
+ def daemon_name
387
+ 'rims'
388
+ end
389
+
390
+ def daemon_debug?
391
+ daemon_config = @config['daemon'] || {}
392
+ if (daemon_config.key? 'debug') then
393
+ daemon_config['debug']
394
+ else
395
+ false
396
+ end
397
+ end
398
+
399
+ def status_file
400
+ file_path = @config.dig('daemon', 'status_file') || 'rims.pid'
401
+ file_path = Pathname(file_path)
402
+ if (file_path.relative?) then
403
+ if (@config.key? 'base_dir') then # to avoid an error with DEFAULT_CONFIG
404
+ file_path = base_dir + file_path
405
+ end
406
+ end
407
+ file_path.to_s
408
+ end
409
+
410
+ def server_polling_interval_seconds
411
+ @config.dig('daemon', 'server_polling_interval_seconds') || 3
412
+ end
413
+
414
+ def server_restart_overlap_seconds
415
+ # to avoid resource conflict between the new server and the old server.
416
+ 0
417
+ end
418
+
419
+ def server_privileged_user
420
+ @config.dig('daemon', 'server_privileged_user') ||
421
+ @config.dig('process_privilege_user') # for backward compatibility
422
+ end
423
+
424
+ def server_privileged_group
425
+ @config.dig('daemon', 'server_privileged_group') ||
426
+ @config.dig('process_privilege_group') # for backward compatibility
427
+ end
428
+
429
+ def listen_address
430
+ if (socket_address = @config.dig('server', 'listen_address')) then
431
+ return socket_address
432
+ end
433
+
434
+ # for backward compatibility
435
+ host = @config.dig('imap_host') || @config.dig('ip_addr')
436
+ port = @config.dig('imap_port') || @config.dig('ip_port')
437
+ if (host || port) then
438
+ socket_adress = {
439
+ 'type' => 'tcp',
440
+ 'host' => host || '0.0.0.0',
441
+ 'port' => port || 1430
442
+ }
443
+ return socket_adress
444
+ end
445
+
446
+ # default
447
+ '0.0.0.0:1430'
448
+ end
449
+
450
+ def accept_polling_timeout_seconds
451
+ @config.dig('server', 'accept_polling_timeout_seconds') || 0.1
452
+ end
453
+
454
+ def process_num
455
+ # not yet supported multi-process server configuration.
456
+ 0
457
+ end
458
+
459
+ def process_queue_size
460
+ 20
461
+ end
462
+
463
+ def process_queue_polling_timeout_seconds
464
+ 0.1
465
+ end
466
+
467
+ def process_send_io_polling_timeout_seconds
468
+ 0.1
469
+ end
470
+
471
+ def thread_num
472
+ @config.dig('server', 'thread_num') || 20
473
+ end
474
+
475
+ def thread_queue_size
476
+ @config.dig('server', 'thread_queue_size') || 20
477
+ end
478
+
479
+ def thread_queue_polling_timeout_seconds
480
+ @config.dig('server', 'thread_queue_polling_timeout_seconds') || 0.1
481
+ end
482
+
483
+ def send_buffer_limit_size
484
+ @config.dig('server', 'send_buffer_limit_size') ||
485
+ @config.dig('send_buffer_limit') || # for backward compatibility
486
+ 1024 * 16
487
+ end
488
+
489
+ module SSLContextConfigAttribute
490
+ def ssl_context
491
+ @__ssl_context__
492
+ end
493
+
494
+ def base_dir
495
+ @__base_dir__
496
+ end
497
+
498
+ alias _ ssl_context
499
+
500
+ class << self
501
+ def new_module(ssl_context, base_dir)
502
+ _module = Module.new
503
+ _module.instance_variable_set(:@__ssl_context__, ssl_context)
504
+ _module.instance_variable_set(:@__base_dir__, base_dir)
505
+ _module.module_eval{
506
+ include OpenSSL
507
+ include OpenSSL::SSL
508
+ extend SSLContextConfigAttribute
509
+ }
510
+ _module
511
+ end
512
+
513
+ # methodized to isolate local variable scope.
514
+ def eval_config(_module, expr, filename='(eval_config)')
515
+ _module.module_eval(expr, filename)
516
+ end
517
+ end
518
+ end
519
+
520
+ def ssl_context
521
+ if (openssl_config = @config['openssl']) then
522
+ if (openssl_config.key? 'use_ssl') then
523
+ use_ssl = openssl_config['use_ssl']
524
+ else
525
+ use_ssl = openssl_config.key? 'ssl_context'
526
+ end
527
+
528
+ if (use_ssl) then
529
+ ssl_context = OpenSSL::SSL::SSLContext.new
530
+ if (ssl_config_expr = openssl_config['ssl_context']) then
531
+ anon_mod = SSLContextConfigAttribute.new_module(ssl_context, base_dir)
532
+ SSLContextConfigAttribute.eval_config(anon_mod, ssl_config_expr, 'ssl_context')
533
+ end
534
+
535
+ ssl_context
536
+ end
537
+ end
538
+ end
539
+
540
+ def read_lock_timeout_seconds
541
+ @config.dig('lock', 'read_lock_timeout_seconds') ||
542
+ @config.dig('read_lock_timeout_seconds') || # for backward compatibility
543
+ 30
544
+ end
545
+
546
+ def write_lock_timeout_seconds
547
+ @config.dig('lock', 'write_lock_timeout_seconds') ||
548
+ @config.dig('write_lock_timeout_seconds') || # for backward compatibility
549
+ 30
550
+ end
551
+
552
+ def cleanup_write_lock_timeout_seconds
553
+ @config.dig('lock', 'cleanup_write_lock_timeout_seconds') ||
554
+ @config.dig('cleanup_write_lock_timeout_seconds') || # for backward compatibility
555
+ 1
556
+ end
557
+
558
+ KeyValueStoreFactoryBuilderParams = Struct.new(:origin_type, :origin_config, :middleware_list)
559
+ class KeyValueStoreFactoryBuilderParams
560
+ def build_factory
561
+ builder = KeyValueStore::FactoryBuilder.new
562
+ builder.open{|name| origin_type.open_with_conf(name, origin_config) }
563
+ for middleware in middleware_list
564
+ builder.use(middleware)
565
+ end
566
+ builder.factory
567
+ end
568
+ end
569
+
570
+ def make_key_value_store_params(collection)
571
+ kvs_params = KeyValueStoreFactoryBuilderParams.new
572
+ kvs_params.origin_type = KeyValueStore::FactoryBuilder.get_plug_in(collection['type'] ||
573
+ collection['plug_in'] || # for backward compatibility
574
+ @config['key_value_store_type'] || # for backward compatibility
575
+ 'gdbm')
576
+ kvs_params.origin_config = get_configuration(collection)
577
+
578
+ if (collection.key? 'use_checksum') then
579
+ use_checksum = collection['use_checksum']
580
+ elsif (@config.key? 'use_key_value_store_checksum') then
581
+ use_checksum = @config['use_key_value_store_checksum'] # for backward compatibility
582
+ else
583
+ use_checksum = true # default
584
+ end
585
+
586
+ kvs_params.middleware_list = []
587
+ kvs_params.middleware_list << Checksum_KeyValueStore if use_checksum
588
+
589
+ kvs_params
590
+ end
591
+ private :make_key_value_store_params
592
+
593
+ def make_meta_key_value_store_params
594
+ make_key_value_store_params(@config.dig('storage', 'meta_key_value_store') ||
595
+ @config.dig('meta_key_value_store') || # for backward compatibility
596
+ {})
597
+ end
598
+
599
+ def make_text_key_value_store_params
600
+ make_key_value_store_params(@config.dig('storage', 'text_key_value_store') ||
601
+ @config.dig('text_key_value_store') || # for backward compatibility
602
+ {})
603
+ end
604
+
605
+ def make_key_value_store_path(mailbox_data_structure_version, unique_user_id)
606
+ if (mailbox_data_structure_version.empty?) then
607
+ raise ArgumentError, 'too short mailbox data structure version.'
608
+ end
609
+ if (unique_user_id.length <= 2) then
610
+ raise ArgumentError, 'too short unique user ID.'
611
+ end
612
+
613
+ bucket_dir_name = unique_user_id[0..1]
614
+ store_dir_name = unique_user_id[2..-1]
615
+
616
+ base_dir + mailbox_data_structure_version + bucket_dir_name + store_dir_name
617
+ end
618
+
619
+ def make_authentication
620
+ if ((@config.key? 'authentication') && (@config['authentication'].is_a? Hash)) then
621
+ auth_conf = @config['authentication']
622
+ else
623
+ auth_conf = {}
624
+ end
625
+
626
+ hostname = auth_conf['hostname'] ||
627
+ @config['hostname'] || # for backward compatibility
628
+ Socket.gethostname
629
+ auth = Authentication.new(hostname: hostname)
630
+
631
+ if (passwd_src_list = auth_conf['password_sources']) then
632
+ for passwd_src_conf in passwd_src_list
633
+ plug_in_name = passwd_src_conf['type'] or raise KeyError, 'not found a password source type.'
634
+ plug_in_config = get_configuration(passwd_src_conf)
635
+ passwd_src = Authentication.get_plug_in(plug_in_name, plug_in_config)
636
+ auth.add_plug_in(passwd_src)
637
+ end
638
+ end
639
+
640
+ # for backward compatibility
641
+ if (user_list = @config['user_list']) then
642
+ plain_src = Password::PlainSource.new
643
+ for pw in user_list
644
+ user = pw['user'] or raise KeyError, 'not found a user_list user.'
645
+ pass = pw['pass'] or raise KeyError, 'not found a user_list pass.'
646
+ plain_src.entry(user, pass)
647
+ end
648
+ auth.add_plug_in(plain_src)
649
+ end
650
+
651
+ # for backward compatibility
652
+ if (username = @config['username']) then
653
+ password = @config['password'] or raise KeyError, 'not found a password.'
654
+ plain_src = Password::PlainSource.new
655
+ plain_src.entry(username, password)
656
+ auth.add_plug_in(plain_src)
657
+ end
658
+
659
+ # for backward compatibility
660
+ if ((@config.key? 'authentication') && (@config['authentication'].is_a? Array)) then
661
+ plug_in_list = @config['authentication']
662
+ for plug_in_conf in plug_in_list
663
+ plug_in_name = plug_in_conf['plug_in'] or raise KeyError, 'not found an authentication plug_in.'
664
+ plug_in_config = get_configuration(plug_in_conf)
665
+ passwd_src = Authentication.get_plug_in(plug_in_name, plug_in_config)
666
+ auth.add_plug_in(passwd_src)
667
+ end
668
+ end
669
+
670
+ auth
671
+ end
672
+
673
+ def mail_delivery_user
674
+ @config.dig('authorization', 'mail_delivery_user') ||
675
+ @config.dig('mail_delivery_user') || # for backward compatibility
676
+ '#postman'
677
+ end
678
+ end
679
+
680
+ DEFAULT_CONFIG = Configuration.new
681
+
682
+ # default features
683
+ DEFAULT_CONFIG.require_features
684
+
685
+ # for read-only
686
+ class << DEFAULT_CONFIG
687
+ undef load
688
+ undef load_yaml
689
+ end
690
+
691
+ def initialize(config)
692
+ @config = config
693
+ end
694
+
695
+ using Logger::JointPlus
696
+
697
+ def setup(server, daemon: false)
698
+ file_logger_params = @config.make_file_logger_params
699
+ logger = Logger.new(*file_logger_params)
700
+
701
+ stdout_logger_params = @config.make_stdout_logger_params
702
+ unless (daemon && @config.daemonize?) then
703
+ logger += Logger.new(*stdout_logger_params)
704
+ end
705
+
706
+ protocol_logger_params = @config.make_protocol_logger_params
707
+ protocol_logger = Logger.new(*protocol_logger_params)
708
+
709
+ logger.info('preload libraries.')
710
+ Riser.preload
711
+ Riser.preload(RIMS)
712
+ Riser.preload(RIMS::Protocol)
713
+ @config.require_features
714
+
715
+ logger.info('setup server.')
716
+ server.accept_polling_timeout_seconds = @config.accept_polling_timeout_seconds
717
+ server.process_num = @config.process_num
718
+ server.process_queue_size = @config.process_queue_size
719
+ server.process_queue_polling_timeout_seconds = @config.process_queue_polling_timeout_seconds
720
+ server.process_send_io_polling_timeout_seconds = @config.process_send_io_polling_timeout_seconds
721
+ server.thread_num = @config.thread_num
722
+ server.thread_queue_size = @config.thread_queue_size
723
+ server.thread_queue_polling_timeout_seconds = @config.thread_queue_polling_timeout_seconds
724
+
725
+ ssl_context = @config.ssl_context
726
+
727
+ make_kvs_factory = lambda{|kvs_params, kvs_type|
728
+ kvs_factory = kvs_params.build_factory
729
+ return lambda{|mailbox_data_structure_version, unique_user_id, db_name|
730
+ kvs_path = @config.make_key_value_store_path(mailbox_data_structure_version, unique_user_id)
731
+ unless (kvs_path.directory?) then
732
+ logger.debug("make a directory: #{kvs_path}") if logger.debug?
733
+ kvs_path.mkpath
734
+ end
735
+ db_path = kvs_path + db_name
736
+ logger.debug("#{kvs_type} data key-value sotre path: #{db_path}") if logger.debug?
737
+ kvs_factory.call(db_path.to_s)
738
+ }, lambda{
739
+ logger.info("#{kvs_type} key-value store parameter: type=#{kvs_params.origin_type}")
740
+ logger.info("#{kvs_type} key-value store parameter: config=#{kvs_params.origin_config.to_json}")
741
+ kvs_params.middleware_list.each_with_index do |middleware, i|
742
+ logger.info("#{kvs_type} key-value store parameter: middleware[#{i}]=#{middleware}")
743
+ end
744
+ }
745
+ }
746
+
747
+ kvs_meta_open, kvs_meta_log = make_kvs_factory.call(@config.make_meta_key_value_store_params, 'meta')
748
+ kvs_text_open, kvs_text_log = make_kvs_factory.call(@config.make_text_key_value_store_params, 'text')
749
+ auth = @config.make_authentication
750
+ mail_store_pool = MailStore.build_pool(kvs_meta_open, kvs_text_open)
751
+
752
+ server.before_start{|server_socket|
753
+ logger.info('start server.')
754
+ for feature in @config.get_required_features
755
+ logger.info("required feature: #{feature}")
756
+ end
757
+ logger.info("file logging parameter: path=#{file_logger_params[0]}")
758
+ file_logger_params[1..-2].each_with_index do |value, i|
759
+ logger.info("file logging parameter: shift_args[#{i}]=#{value}")
760
+ end
761
+ for name, value in file_logger_params[-1]
762
+ logger.info("file logging parameter: #{name}=#{value}")
763
+ end
764
+ for name, value in stdout_logger_params[-1]
765
+ logger.info("stdout logging parameter: #{name}=#{value}")
766
+ end
767
+ logger.info("protocol logging parameter: path=#{protocol_logger_params[0]}")
768
+ protocol_logger_params[1..-2].each_with_index do |value, i|
769
+ logger.info("protocol logging parameter: shift_args[#{i}]=#{value}")
770
+ end
771
+ for name, value in protocol_logger_params[-1]
772
+ logger.info("protocol logging parameter: #{name}=#{value}")
773
+ end
774
+ logger.info("listen address: #{server_socket.local_address.inspect_sockaddr}")
775
+ privileged_user = Etc.getpwuid(Process.euid).name rescue ''
776
+ logger.info("server privileged user: #{privileged_user}(#{Process.euid})")
777
+ privileged_group = Etc.getgrgid(Process.egid).name rescue ''
778
+ logger.info("server privileged group: #{privileged_group}(#{Process.egid})")
779
+ logger.info("server parameter: accept_polling_timeout_seconds=#{server.accept_polling_timeout_seconds}")
780
+ logger.info("server parameter: process_num=#{server.process_num}")
781
+ logger.info("server parameter: process_queue_size=#{server.process_queue_size}")
782
+ logger.info("server parameter: process_queue_polling_timeout_seconds=#{server.process_queue_polling_timeout_seconds}")
783
+ logger.info("server parameter: process_send_io_polling_timeout_seconds=#{server.process_send_io_polling_timeout_seconds}")
784
+ logger.info("server parameter: thread_num=#{server.thread_num}")
785
+ logger.info("server parameter: thread_queue_size=#{server.thread_queue_size}")
786
+ logger.info("server parameter: thread_queue_polling_timeout_seconds=#{server.thread_queue_polling_timeout_seconds}")
787
+ logger.info("server parameter: send_buffer_limit_size=#{@config.send_buffer_limit_size}")
788
+ if (ssl_context) then
789
+ Array(ssl_context.alpn_protocols).each_with_index do |protocol, i|
790
+ logger.info("openssl parameter: alpn_protocols[#{i}]=#{protocol}")
791
+ end
792
+ logger.info("openssl parameter: alpn_select_cb=#{ssl_context.alpn_select_cb.inspect}") if ssl_context.alpn_select_cb
793
+ logger.info("openssl parameter: ca_file=#{ssl_context.ca_file}") if ssl_context.ca_file
794
+ logger.info("openssl parameter: ca_path=#{ssl_context.ca_path}") if ssl_context.ca_path
795
+ if (ssl_context.cert) then
796
+ ssl_context.cert.to_text.each_line do |line|
797
+ logger.info("openssl parameter: [cert] #{line.chomp}")
798
+ end
799
+ else
800
+ logger.warn('openssl parameter: not defined cert attribute.')
801
+ end
802
+ logger.info("openssl parameter: cert_store=#{ssl_context.cert_store.inspect}") if ssl_context.cert_store
803
+ Array(ssl_context.ciphers).each_with_index do |cipher, i|
804
+ logger.info("openssl parameter: ciphers[#{i}]=#{cipher.join(',')}")
805
+ end
806
+ Array(ssl_context.client_ca).each_with_index do |cert, i|
807
+ cert.to_text.each_line do |line|
808
+ logger.info("openssl parameter: client_ca[#{i}]: #{line.chomp}")
809
+ end
810
+ end
811
+ logger.info("openssl parameter: client_cert_cb=#{ssl_context.client_cert_cb.inspect}") if ssl_context.client_cert_cb
812
+ Array(ssl_context.extra_chain_cert).each_with_index do |cert, i|
813
+ cert.to_text.each_line do |line|
814
+ logger.info("openssl parameter: extra_chain_cert[#{i}]: #{line.chomp}")
815
+ end
816
+ end
817
+ if (ssl_context.key) then
818
+ logger.info("openssl parameter: key=#{ssl_context.key.inspect}")
819
+ if (logger.debug?) then
820
+ ssl_context.key.to_text.each_line do |line|
821
+ logger.debug("openssl parameter: [key] #{line.chomp}")
822
+ end
823
+ end
824
+ else
825
+ logger.warn('openssl parameter: not defined key attribute.')
826
+ end
827
+ Array(ssl_context.npn_protocols).each_with_index do |protocol, i|
828
+ logger.info("openssl parameter: npn_protocols[#{i}]=#{protocol}")
829
+ end
830
+ logger.info("openssl parameter: npn_select_cb=#{ssl_context.npn_select_cb.inspect}") if ssl_context.npn_select_cb
831
+ logger.info("openssl parameter: options=0x#{'%08x' % ssl_context.options}") if ssl_context.options
832
+ logger.info("openssl parameter: renegotiation_cb=#{ssl_context.renegotiation_cb.inspect}") if ssl_context.renegotiation_cb
833
+ logger.info("openssl parameter: security_level=#{ssl_context.security_level}")
834
+ logger.info("openssl parameter: servername_cb=#{ssl_context.servername_cb.inspect}") if ssl_context.servername_cb
835
+ logger.info("openssl parameter: session_cache_mode=0x#{'%08x' % ssl_context.session_cache_mode}")
836
+ logger.info("openssl parameter: session_cache_size=#{ssl_context.session_cache_size }")
837
+ logger.info("openssl parameter: session_get_cb=#{ssl_context.session_get_cb.inspect}") if ssl_context.session_get_cb
838
+ logger.info("openssl parameter: session_id_context=#{ssl_context.session_id_context}") if ssl_context.session_id_context
839
+ logger.info("openssl parameter: session_new_cb=#{ssl_context.session_new_cb.inspect}") if ssl_context.session_new_cb
840
+ logger.info("openssl parameter: session_remove_cb=#{ssl_context.session_remove_cb}") if ssl_context.session_remove_cb
841
+ logger.info("openssl parameter: ssl_timeout=#{ssl_context.ssl_timeout}") if ssl_context.ssl_timeout
842
+ logger.info("openssl parameter: tmp_dh_callback=#{ssl_context.tmp_dh_callback}") if ssl_context.tmp_dh_callback
843
+ logger.info("openssl parameter: verify_callback=#{ssl_context.verify_callback}") if ssl_context.verify_callback
844
+ logger.info("openssl parameter: verify_depth=#{ssl_context.verify_depth}") if ssl_context.verify_depth
845
+ logger.info("openssl parameter: verify_hostname=#{ssl_context.verify_hostname}") if ssl_context.verify_hostname
846
+ logger.info("openssl parameter: verify_mode=0x#{'%08x' % ssl_context.verify_mode}") if ssl_context.verify_mode
847
+ end
848
+ logger.info("lock parameter: read_lock_timeout_seconds=#{@config.read_lock_timeout_seconds}")
849
+ logger.info("lock parameter: write_lock_timeout_seconds=#{@config.write_lock_timeout_seconds}")
850
+ logger.info("lock parameter: cleanup_write_lock_timeout_seconds=#{@config.cleanup_write_lock_timeout_seconds}")
851
+ kvs_meta_log.call
852
+ kvs_text_log.call
853
+ logger.info("authentication parameter: hostname=#{auth.hostname}")
854
+ logger.info("authorization parameter: mail_delivery_user=#{@config.mail_delivery_user}")
855
+ }
856
+ # server.at_fork{}
857
+ # server.at_stop{}
858
+ server.at_stat{|info|
859
+ logger.info("stat: #{info.to_json}")
860
+ }
861
+ server.preprocess{
862
+ auth.start_plug_in(logger)
863
+ }
864
+ server.dispatch{|socket|
865
+ begin
866
+ begin
867
+ begin
868
+ begin
869
+ remote_address = socket.remote_address # the place where the remote socket is most likely not closed is here
870
+ logger.info("accept connection: #{remote_address.inspect_sockaddr}")
871
+ if (ssl_context) then
872
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
873
+ logger.info("start tls: #{ssl_socket.state}")
874
+ ssl_socket.accept
875
+ logger.info("accept tls: #{ssl_socket.state}")
876
+ ssl_socket.sync = true
877
+ stream = Riser::WriteBufferStream.new(ssl_socket, @config.send_buffer_limit_size)
878
+ else
879
+ stream = Riser::WriteBufferStream.new(socket, @config.send_buffer_limit_size)
880
+ end
881
+ stream = Riser::LoggingStream.new(stream, protocol_logger)
882
+ decoder = Protocol::Decoder.new_decoder(mail_store_pool, auth, logger,
883
+ mail_delivery_user: @config.mail_delivery_user,
884
+ read_lock_timeout_seconds: @config.read_lock_timeout_seconds,
885
+ write_lock_timeout_seconds: @config.write_lock_timeout_seconds,
886
+ cleanup_write_lock_timeout_seconds: @config.cleanup_write_lock_timeout_seconds)
887
+ Protocol::Decoder.repl(decoder, stream, stream, logger)
888
+ ensure
889
+ if (stream) then
890
+ stream.flush
891
+ end
892
+ end
893
+ ensure
894
+ if (ssl_socket) then
895
+ ssl_socket.close
896
+ logger.info("close tls: #{ssl_socket.state}")
897
+ end
898
+ end
899
+ ensure
900
+ socket.close
901
+ if (remote_address) then
902
+ logger.info("close connection: #{remote_address.inspect_sockaddr}")
903
+ else
904
+ logger.info('close connection.')
905
+ end
906
+ end
907
+ rescue
908
+ logger.error('interrupt connection with unexpected error.')
909
+ Error.trace_error_chain($!) do |exception|
910
+ logger.error(exception)
911
+ end
912
+ end
913
+ }
914
+ server.postprocess{
915
+ auth.stop_plug_in(logger)
916
+ }
917
+ server.after_stop{
918
+ logger.info('stop server.')
919
+ }
920
+
921
+ nil
922
+ end
923
+ end
924
+ end
925
+
926
+ if ($0 == __FILE__) then
927
+ require 'pp' if $DEBUG
928
+ require 'rims'
929
+
930
+ if (ARGV.length != 1) then
931
+ STDERR.puts "usage: #{$0} config.yml"
932
+ exit(1)
933
+ end
934
+
935
+ config = RIMS::Service::Configuration.new
936
+ config.load_yaml(ARGV[0])
937
+ pp config if $DEBUG
938
+
939
+ server = Riser::SocketServer.new
940
+ service = RIMS::Service.new(config)
941
+ service.setup(server)
942
+
943
+ Signal.trap(:INT) { server.signal_stop_forced }
944
+ Signal.trap(:TERM) { server.signal_stop_graceful }
945
+
946
+ listen_address = Riser::SocketAddress.parse(config.listen_address)
947
+ server.start(listen_address.open_server)
948
+ end
949
+
950
+ # Local Variables:
951
+ # mode: Ruby
952
+ # indent-tabs-mode: nil
953
+ # End: