schleuder 3.2.2 → 3.5.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -11
  3. data/Rakefile +18 -10
  4. data/bin/schleuder +2 -1
  5. data/bin/schleuder-api-daemon +3 -2
  6. data/db/migrate/20180110203100_add_sig_enc_to_headers_to_meta_defaults.rb +30 -0
  7. data/db/migrate/20180723173900_add_deliver_selfsent_to_list.rb +11 -0
  8. data/db/migrate/20190906194820_add_autocrypt_header_to_list.rb +11 -0
  9. data/db/schema.rb +4 -2
  10. data/etc/list-defaults.yml +13 -3
  11. data/etc/schleuder.yml +11 -0
  12. data/lib/schleuder-api-daemon.rb +9 -354
  13. data/lib/schleuder-api-daemon/helpers/schleuder-api-daemon-helper.rb +143 -0
  14. data/lib/schleuder-api-daemon/routes/key.rb +40 -0
  15. data/lib/schleuder-api-daemon/routes/list.rb +69 -0
  16. data/lib/schleuder-api-daemon/routes/status.rb +5 -0
  17. data/lib/schleuder-api-daemon/routes/subscription.rb +99 -0
  18. data/lib/schleuder-api-daemon/routes/version.rb +5 -0
  19. data/lib/schleuder.rb +12 -3
  20. data/lib/schleuder/cli.rb +33 -3
  21. data/lib/schleuder/cli/subcommand_fix.rb +1 -1
  22. data/lib/schleuder/conf.rb +7 -1
  23. data/lib/schleuder/errors/active_model_error.rb +2 -5
  24. data/lib/schleuder/errors/decryption_failed.rb +2 -7
  25. data/lib/schleuder/errors/key_adduid_failed.rb +1 -5
  26. data/lib/schleuder/errors/key_generation_failed.rb +1 -8
  27. data/lib/schleuder/errors/keyword_admin_only.rb +1 -5
  28. data/lib/schleuder/errors/list_not_found.rb +1 -5
  29. data/lib/schleuder/errors/listdir_problem.rb +2 -7
  30. data/lib/schleuder/errors/loading_list_settings_failed.rb +2 -5
  31. data/lib/schleuder/errors/message_empty.rb +1 -5
  32. data/lib/schleuder/errors/message_not_from_admin.rb +2 -5
  33. data/lib/schleuder/errors/message_sender_not_subscribed.rb +2 -5
  34. data/lib/schleuder/errors/message_too_big.rb +2 -5
  35. data/lib/schleuder/errors/message_unauthenticated.rb +1 -4
  36. data/lib/schleuder/errors/message_unencrypted.rb +2 -5
  37. data/lib/schleuder/errors/message_unsigned.rb +2 -5
  38. data/lib/schleuder/errors/too_many_keys.rb +1 -8
  39. data/lib/schleuder/filters/{request_filter.rb → post_decryption/10_request.rb} +0 -0
  40. data/lib/schleuder/filters/{max_message_size.rb → post_decryption/20_max_message_size.rb} +0 -0
  41. data/lib/schleuder/filters/{forward_filter.rb → post_decryption/30_forward_to_owner.rb} +0 -0
  42. data/lib/schleuder/filters/post_decryption/40_receive_admin_only.rb +10 -0
  43. data/lib/schleuder/filters/post_decryption/50_receive_authenticated_only.rb +10 -0
  44. data/lib/schleuder/filters/post_decryption/60_receive_signed_only.rb +10 -0
  45. data/lib/schleuder/filters/post_decryption/70_receive_encrypted_only.rb +10 -0
  46. data/lib/schleuder/filters/post_decryption/80_receive_from_subscribed_emailaddresses_only.rb +10 -0
  47. data/lib/schleuder/filters/post_decryption/90_strip_html_from_alternative_if_keywords_present.rb +21 -0
  48. data/lib/schleuder/filters/{bounces_filter.rb → pre_decryption/10_forward_bounce_to_admins.rb} +0 -0
  49. data/lib/schleuder/filters/{forward_incoming.rb → pre_decryption/20_forward_all_incoming_to_admins.rb} +0 -0
  50. data/lib/schleuder/filters/{send_key_filter.rb → pre_decryption/30_send_key.rb} +0 -0
  51. data/lib/schleuder/filters/{hotmail_message_filter.rb → pre_decryption/40_fix_exchange_messages.rb} +5 -3
  52. data/lib/schleuder/filters/{strip_alternative_filter.rb → pre_decryption/50_strip_html_from_alternative.rb} +1 -1
  53. data/lib/schleuder/filters_runner.rb +41 -31
  54. data/lib/schleuder/gpgme/ctx.rb +24 -3
  55. data/lib/schleuder/gpgme/import_status.rb +13 -7
  56. data/lib/schleuder/gpgme/key.rb +8 -0
  57. data/lib/schleuder/list.rb +26 -4
  58. data/lib/schleuder/logger_notifications.rb +8 -1
  59. data/lib/schleuder/mail/encrypted_part.rb +14 -0
  60. data/lib/schleuder/mail/gpg.rb +15 -0
  61. data/lib/schleuder/mail/message.rb +97 -49
  62. data/lib/schleuder/plugins/attach_listkey.rb +6 -10
  63. data/lib/schleuder/plugins/key_management.rb +34 -26
  64. data/lib/schleuder/plugins/resend.rb +14 -11
  65. data/lib/schleuder/plugins/subscription_management.rb +70 -3
  66. data/lib/schleuder/runner.rb +49 -10
  67. data/lib/schleuder/subscription.rb +5 -9
  68. data/lib/schleuder/validators/fingerprint_validator.rb +1 -1
  69. data/lib/schleuder/version.rb +1 -1
  70. data/locales/de.yml +101 -9
  71. data/locales/en.yml +107 -11
  72. metadata +72 -34
  73. data/lib/schleuder/errors/file_not_found.rb +0 -14
  74. data/lib/schleuder/errors/invalid_listname.rb +0 -13
  75. data/lib/schleuder/errors/list_exists.rb +0 -13
  76. data/lib/schleuder/errors/unknown_list_option.rb +0 -14
  77. data/lib/schleuder/filters/auth_filter.rb +0 -39
@@ -0,0 +1,143 @@
1
+ module SchleuderApiDaemonHelper
2
+ def valid_credentials?
3
+ @auth ||= Rack::Auth::Basic::Request.new(request.env)
4
+ if @auth.provided? && @auth.basic? && @auth.credentials.present?
5
+ username, api_key = @auth.credentials
6
+ username == 'schleuder' && Conf.api_valid_api_keys.include?(api_key)
7
+ else
8
+ false
9
+ end
10
+ end
11
+
12
+ def authenticate!
13
+ # Be careful to use path_info() — it can be changed by other filters!
14
+ return if request.path_info == '/status.json'
15
+ if ! valid_credentials?
16
+ headers['WWW-Authenticate'] = 'Basic realm="Schleuder API Daemon"'
17
+ halt 401, "Not authorized\n"
18
+ end
19
+ end
20
+
21
+ def list(id_or_email=nil)
22
+ if id_or_email.blank?
23
+ if params[:list_id].present?
24
+ id_or_email = params[:list_id]
25
+ else
26
+ client_error "Parameter list_id is required"
27
+ end
28
+ end
29
+ if is_an_integer?(id_or_email)
30
+ list = List.where(id: id_or_email).first
31
+ else
32
+ # list_id is actually an email address
33
+ list = List.where(email: id_or_email).first
34
+ end
35
+ list || halt(404)
36
+ end
37
+
38
+ def subscription(id_or_email)
39
+ if is_an_integer?(id_or_email)
40
+ sub = Subscription.where(id: id_or_email.to_i).first
41
+ else
42
+ # Email
43
+ if params[:list_id].blank?
44
+ client_error "Parameter list_id is required when using email as identifier for subscriptions."
45
+ else
46
+ sub = list.subscriptions.where(email: id_or_email).first
47
+ end
48
+ end
49
+ sub || halt(404)
50
+ end
51
+
52
+ def requested_list_id
53
+ # ActiveResource doesn't want to use query-params with create(), so here
54
+ # list_id might be included in the request-body.
55
+ params['list_id'] || parsed_body['list_id'] || client_error('Need list_id')
56
+ end
57
+
58
+ def parsed_body
59
+ @parsed_body ||= begin
60
+ b = JSON.parse(request.body.read)
61
+ logger.debug "parsed body: #{b.inspect}"
62
+ b
63
+ end
64
+ end
65
+
66
+ def server_error(msg)
67
+ logger.warn msg
68
+ halt(500, json(error: msg))
69
+ end
70
+
71
+ # TODO: unify error messages. This method currently sends an old error format. See <https://github.com/rails/activeresource/blob/d6a5186/lib/active_resource/base.rb#L227>.
72
+ def client_error(obj_or_msg, http_code=400)
73
+ text = case obj_or_msg
74
+ when String, Symbol
75
+ obj_or_msg.to_s
76
+ when ActiveRecord::Base
77
+ obj_or_msg.errors.full_messages
78
+ else
79
+ obj_or_msg
80
+ end
81
+ logger.error "Sending error to client: #{text.inspect}"
82
+ halt(http_code, json(errors: text))
83
+ end
84
+
85
+ # poor persons type casting
86
+ def cast_param_values
87
+ params.each do |key, value|
88
+ params[key] =
89
+ case value
90
+ when 'true' then true
91
+ when 'false' then false
92
+ when '0' then 0
93
+ when is_an_integer?(value) then value.to_i
94
+ else value
95
+ end
96
+ end
97
+ end
98
+
99
+ def key_to_hash(key, include_keydata=false)
100
+ hash = {
101
+ fingerprint: key.fingerprint,
102
+ email: key.email,
103
+ expiry: key.expires,
104
+ generated_at: key.generated_at,
105
+ primary_uid: key.primary_uid.uid,
106
+ oneline: key.oneline,
107
+ trust_issues: key.usability_issue
108
+ }
109
+ if include_keydata
110
+ hash[:description] = key.to_s
111
+ hash[:ascii] = key.armored
112
+ end
113
+ hash
114
+ end
115
+
116
+ def set_x_messages(messages)
117
+ if messages.present?
118
+ headers 'X-Messages' => Array(messages).join(' // ').gsub(/\n/, ' // ')
119
+ end
120
+ end
121
+
122
+ def find_key_material
123
+ key_material = parsed_body['key_material'].presence
124
+ # By convention key_material is either ASCII or base64-encoded.
125
+ if key_material && ! key_material.match('BEGIN PGP')
126
+ key_material = Base64.decode64(key_material)
127
+ end
128
+ key_material
129
+ end
130
+
131
+ def find_attributes_from_body(attribs)
132
+ Array(attribs).inject({}) do |memo, attrib|
133
+ if parsed_body.has_key?(attrib)
134
+ memo[attrib] = parsed_body[attrib]
135
+ end
136
+ memo
137
+ end
138
+ end
139
+
140
+ def is_an_integer?(input)
141
+ input.to_s.match(/^[0-9]+$/).present?
142
+ end
143
+ end
@@ -0,0 +1,40 @@
1
+ class SchleuderApiDaemon < Sinatra::Base
2
+ register Sinatra::Namespace
3
+
4
+ namespace '/keys' do
5
+ get '.json' do
6
+ keys = list.keys.sort_by(&:email).map do |key|
7
+ key_to_hash(key)
8
+ end
9
+ json keys
10
+ end
11
+
12
+ post '.json' do
13
+ input = parsed_body['keymaterial']
14
+ if ! input.match('BEGIN PGP')
15
+ input = Base64.decode64(input)
16
+ end
17
+ json list(requested_list_id).import_key(input)
18
+ end
19
+
20
+ get '/check_keys.json' do
21
+ json result: list.check_keys
22
+ end
23
+
24
+ get '/:fingerprint.json' do |fingerprint|
25
+ if key = list.key(fingerprint)
26
+ json key_to_hash(key, true)
27
+ else
28
+ 404
29
+ end
30
+ end
31
+
32
+ delete '/:fingerprint.json' do |fingerprint|
33
+ if list.delete_key(fingerprint)
34
+ 200
35
+ else
36
+ 404
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,69 @@
1
+ class SchleuderApiDaemon < Sinatra::Base
2
+ register Sinatra::Namespace
3
+
4
+ namespace '/lists' do
5
+ get '.json' do
6
+ json List.all, include: :subscriptions
7
+ end
8
+
9
+ post '.json' do
10
+ listname = parsed_body['email']
11
+ fingerprint = parsed_body['fingerprint']
12
+ adminaddress = parsed_body['adminaddress']
13
+ adminfingerprint = parsed_body['adminfingerprint']
14
+ adminkey = parsed_body['adminkey']
15
+ list, messages = ListBuilder.new({email: listname, fingerprint: fingerprint}, adminaddress, adminfingerprint, adminkey).run
16
+ if list.nil?
17
+ client_error(messages, 422)
18
+ elsif ! list.valid?
19
+ client_error(list, 422)
20
+ else
21
+ set_x_messages(messages)
22
+ body json(list)
23
+ end
24
+ end
25
+
26
+ get '/configurable_attributes.json' do
27
+ json(List.configurable_attributes) + "\n"
28
+ end
29
+
30
+ post '/send_list_key_to_subscriptions.json' do
31
+ json(result: list.send_list_key_to_subscriptions)
32
+ end
33
+
34
+ get '/new.json' do
35
+ json List.new
36
+ end
37
+
38
+ get '/:id.json' do |id|
39
+ json list(id)
40
+ end
41
+
42
+ put '/:id.json' do |id|
43
+ list = list(id)
44
+ if list.update(parsed_body)
45
+ 204
46
+ else
47
+ client_error(list)
48
+ end
49
+ end
50
+
51
+ patch '/:id.json' do |id|
52
+ list = list(id)
53
+ if list.update(parsed_body)
54
+ 204
55
+ else
56
+ client_error(list)
57
+ end
58
+ end
59
+
60
+ delete '/:id.json' do |id|
61
+ list = list(id)
62
+ if list.destroy
63
+ 200
64
+ else
65
+ client_error(list)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,5 @@
1
+ class SchleuderApiDaemon < Sinatra::Base
2
+ get '/status.json' do
3
+ json status: :ok
4
+ end
5
+ end
@@ -0,0 +1,99 @@
1
+ class SchleuderApiDaemon < Sinatra::Base
2
+ register Sinatra::Namespace
3
+
4
+ namespace '/subscriptions' do
5
+ get '.json' do
6
+ filterkeys = Subscription.configurable_attributes + [:list_id, :email]
7
+ filter = params.select do |param|
8
+ filterkeys.include?(param.to_sym)
9
+ end
10
+
11
+ logger.debug "Subscription filter: #{filter.inspect}"
12
+ if filter['list_id'] && ! is_an_integer?(filter['list_id'])
13
+ # Value is an email-address
14
+ if list = List.where(email: filter['list_id']).first
15
+ filter['list_id'] = list.id
16
+ else
17
+ status 404
18
+ return json(errors: 'No such list')
19
+ end
20
+ end
21
+
22
+ json Subscription.where(filter)
23
+ end
24
+
25
+ post '.json' do
26
+ begin
27
+ list = list(requested_list_id)
28
+ # We don't have to care about nil-values, subscribe() does that for us.
29
+ sub, msgs = list.subscribe(
30
+ parsed_body['email'],
31
+ parsed_body['fingerprint'],
32
+ parsed_body['admin'],
33
+ parsed_body['delivery_enabled'],
34
+ find_key_material
35
+ )
36
+ set_x_messages(msgs)
37
+ logger.debug "subcription: #{sub.inspect}"
38
+ if sub.valid?
39
+ logger.debug "Subscribed: #{sub.inspect}"
40
+ # TODO: why redirect instead of respond with result?
41
+ redirect to("/subscriptions/#{sub.id}.json"), 201
42
+ else
43
+ client_error(sub, 422)
44
+ end
45
+ rescue ActiveRecord::RecordNotUnique
46
+ logger.error "Already subscribed"
47
+ status 422
48
+ json errors: {email: ['is already subscribed']}
49
+ end
50
+ end
51
+
52
+ get '/configurable_attributes.json' do
53
+ json(Subscription.configurable_attributes) + "\n"
54
+ end
55
+
56
+ get '/new.json' do
57
+ json Subscription.new
58
+ end
59
+
60
+ get '/:id.json' do |id|
61
+ json subscription(id)
62
+ end
63
+
64
+ put '/:id.json' do |id|
65
+ sub = subscription(id)
66
+ list = sub.list
67
+ args = find_attributes_from_body(%w[email fingerprint admin delivery_enabled])
68
+ fingerprint, messages = list.import_key_and_find_fingerprint(find_key_material)
69
+ set_x_messages(messages)
70
+ # For an already existing subscription, only update fingerprint if a
71
+ # new one has been selected from the upload.
72
+ if fingerprint.present?
73
+ args["fingerprint"] = fingerprint
74
+ end
75
+ if sub.update(args)
76
+ 200
77
+ else
78
+ client_error(sub, 422)
79
+ end
80
+ end
81
+
82
+ patch '/:id.json' do |id|
83
+ sub = subscription(id)
84
+ if sub.update(parsed_body)
85
+ 200
86
+ else
87
+ client_error(sub)
88
+ end
89
+ end
90
+
91
+ delete '/:id.json' do |id|
92
+ if sub = subscription(id).destroy
93
+ 200
94
+ else
95
+ client_error(sub)
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,5 @@
1
+ class SchleuderApiDaemon < Sinatra::Base
2
+ get '/version.json' do
3
+ json version: Schleuder::VERSION
4
+ end
5
+ end
@@ -1,3 +1,10 @@
1
+ # default to UTF-8 encoding as early as possible for external
2
+ # data.
3
+ #
4
+ # this should ensure we are able to parse most incoming
5
+ # plain text mails in different charsets.
6
+ Encoding.default_external = Encoding::UTF_8
7
+
1
8
  # Stdlib
2
9
  require 'fileutils'
3
10
  require 'singleton'
@@ -22,6 +29,8 @@ $:.unshift libdir
22
29
  # Monkeypatches
23
30
  require 'schleuder/mail/parts_list.rb'
24
31
  require 'schleuder/mail/message.rb'
32
+ require 'schleuder/mail/gpg.rb'
33
+ require 'schleuder/mail/encrypted_part.rb'
25
34
  require 'schleuder/gpgme/import_status.rb'
26
35
  require 'schleuder/gpgme/key.rb'
27
36
  require 'schleuder/gpgme/sub_key.rb'
@@ -46,9 +55,6 @@ Dir["#{libdir}/schleuder/plugins/*.rb"].each do |file|
46
55
  require file
47
56
  end
48
57
  require 'schleuder/filters_runner'
49
- Dir["#{libdir}/schleuder/filters/*.rb"].each do |file|
50
- require file
51
- end
52
58
  Dir["#{libdir}/schleuder/validators/*.rb"].each do |file|
53
59
  require file
54
60
  end
@@ -62,6 +68,9 @@ ENV["SCHLEUDER_CONFIG"] ||= '/etc/schleuder/schleuder.yml'
62
68
  ENV["SCHLEUDER_LIST_DEFAULTS"] ||= '/etc/schleuder/list-defaults.yml'
63
69
  ENV["SCHLEUDER_ENV"] ||= 'production'
64
70
  ENV["SCHLEUDER_ROOT"] = rootdir.to_s
71
+ # Ensure that gnupg never-ever tries to ask for a passphrase.
72
+ ENV["GPG_TTY"] = "/nonexistant-#{rand}"
73
+ ENV["DISPLAY"] = nil
65
74
 
66
75
  GPGME::Ctx.set_gpg_path_from_env
67
76
  GPGME::Ctx.check_gpg_version
@@ -1,6 +1,7 @@
1
1
  require 'thor'
2
2
  require 'yaml'
3
3
  require 'gpgme'
4
+ require 'charlock_holmes'
4
5
 
5
6
  require_relative '../schleuder'
6
7
  require 'schleuder/cli/subcommand_fix'
@@ -62,11 +63,14 @@ module Schleuder
62
63
  list.logger.notify_admin(msg, nil, I18n.t('check_keys'))
63
64
  end
64
65
  end
66
+ permission_notice
65
67
  end
66
68
 
67
69
  desc 'refresh_keys [list1@example.com]', "Refresh all keys of all list from the keyservers sequentially (one by one or on the passed list). (This is supposed to be run from cron weekly.)"
68
70
  def refresh_keys(list=nil)
71
+ GPGME::Ctx.send_notice_if_gpg_does_not_know_import_filter
69
72
  work_on_lists(:refresh_keys,list)
73
+ permission_notice
70
74
  end
71
75
 
72
76
  desc 'pin_keys [list1@example.com]', "Find keys for subscriptions without a pinned key and try to pin a certain key (one by one or based on the passed list)."
@@ -122,6 +126,7 @@ module Schleuder
122
126
  end
123
127
 
124
128
  say "Schleuder has been set up. You can now create a new list using `schleuder-cli`.\nWe hope you enjoy!"
129
+ permission_notice
125
130
  rescue => exc
126
131
  fatal exc.message
127
132
  end
@@ -257,6 +262,7 @@ Please notify the users and admins of this list of these changes.
257
262
  if messages.present?
258
263
  say messages.gsub(' // ', "\n")
259
264
  end
265
+ permission_notice
260
266
  rescue => exc
261
267
  fatal "#{exc}\n#{exc.backtrace.first}"
262
268
  end
@@ -315,11 +321,15 @@ Please notify the users and admins of this list of these changes.
315
321
  private
316
322
 
317
323
  def work_on_lists(subj, list=nil)
318
- selected_lists = if list.nil?
319
- List.all
324
+ if list.nil?
325
+ selected_lists = List.all
320
326
  else
321
- List.where(email: list)
327
+ selected_lists = List.where(email: list)
328
+ if selected_lists.blank?
329
+ error("No list with this address exists: #{list.inspect}")
330
+ end
322
331
  end
332
+
323
333
  selected_lists.each do |list|
324
334
  I18n.locale = list.language
325
335
  output = list.send(subj)
@@ -330,5 +340,25 @@ Please notify the users and admins of this list of these changes.
330
340
  end
331
341
  end
332
342
 
343
+ # Make this class exit with code 1 in case of an error. See <https://github.com/erikhuda/thor/issues/244>.
344
+ def self.exit_on_failure?
345
+ true
346
+ end
347
+
348
+ def permission_notice
349
+ if Process.euid == 0
350
+ dirs = [Conf.lists_dir, Conf.listlogs_dir]
351
+ if Conf.database['adapter'] == 'sqlite3'
352
+ dirs << Conf.database['database']
353
+ end
354
+ dirs_sentence = dirs.uniq.map { |dir| enquote(dir) }.to_sentence
355
+ say "Warning: this process was run as root -- please make sure that all files in #{dirs_sentence} have correct file system permissions for the user that is running both, schleuder from the MTA and `schleuder-api-daemon`."
356
+ end
357
+ end
358
+
359
+ def enquote(string)
360
+ "\`#{string}\`"
361
+ end
362
+
333
363
  end
334
364
  end