schleuder 3.2.2 → 3.3.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 +4 -4
- data/README.md +20 -10
- data/Rakefile +16 -8
- data/bin/schleuder +2 -1
- data/bin/schleuder-api-daemon +3 -2
- data/db/migrate/20180110203100_add_sig_enc_to_headers_to_meta_defaults.rb +30 -0
- data/db/schema.rb +2 -2
- data/etc/list-defaults.yml +4 -2
- data/etc/schleuder.yml +11 -0
- data/lib/schleuder-api-daemon.rb +9 -354
- data/lib/schleuder-api-daemon/helpers/schleuder-api-daemon-helper.rb +143 -0
- data/lib/schleuder-api-daemon/routes/key.rb +40 -0
- data/lib/schleuder-api-daemon/routes/list.rb +69 -0
- data/lib/schleuder-api-daemon/routes/status.rb +5 -0
- data/lib/schleuder-api-daemon/routes/subscription.rb +99 -0
- data/lib/schleuder-api-daemon/routes/version.rb +5 -0
- data/lib/schleuder.rb +2 -3
- data/lib/schleuder/cli.rb +24 -0
- data/lib/schleuder/cli/subcommand_fix.rb +1 -1
- data/lib/schleuder/conf.rb +7 -1
- data/lib/schleuder/errors/active_model_error.rb +2 -5
- data/lib/schleuder/errors/decryption_failed.rb +2 -7
- data/lib/schleuder/errors/key_adduid_failed.rb +1 -5
- data/lib/schleuder/errors/key_generation_failed.rb +1 -8
- data/lib/schleuder/errors/keyword_admin_only.rb +1 -5
- data/lib/schleuder/errors/list_not_found.rb +1 -5
- data/lib/schleuder/errors/listdir_problem.rb +2 -7
- data/lib/schleuder/errors/loading_list_settings_failed.rb +2 -5
- data/lib/schleuder/errors/message_empty.rb +1 -5
- data/lib/schleuder/errors/message_not_from_admin.rb +2 -5
- data/lib/schleuder/errors/message_sender_not_subscribed.rb +2 -5
- data/lib/schleuder/errors/message_too_big.rb +2 -5
- data/lib/schleuder/errors/message_unauthenticated.rb +1 -4
- data/lib/schleuder/errors/message_unencrypted.rb +2 -5
- data/lib/schleuder/errors/message_unsigned.rb +2 -5
- data/lib/schleuder/errors/too_many_keys.rb +1 -8
- data/lib/schleuder/filters/{request_filter.rb → post_decryption/10_request.rb} +0 -0
- data/lib/schleuder/filters/{max_message_size.rb → post_decryption/20_max_message_size.rb} +0 -0
- data/lib/schleuder/filters/{forward_filter.rb → post_decryption/30_forward_to_owner.rb} +0 -0
- data/lib/schleuder/filters/post_decryption/40_receive_admin_only.rb +10 -0
- data/lib/schleuder/filters/post_decryption/50_receive_authenticated_only.rb +10 -0
- data/lib/schleuder/filters/post_decryption/60_receive_signed_only.rb +10 -0
- data/lib/schleuder/filters/post_decryption/70_receive_encrypted_only.rb +10 -0
- data/lib/schleuder/filters/post_decryption/80_receive_from_subscribed_emailaddresses_only.rb +10 -0
- data/lib/schleuder/filters/{bounces_filter.rb → pre_decryption/10_forward_bounce_to_admins.rb} +0 -0
- data/lib/schleuder/filters/{forward_incoming.rb → pre_decryption/20_forward_all_incoming_to_admins.rb} +0 -0
- data/lib/schleuder/filters/{send_key_filter.rb → pre_decryption/30_send_key.rb} +0 -0
- data/lib/schleuder/filters/{hotmail_message_filter.rb → pre_decryption/40_fix_exchange_messages.rb} +5 -3
- data/lib/schleuder/filters/{strip_alternative_filter.rb → pre_decryption/50_strip_html_from_alternative.rb} +1 -1
- data/lib/schleuder/filters_runner.rb +41 -31
- data/lib/schleuder/gpgme/ctx.rb +1 -1
- data/lib/schleuder/gpgme/import_status.rb +13 -7
- data/lib/schleuder/gpgme/key.rb +4 -0
- data/lib/schleuder/list.rb +7 -4
- data/lib/schleuder/mail/encrypted_part.rb +14 -0
- data/lib/schleuder/mail/gpg.rb +15 -0
- data/lib/schleuder/mail/message.rb +70 -30
- data/lib/schleuder/plugins/key_management.rb +32 -7
- data/lib/schleuder/plugins/subscription_management.rb +70 -3
- data/lib/schleuder/runner.rb +19 -8
- data/lib/schleuder/subscription.rb +5 -9
- data/lib/schleuder/validators/fingerprint_validator.rb +1 -1
- data/lib/schleuder/version.rb +1 -1
- data/locales/de.yml +96 -8
- data/locales/en.yml +102 -10
- metadata +48 -27
- data/lib/schleuder/errors/file_not_found.rb +0 -14
- data/lib/schleuder/errors/invalid_listname.rb +0 -13
- data/lib/schleuder/errors/list_exists.rb +0 -13
- data/lib/schleuder/errors/unknown_list_option.rb +0 -14
- 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,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
|
data/lib/schleuder.rb
CHANGED
@@ -22,6 +22,8 @@ $:.unshift libdir
|
|
22
22
|
# Monkeypatches
|
23
23
|
require 'schleuder/mail/parts_list.rb'
|
24
24
|
require 'schleuder/mail/message.rb'
|
25
|
+
require 'schleuder/mail/gpg.rb'
|
26
|
+
require 'schleuder/mail/encrypted_part.rb'
|
25
27
|
require 'schleuder/gpgme/import_status.rb'
|
26
28
|
require 'schleuder/gpgme/key.rb'
|
27
29
|
require 'schleuder/gpgme/sub_key.rb'
|
@@ -46,9 +48,6 @@ Dir["#{libdir}/schleuder/plugins/*.rb"].each do |file|
|
|
46
48
|
require file
|
47
49
|
end
|
48
50
|
require 'schleuder/filters_runner'
|
49
|
-
Dir["#{libdir}/schleuder/filters/*.rb"].each do |file|
|
50
|
-
require file
|
51
|
-
end
|
52
51
|
Dir["#{libdir}/schleuder/validators/*.rb"].each do |file|
|
53
52
|
require file
|
54
53
|
end
|
data/lib/schleuder/cli.rb
CHANGED
@@ -62,11 +62,13 @@ module Schleuder
|
|
62
62
|
list.logger.notify_admin(msg, nil, I18n.t('check_keys'))
|
63
63
|
end
|
64
64
|
end
|
65
|
+
permission_notice
|
65
66
|
end
|
66
67
|
|
67
68
|
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
69
|
def refresh_keys(list=nil)
|
69
70
|
work_on_lists(:refresh_keys,list)
|
71
|
+
permission_notice
|
70
72
|
end
|
71
73
|
|
72
74
|
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 +124,7 @@ module Schleuder
|
|
122
124
|
end
|
123
125
|
|
124
126
|
say "Schleuder has been set up. You can now create a new list using `schleuder-cli`.\nWe hope you enjoy!"
|
127
|
+
permission_notice
|
125
128
|
rescue => exc
|
126
129
|
fatal exc.message
|
127
130
|
end
|
@@ -257,6 +260,7 @@ Please notify the users and admins of this list of these changes.
|
|
257
260
|
if messages.present?
|
258
261
|
say messages.gsub(' // ', "\n")
|
259
262
|
end
|
263
|
+
permission_notice
|
260
264
|
rescue => exc
|
261
265
|
fatal "#{exc}\n#{exc.backtrace.first}"
|
262
266
|
end
|
@@ -330,5 +334,25 @@ Please notify the users and admins of this list of these changes.
|
|
330
334
|
end
|
331
335
|
end
|
332
336
|
|
337
|
+
# Make this class exit with code 1 in case of an error. See <https://github.com/erikhuda/thor/issues/244>.
|
338
|
+
def self.exit_on_failure?
|
339
|
+
true
|
340
|
+
end
|
341
|
+
|
342
|
+
def permission_notice
|
343
|
+
if Process.euid == 0
|
344
|
+
dirs = [Conf.lists_dir, Conf.listlogs_dir]
|
345
|
+
if Conf.database['adapter'] == 'sqlite3'
|
346
|
+
dirs << Conf.database['database']
|
347
|
+
end
|
348
|
+
dirs_sentence = dirs.uniq.map { |dir| enquote(dir) }.to_sentence
|
349
|
+
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`."
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
def enquote(string)
|
354
|
+
"\`#{string}\`"
|
355
|
+
end
|
356
|
+
|
333
357
|
end
|
334
358
|
end
|
@@ -2,7 +2,7 @@ module Schleuder
|
|
2
2
|
module SubcommandFix
|
3
3
|
|
4
4
|
# Fixing a bug in Thor where the actual subcommand wouldn't show up
|
5
|
-
# with some
|
5
|
+
# with some invocations of the help-output.
|
6
6
|
def banner(task, namespace = true, subcommand = true)
|
7
7
|
"#{basename} #{task.formatted_usage(self, true, subcommand).split(':').join(' ')}"
|
8
8
|
end
|