schleuder-cli 0.2.0

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