schleuder-cli 0.2.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ac4d12688077b4652cfd1a4022bbb09446ad196cf8de8f030a5cd7d751715080
4
+ data.tar.gz: 3e6a4e4cf966c2424a1259020c2c58da392c770e49194c99aae8f3b694030b22
5
+ SHA512:
6
+ metadata.gz: 6739403ab27f9bb7b065c5595f1726270786d144488e1efc40615a1722ad584cdf5ce535f8ad148fff9625702ca38a55d3139340fcd7c167133d80bbf71bf446
7
+ data.tar.gz: bc85f015ecc4d1c58ae91d8a64147b38f5f62c143f4a71040f8cdd22d38b58b543fd37a65191de55c90dbbbd652d60e978ba71efd330bbf864b092758693bdc7
data/README.md ADDED
@@ -0,0 +1,111 @@
1
+ Schleuder-cli
2
+ ==============
3
+
4
+ A command line tool to create and manage schleuder-lists.
5
+
6
+ Schleuder-cli enables creating, configuring, and deleting lists, subscriptions, keys, etc. It uses the Schleuder API, provided by schleuder-api-daemon (part of Schleuder).
7
+
8
+ Authentication and TLS-verification are mandatory. You need an API-key and the fingerprint of the TLS-certificate of the Schleuder API, respectively. Both should be provided by the API operators.
9
+
10
+ schleuder-cli does *not* authorize access. Only people who are supposed to have full access to all lists should be allowed to use it on/with your server.
11
+
12
+
13
+ Requirements
14
+ ------------
15
+ * ruby >=2.7
16
+ * thor
17
+
18
+ Installation
19
+ ------------
20
+
21
+ 1. Download [the gem](https://0xacab.org/schleuder/schleuder-cli/raw/main/gems/schleuder-cli-0.2.0.gem) and [the OpenPGP-signature](https://0xacab.org/schleuder/schleuder-cli/raw/main/gems/schleuder-cli-0.2.0.gem.sig) and verify:
22
+ ```
23
+ gpg --recv-key 0xB3D190D5235C74E1907EACFE898F2C91E2E6E1F3
24
+ gpg --verify schleuder-cli-0.2.0.gem.sig
25
+ ```
26
+
27
+ 2. If all went well install the gem:
28
+ ```
29
+ gem install schleuder-cli-0.2.0.gem
30
+ ```
31
+
32
+ You probably want to install [schleuder](https://0xacab.org/schleuder/schleuder), too. Without schleuder, this software is very useless.
33
+
34
+ Configuration
35
+ -------------
36
+
37
+ SchleuderCli reads its settings from a file that it by default expects at `$HOME/.schleuder-cli/schleuder-cli.yml`. To make it read a different file set the environment variable `SCHLEUDER_CLI_CONFIG` to the path to your file. E.g.:
38
+
39
+ SCHLEUDER_CLI_CONFIG=/usr/local/etc/schleuder-cli.yml schleuder-cli ...
40
+
41
+ The configuration file specifies how to connect to the Schleuder API. If it doesn't exist, it will be filled with the default settings.
42
+
43
+ The default settings will work out of the box with the default settings of Schleuder if both are running on the same host.
44
+
45
+ **Options**
46
+
47
+ These are the configuration file options and their default values:
48
+
49
+ * `host`: The hostname (or IP-address) to connect to. Default: `localhost`.
50
+ * `port`: The port to connect to. Default: `4443`.
51
+ * `tls_fingerprint`: TLS-fingerprint of the Schleuder API. To be fetched from the API operators. Default: empty.
52
+ * `api_key`: Key to authenticate with against the Schleuder API. To be fetched from the API operators. Default: empty.
53
+
54
+
55
+ Usage
56
+ -----
57
+ See `schleuder-cli help`.
58
+
59
+ E.g.:
60
+
61
+ Commands:
62
+ schleuder-cli help [COMMAND] # Describe available commands or one specific command
63
+ schleuder-cli keys ... # Manage OpenPGP-keys
64
+ schleuder-cli lists ... # Create and configure lists
65
+ schleuder-cli subscriptions ... # Create and manage subscriptions
66
+ schleuder-cli version # Show version of schleuder-cli or Schleuder.
67
+
68
+
69
+
70
+ Contributing
71
+ ------------
72
+
73
+ Please see [CONTRIBUTING.md](CONTRIBUTING.md).
74
+
75
+
76
+ Mission statement
77
+ -----------------
78
+
79
+ We summarized our motivation in [MISSION_STATEMENT.md](MISSION_STATEMENT.md).
80
+
81
+
82
+ Code of Conduct
83
+ ---------------
84
+
85
+ We adopted a code of conduct. Please read [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).
86
+
87
+
88
+ License
89
+ -------
90
+
91
+ GNU GPL 3.0. Please see [LICENSE.txt](LICENSE.txt).
92
+
93
+ Tests
94
+ -----
95
+ To run the integration tests, you must have a schleuder-api-daemon running locally (in test mode, preferably). From the root directory of the schleuder-repo execute this:
96
+
97
+ ```
98
+ SCHLEUDER_ENV=test SCHLEUDER_CONFIG=spec/schleuder.yml SCHLEUDER_LIST_DEFAULTS=etc/list-defaults.yml bundle exec ./bin/schleuder-api-daemon
99
+ ```
100
+
101
+ Then you can run the tests:
102
+
103
+ ```
104
+ bundle exec rspec
105
+ ```
106
+
107
+
108
+ Alternative Download
109
+ --------------------
110
+
111
+ Alternatively to the gem-files you can download the latest release as [a tarball](https://0xacab.org/schleuder/schleuder-cli/raw/main/gems/schleuder-cli-0.2.0.tar.gz) and [its OpenPGP-signature](https://0xacab.org/schleuder/schleuder-cli/raw/main/gems/schleuder-cli-0.2.0.tar.gz.sig).
data/bin/schleuder-cli ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/schleuder-cli'
4
+
5
+ trap("INT") { exit 1 }
6
+
7
+ SchleuderCli::Base.start
@@ -0,0 +1,37 @@
1
+ module SchleuderCli
2
+ class Base < Thor
3
+ include Helper
4
+
5
+ register(Subscriptions,
6
+ 'subscriptions',
7
+ 'subscriptions ...',
8
+ 'Create and manage subscriptions')
9
+
10
+ register(Keys,
11
+ 'keys',
12
+ 'keys ...',
13
+ 'Manage OpenPGP-keys')
14
+
15
+ register(Lists,
16
+ 'lists',
17
+ 'lists ...',
18
+ 'Create and configure lists')
19
+
20
+ map '-v' => :version
21
+ desc 'version', "Show version of schleuder-cli or Schleuder."
22
+ method_option :remote, aliases: '-r', banner: '', desc: "Show version of Schleuder at the server."
23
+ def version
24
+ if options.remote
25
+ say get('/version.json')['version']
26
+ else
27
+ say VERSION
28
+ end
29
+ end
30
+
31
+ # This tells Thor to go with its new behaviour since v1.0.0, which is
32
+ # exiting in case of failures.
33
+ def self.exit_on_failure?
34
+ true
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,103 @@
1
+ module SchleuderCli
2
+ class Conf
3
+ include Singleton
4
+
5
+ FINGERPRINT_REGEXP = /\A0?x?[a-f0-9]{32,}\z/i
6
+
7
+ DEFAULTS = {
8
+ 'host' => 'localhost',
9
+ 'port' => 4443,
10
+ 'tls_fingerprint' => nil,
11
+ 'api_key' => nil
12
+ }
13
+
14
+ def config
15
+ @config ||= load_config(ENV['SCHLEUDER_CLI_CONFIG'])
16
+ end
17
+
18
+ def self.host
19
+ instance.config['host'].to_s
20
+ end
21
+
22
+ def self.port
23
+ instance.config['port'].to_s
24
+ end
25
+
26
+ def self.tls_fingerprint
27
+ instance.config['tls_fingerprint'].to_s
28
+ end
29
+
30
+ def self.api_key
31
+ instance.config['api_key'].to_s
32
+ end
33
+
34
+ def self.remote_cert_file
35
+ path = instance.config['remote_cert_file'].to_s
36
+ if path.empty?
37
+ fatal "Error: remote_cert_file is empty, can't verify remote server without it (in #{ENV['SCHLEUDER_CLI_CONFIG']})."
38
+ end
39
+ file = Pathname.new(path).expand_path
40
+ if ! file.readable?
41
+ fatal "Error: remote_cert_file is set to a not readable file (in #{ENV['SCHLEUDER_CLI_CONFIG']})."
42
+ end
43
+ file.to_s
44
+ end
45
+
46
+
47
+ private
48
+
49
+
50
+ def load_config(filename)
51
+ file = Pathname.new(filename)
52
+ if file.exist?
53
+ load_config_file(file)
54
+ else
55
+ write_defaults_to_config_file(file)
56
+ end
57
+ end
58
+
59
+
60
+ def load_config_file(file)
61
+ if ! file.readable?
62
+ fatal "Error: #{file} is not readable."
63
+ end
64
+ yaml = YAML.load(file.read)
65
+ if ! yaml.is_a?(Hash)
66
+ fatal "Error: #{file} cannot be parsed correctly, please fix it. (To get a new default configuration file remove the current one and run again.)"
67
+ end
68
+ # Test for old, nested config
69
+ if ! yaml['api'].nil?
70
+ self.class.fatal "Your configuration file is outdated, please fix it: Remove the first level key called 'api', and move the keys below it to the first level. It should look like this (possibly with different values):\n\n#{defaults_as_yaml}"
71
+ end
72
+ yaml
73
+ end
74
+
75
+ def write_defaults_to_config_file(file)
76
+ dir = file.dirname
77
+ if dir.dirname.writable?
78
+ dir.mkdir(0700)
79
+ else
80
+ fatal "Error: '#{dir}' is not writable, cannot write default config to '#{file}'."
81
+ end
82
+ file.open('w', 0600) do |fh|
83
+ fh.puts defaults_as_yaml
84
+ end
85
+ puts "NOTE: A default configuration file has been written to #{file}."
86
+ DEFAULTS
87
+ end
88
+
89
+ def fatal(msg)
90
+ self.class.fatal(msg)
91
+ end
92
+
93
+ def self.fatal(msg)
94
+ $stderr.puts msg
95
+ exit 1
96
+ end
97
+
98
+ def defaults_as_yaml
99
+ # Strip the document starting dashes. We don't need them, they only confuse people.
100
+ DEFAULTS.to_yaml.lines[1..-1].join
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,279 @@
1
+ module SchleuderCli
2
+ module Helper
3
+
4
+ def api
5
+ @http ||= begin
6
+ http = Net::HTTP.new(Conf.host, Conf.port)
7
+ http.use_ssl = true
8
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
9
+ http.verify_callback = lambda { |*a| ssl_verify_callback(*a) }
10
+ #http.ca_file = Conf.remote_cert_file
11
+ http
12
+ end
13
+ end
14
+
15
+ def url(*args)
16
+ if args.last.is_a?(Hash)
17
+ params = args.pop
18
+ end
19
+ u = "/#{args.join('/')}.json"
20
+ if params
21
+ paramstring = params.map do |k,v|
22
+ "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}"
23
+ end.join('&')
24
+ u << "?#{paramstring}"
25
+ end
26
+ u
27
+ end
28
+
29
+ def get(url)
30
+ req = Net::HTTP::Get.new(url)
31
+ request(req)
32
+ end
33
+
34
+ def post(url, payload, &block)
35
+ req = Net::HTTP::Post.new(url)
36
+ req.body = payload.to_json
37
+ req.content_type = 'application/json'
38
+ request(req, &block)
39
+ end
40
+
41
+ def patch(url, payload)
42
+ req = Net::HTTP::Patch.new(url)
43
+ req.body = payload.to_json
44
+ req.content_type = 'application/json'
45
+ request(req)
46
+ end
47
+
48
+ def delete_req(url)
49
+ req = Net::HTTP::Delete.new(url)
50
+ request(req)
51
+ end
52
+
53
+ def debug(msg)
54
+ if $DEBUG
55
+ $stderr.puts "SchleuderCli: #{msg}"
56
+ end
57
+ end
58
+
59
+ def request(req, &block)
60
+ test_mandatory_config
61
+ req.basic_auth 'schleuder', Conf.api_key
62
+ debug "Request to API: #{req.inspect}"
63
+ debug "API request path: #{req.path.inspect}"
64
+ debug "API request headers: #{req.to_hash.inspect}"
65
+ debug "API request body: #{req.body.inspect}"
66
+ if block
67
+ resp = block.call(api, req)
68
+ else
69
+ resp = api.request(req)
70
+ end
71
+ debug "Response from API: #{resp.inspect}"
72
+ debug "API response headers: #{resp.to_hash.inspect}"
73
+ debug "API response body: #{resp.body}"
74
+ handle_response_errors(resp)
75
+ handle_response_messages(resp)
76
+ parse_body(resp.body)
77
+ rescue Errno::ECONNREFUSED
78
+ fatal "Error: Cannot connect to Schleuder API at #{api.address}:#{api.port}, please check if it's running."
79
+ rescue Net::ReadTimeout => exc
80
+ error "Error: Timeout while waiting for server."
81
+ # If you set the timeout explicitly you'll have the exception passed on
82
+ # in order to react explicitly, too.
83
+ if timeout.to_i > 0
84
+ raise exc
85
+ end
86
+ rescue OpenSSL::SSL::SSLError => exc
87
+ case exc.message
88
+ when /certificate verify failed/
89
+ fatal exc.message.split('state=').last.capitalize
90
+ else
91
+ fatal exc.message
92
+ end
93
+ rescue => exc
94
+ fatal exc.message
95
+ end
96
+
97
+ def handle_response_errors(response)
98
+ case response.code.to_i
99
+ when 404
100
+ fatal "Schleuder could not find the requested resource, please check your input."
101
+ when 401
102
+ fatal "Authentication failed, please check your API key."
103
+ when 400
104
+ if body = parse_body(response.body)
105
+ fatal "Error: #{body['errors']}"
106
+ else
107
+ fatal "Unknown error"
108
+ end
109
+ when 500
110
+ fatal 'Server error, try again later'
111
+ end
112
+ end
113
+
114
+ def handle_response_messages(response)
115
+ messages = response.header['X-Messages'].to_s.split(' // ')
116
+ if ! messages.empty?
117
+ say "\n"
118
+ messages.each do |message|
119
+ say " ! #{message}"
120
+ end
121
+ say "\n"
122
+ end
123
+ end
124
+
125
+ def parse_body(body)
126
+ if body.to_s.empty?
127
+ nil
128
+ else
129
+ JSON.parse(body)
130
+ end
131
+ end
132
+
133
+ def fatal(msgs)
134
+ Array(msgs).each do |msg|
135
+ error msg
136
+ end
137
+ exit 1
138
+ end
139
+
140
+ def check_option_presence(hash, option)
141
+ if ! hash.has_key?(option)
142
+ fatal "No such option"
143
+ end
144
+ end
145
+
146
+ def show_value(value)
147
+ case value
148
+ when Array, Hash
149
+ say value.inspect
150
+ else
151
+ say value
152
+ end
153
+ exit
154
+ end
155
+
156
+ def ok
157
+ say "Ok."
158
+ exit 0
159
+ end
160
+
161
+ def say_key_import_stati(import_stati)
162
+ Array(import_stati).each do |import_status|
163
+ say "Key #{import_status['fpr']}: #{import_status['action']}"
164
+ end
165
+ end
166
+
167
+ def say_key_import_result(keys)
168
+ keys.each do |key|
169
+ if key['import_action'] == 'error'
170
+ say "Unexpected error while importing key #{key['fingerprint']}"
171
+ else
172
+ say "#{key['import_action'].capitalize}: #{key['summary']}"
173
+ end
174
+ end
175
+ end
176
+
177
+ def import_key_and_find_fingerprint(listname, keyfile)
178
+ return nil if keyfile.to_s.empty?
179
+
180
+ fingerprints = import_key(listname, keyfile)
181
+
182
+ if fingerprints.size == 1
183
+ fingerprints.first
184
+ else
185
+ say "#{keyfile} contains more than one key, cannot determine which fingerprint to use. Please set it manually!"
186
+ nil
187
+ end
188
+ end
189
+
190
+ def import_key(listname, keyfile)
191
+ test_file(keyfile)
192
+ keydata = File.binread(keyfile)
193
+ if ! keydata.match('BEGIN PGP')
194
+ keydata = Base64.encode64(keydata)
195
+ end
196
+ result = post(url(:keys, {list_id: listname}), {keymaterial: keydata})
197
+ if result.has_key?("keys")
198
+ # API is v5 or later.
199
+ num = result["keys"].size
200
+ if num == 0
201
+ say "#{keyfile} did not contain any keys!"
202
+ return nil
203
+ end
204
+ say_key_import_result(result["keys"])
205
+ result["keys"].map { |key| key["fingerprint"] }
206
+ else
207
+ # API is v4 or earlier.
208
+ num = result["considered"]
209
+ if num == 0
210
+ say "#{keyfile} did not contain any keys!"
211
+ return nil
212
+ end
213
+ say_key_import_stati(result['imports'])
214
+ result['imports'].map { |import| import['fpr'] }
215
+ end
216
+ end
217
+
218
+ def test_file(filename)
219
+ if ! File.readable?(filename)
220
+ fatal "File not found: #{filename}"
221
+ end
222
+ end
223
+
224
+ def subscribe(listname, email, fingerprint, adminflag=false)
225
+ res = post(url(:subscriptions, {list_id: listname}), {
226
+ email: email,
227
+ fingerprint: fingerprint.to_s,
228
+ admin: adminflag.to_s
229
+ })
230
+ if res && res['errors']
231
+ print "Error subscribing #{email}: "
232
+ show_errors(res['errors'])
233
+ end
234
+ text = "#{email} subscribed to #{listname} "
235
+ if fingerprint
236
+ text << "with fingerprint #{fingerprint}."
237
+ else
238
+ text << "without setting a fingerprint."
239
+ end
240
+ say text
241
+ end
242
+
243
+ def show_errors(errors)
244
+ Array(errors).each do |k,v|
245
+ if v
246
+ say "#{k.capitalize} #{v.join(', ')}"
247
+ else
248
+ say k
249
+ end
250
+ end
251
+ exit 1
252
+ end
253
+
254
+ def ssl_verify_callback(pre_ok, cert_store)
255
+ cert = cert_store.chain[0]
256
+ # Only really compare if we're looking at the last cert in the chain.
257
+ if cert.to_der != cert_store.current_cert.to_der
258
+ return true
259
+ end
260
+ fingerprint = OpenSSL::Digest::SHA256.new(cert.to_der).to_s
261
+ fingerprint == Conf.tls_fingerprint
262
+ end
263
+
264
+ def test_mandatory_config
265
+ if Conf.host.empty?
266
+ fatal "Error: 'host' is empty, can't connect (in #{ENV['SCHLEUDER_CLI_CONFIG']})."
267
+ end
268
+ if Conf.port.empty?
269
+ fatal "Error: 'port' is empty, can't connect (in #{ENV['SCHLEUDER_CLI_CONFIG']})."
270
+ end
271
+ if Conf.tls_fingerprint.empty?
272
+ fatal "Error: 'tls_fingerprint' is empty but required (in #{ENV['SCHLEUDER_CLI_CONFIG']})."
273
+ end
274
+ if Conf.api_key.empty?
275
+ fatal "Error: 'api_key' is empty but required (in #{ENV['SCHLEUDER_CLI_CONFIG']})."
276
+ end
277
+ end
278
+ end
279
+ end
@@ -0,0 +1,39 @@
1
+ module SchleuderCli
2
+ class Keys < Thor
3
+ extend SubcommandFix
4
+ include Helper
5
+
6
+ desc 'import <list@hostname> </path/to/keyfile>', "Import a key into a list's keyring."
7
+ def import(listname, keyfile)
8
+ import_key(listname, keyfile)
9
+ end
10
+
11
+ desc 'export <list@hostname> <fingerprint>', "Get the key exported from the list's keyring."
12
+ def export(listname, fingerprint)
13
+ if hash = get(url(:keys, fingerprint, {list_id: listname}))
14
+ say hash['ascii']
15
+ end
16
+ end
17
+
18
+ desc 'list <list@hostname>', "List keys available to list."
19
+ def list(listname)
20
+ if keys = get(url(:keys, {list_id: listname}))
21
+ keys.each do |hash|
22
+ say "#{hash['fingerprint']} #{hash['email']}"
23
+ end
24
+ end
25
+ end
26
+
27
+ desc 'delete <list@hostname> <fingerprint>', "Delete key from list."
28
+ def delete(listname, fingerprint)
29
+ delete_req(url(:keys, fingerprint, {list_id: listname})) || ok
30
+ end
31
+
32
+ desc 'check <list@hostname>', "Check for expiring or unusable keys."
33
+ def check(listname)
34
+ resp = get(url(:keys, :check_keys, {list_id: listname}))
35
+ say resp['result']
36
+ end
37
+ end
38
+ end
39
+
@@ -0,0 +1,80 @@
1
+ module SchleuderCli
2
+ class Lists < Thor
3
+ include Helper
4
+ extend SubcommandFix
5
+
6
+ desc 'list', 'List all known lists.'
7
+ def list
8
+ get(url(:lists)).each do |list|
9
+ say list['email']
10
+ end
11
+ end
12
+
13
+ desc 'new <list@hostname> <adminaddress> [</path/to/publickeys.asc>]', 'Create a new schleuder list.'
14
+ def new(listname, adminaddress, keyfile=nil)
15
+ res = post(url(:lists), {email: listname}) do |http, request|
16
+ test_file(keyfile) if keyfile
17
+ http.read_timeout = 120
18
+ begin
19
+ http.request(request)
20
+ rescue Net::ReadTimeout
21
+ fatal "Error: Timeout while waiting for Schleuder API!\n\nThe Schleuder API didn't answer in time. This happens sometimes when creating a new list because generating a new OpenPGP key-pair requires a lot of randomness — if that's not given, GnuPG just waits until there's more.\nUnfortunately we can't know what the issue really is. But chances are that the list was created just fine and in a few minutes the new OpenPGP key-pair will have been generated, too. So please wait a little while and then have a look if your list was created."
22
+ end
23
+ end
24
+
25
+ if res && res['errors']
26
+ show_errors(res['errors'])
27
+ else
28
+ say "List #{listname} successfully created! Don't forget to hook it into your MTA."
29
+ end
30
+
31
+ fingerprint = import_key_and_find_fingerprint(listname, keyfile)
32
+ subscribe(listname, adminaddress, fingerprint, true)
33
+ end
34
+
35
+ desc 'list-options', 'List available options for lists.'
36
+ def list_options()
37
+ say get(url(:lists, 'configurable_attributes')).join("\n")
38
+ end
39
+
40
+ desc 'show <list@hostname> <option>', 'Get the value of a list-option.'
41
+ def show(listname, option)
42
+ list = get(url(:lists, listname))
43
+ check_option_presence(list, option)
44
+ show_value(list[option])
45
+ end
46
+
47
+ desc 'set <list@hostname> <option> <value>', 'Get or set the value of a list-option.'
48
+ def set(listname, option, value)
49
+ if option.start_with?('headers_to_meta', 'keywords_admin_notify', 'keywords_admin_only', 'bounces_drop_on_headers')
50
+ begin
51
+ value = JSON.parse(value)
52
+ rescue => exc
53
+ error "Parsing value as JSON failed: #{exc.message}"
54
+ fatal "Input must be valid JSON."
55
+ end
56
+ end
57
+ patch(url(:lists, listname), {option => value})
58
+ show_value(value)
59
+ end
60
+
61
+ desc 'delete <list@hostname> [--YES]', "Delete the list. To skip confirmation use --YES"
62
+ def delete(listname, dontask=nil)
63
+ if dontask != '--YES'
64
+ answer = ask "Really delete list including all its data? [yN] "
65
+ if answer.downcase != 'y'
66
+ say "Not deleted."
67
+ exit 0
68
+ end
69
+ end
70
+ say delete_req(url(:lists, listname))
71
+ end
72
+
73
+ desc 'send-list-key-to-subscriptions <list@hostname>', "Triggers the sending of the list's public key to all subscriptions."
74
+ def send_list_key_to_subscriptions(listname)
75
+ response = post(url(:lists, :send_list_key_to_subscriptions, {list_id: listname}), :irrelevant)
76
+ say response['result']
77
+ end
78
+
79
+ end
80
+ end
@@ -0,0 +1,12 @@
1
+ # Monkey-patch OpenSSL to skip checking the hostname of the remote certificate.
2
+ # We need to enable VERIFY_PEER in order to get hands on the remote certificate
3
+ # to check the fingerprint. But we don't care for the matching of the
4
+ # hostnames. Unfortunately this patch is apparently the only way to achieve
5
+ # that.
6
+ module OpenSSL
7
+ module SSL
8
+ def self.verify_certificate_identity(peer_cert, hostname)
9
+ true
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ module SchleuderCli
2
+ module SubcommandFix
3
+
4
+ # Fixing a bug in Thor where the actual subcommand wouldn't show up
5
+ # with some invocations of the help-output.
6
+ def banner(task, namespace = true, subcommand = true)
7
+ "#{basename} #{task.formatted_usage(self, true, subcommand).split(':').join(' ')}"
8
+ end
9
+
10
+ end
11
+ end