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 +7 -0
- data/README.md +111 -0
- data/bin/schleuder-cli +7 -0
- data/lib/schleuder-cli/base.rb +37 -0
- data/lib/schleuder-cli/conf.rb +103 -0
- data/lib/schleuder-cli/helper.rb +279 -0
- data/lib/schleuder-cli/keys.rb +39 -0
- data/lib/schleuder-cli/lists.rb +80 -0
- data/lib/schleuder-cli/openssl_ssl_patch.rb +12 -0
- data/lib/schleuder-cli/subcommand_fix.rb +11 -0
- data/lib/schleuder-cli/subscriptions.rb +56 -0
- data/lib/schleuder-cli/version.rb +3 -0
- data/lib/schleuder-cli.rb +25 -0
- data/man/schleuder-cli.8 +97 -0
- data/man/schleuder-cli.8.ron +80 -0
- data/spec/example_key.txt +52 -0
- data/spec/integration/keys_spec.rb +34 -0
- data/spec/integration/lists_spec.rb +55 -0
- data/spec/schleuder-cli/cli_spec.rb +31 -0
- data/spec/schleuder-cli.yml +4 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/two_keys.asc +76 -0
- metadata +114 -0
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,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
|