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