schleuder 3.2.2 → 3.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|