certmeister-rack 0.3.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
+ SHA1:
3
+ metadata.gz: 50f3f6929f52a9500d3e1a1f1f983ed7de5a3fd7
4
+ data.tar.gz: c5b34e536ce55388c1937c8f25eef3bbd79f06c1
5
+ SHA512:
6
+ metadata.gz: cd6d0dea0018201e2fb0e4e26a5ce1aa41f609ca6208d42e411ee3fd670cfa0a8cda1fd1fb820aa2ae02eae2951be134ecd47b8bd047a90a8385b04c1a0ebd22
7
+ data.tar.gz: a0b1d591d24dbf43d9a3dea978f7db4cabf1211130bf5ea14e9ad6e915570eb526c98a489da90448f2722e229bd83b5422734d5ca5644f29e9509110c7aa70b8
@@ -0,0 +1,107 @@
1
+ require 'rack/request'
2
+ require 'certmeister/rack/symbolic_hash_accessor'
3
+
4
+ module Certmeister
5
+
6
+ module Rack
7
+
8
+ class App
9
+
10
+ def initialize(ca)
11
+ @ca = ca
12
+ end
13
+
14
+ def call(env)
15
+ req = ::Rack::Request.new(env)
16
+ if req.path_info == '/ping'
17
+ if req.request_method == 'GET'
18
+ ok('PONG')
19
+ else
20
+ method_not_allowed
21
+ end
22
+ elsif req.path_info =~ %r{^/certificate/(.+)}
23
+ req.params['cn'] = $1
24
+ req.params['ip'] = req.ip
25
+ case req.request_method
26
+ when 'POST' then sign_action(req)
27
+ when 'GET' then fetch_action(req)
28
+ when 'DELETE' then remove_action(req)
29
+ else method_not_allowed
30
+ end
31
+ else
32
+ not_implemented
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def sign_action(req)
39
+ response = @ca.sign(SymbolicHashAccessor.new(req.params))
40
+ if response.hit?
41
+ redirect(req.path)
42
+ elsif response.denied?
43
+ forbidden(response.error)
44
+ else
45
+ internal_server_error(response.error)
46
+ end
47
+ end
48
+
49
+ def fetch_action(req)
50
+ response = @ca.fetch(SymbolicHashAccessor.new(req.params))
51
+ if response.hit?
52
+ ok(response.pem, 'application/x-pem-file')
53
+ elsif response.miss?
54
+ not_found
55
+ elsif response.denied?
56
+ forbidden(response.error)
57
+ else
58
+ internal_server_error(response.error)
59
+ end
60
+ end
61
+
62
+ def remove_action(req)
63
+ response = @ca.remove(SymbolicHashAccessor.new(req.params))
64
+ if response.hit?
65
+ ok
66
+ elsif response.miss?
67
+ not_found
68
+ elsif response.denied?
69
+ forbidden(response.error)
70
+ else
71
+ internal_server_error(response.error)
72
+ end
73
+ end
74
+
75
+ def ok(body = '200 OK', content_type = 'text/plain')
76
+ [200, {'Content-Type' => content_type}, [body]]
77
+ end
78
+
79
+ def redirect(location)
80
+ [303, {'Content-Type' => 'text/plain', 'Location' => location}, ['303 See Other']]
81
+ end
82
+
83
+ def not_found
84
+ [404, {'Content-Type' => 'text/plain'}, ["404 Not Found"]]
85
+ end
86
+
87
+ def forbidden(reason)
88
+ [403, {'Content-Type' => 'text/plain'}, ["403 Forbidden (#{reason})"]]
89
+ end
90
+
91
+ def method_not_allowed
92
+ [405, {'Content-Type' => 'text/plain'}, ['405 Method Not Allowed']]
93
+ end
94
+
95
+ def internal_server_error(reason)
96
+ [500, {'Content-Type' => 'text/plain'}, ["500 Internal Server Error (#{reason})"]]
97
+ end
98
+
99
+ def not_implemented
100
+ [501, {'Content-Type' => 'text/plain'}, ['501 Not Implemented']]
101
+ end
102
+
103
+ end
104
+
105
+ end
106
+
107
+ end
@@ -0,0 +1,42 @@
1
+ require 'delegate'
2
+
3
+ # Light-weight alternative to active_support's HashWithIndifferentAccess.
4
+ #
5
+ # The rack application must not symbolize params from the client, because
6
+ # symbols cannot be garbage-collected, and so provide a memory starvation
7
+ # attack.
8
+ #
9
+ # So instead we wrap the parameters with a symbolic accessor, so that
10
+ # Certmeister::Base and Certmeister::Policy::* can refer to parameters
11
+ # symbolically.
12
+
13
+ module Certmeister
14
+
15
+ module Rack
16
+
17
+ class SymbolicHashAccessor < SimpleDelegator
18
+
19
+ def initialize(hash)
20
+ @hash = hash
21
+ super(@hash)
22
+ end
23
+
24
+ def [](key)
25
+ @hash[key.to_s]
26
+ end
27
+
28
+ def fetch(*args)
29
+ args[0] = args[0].to_s
30
+ @hash.fetch(*args)
31
+ end
32
+
33
+ def has_key?(key)
34
+ @hash.has_key?(key.to_s)
35
+ end
36
+ alias_method :include?, :has_key?
37
+
38
+ end
39
+
40
+ end
41
+
42
+ end
@@ -0,0 +1,373 @@
1
+ require 'spec_helper'
2
+ require 'rack/test'
3
+
4
+ require 'certmeister'
5
+ require 'certmeister/rack/app'
6
+
7
+ describe Certmeister::Rack::App do
8
+
9
+ include Rack::Test::Methods
10
+
11
+ let(:response) { double(Certmeister::Response).as_null_object }
12
+ let(:ca) { double(Certmeister::Base, sign: response, fetch: response, remove: response) }
13
+ let(:app) { Certmeister::Rack::App.new(ca) }
14
+
15
+ it "GET /ping always PONGs (although one day we want a health check)" do
16
+ get "/ping"
17
+ expect(last_response.status).to eql 200
18
+ expect(last_response.headers['Content-Type']).to eql "text/plain"
19
+ expect(last_response.body).to eql "PONG"
20
+ end
21
+
22
+ it "/ping returns 405 Method Not Allowed for other HTTP verbs" do
23
+ head "/ping"
24
+ expect(last_response.status).to eql 405
25
+ expect(last_response.headers['Content-Type']).to eql "text/plain"
26
+ expect(last_response.body).to eql "405 Method Not Allowed"
27
+ end
28
+
29
+ it "returns 501 Not Implemented for an unknown URI" do
30
+ get "/nonexistent"
31
+ expect(last_response.status).to eql 501
32
+ expect(last_response.headers['Content-Type']).to eql "text/plain"
33
+ expect(last_response.body).to eql "501 Not Implemented"
34
+ end
35
+
36
+ describe "POST /certificate/:cn" do
37
+
38
+ context "parameter handling" do
39
+
40
+ let (:response) { Certmeister::Response.hit("...crt...") }
41
+ before(:each) do
42
+ post "/certificate/axl.starjuice.net", {"csr" => "...csr..."}, {"REMOTE_ADDR" => "192.168.1.2"}
43
+ end
44
+
45
+ it "copies the cn into the params" do
46
+ expect(ca).to have_received(:sign).with hash_including(cn: "axl.starjuice.net")
47
+ end
48
+
49
+ it "copies the ip into the params" do
50
+ expect(ca).to have_received(:sign).with hash_including(ip: "192.168.1.2")
51
+ end
52
+
53
+ it "passes the form params into the CA" do
54
+ expect(ca).to have_received(:sign).with hash_including(csr: "...csr...")
55
+ end
56
+
57
+ end
58
+
59
+ context "on hit" do
60
+
61
+ let (:response) { Certmeister::Response.hit("...crt...") }
62
+ before(:each) do
63
+ post "/certificate/axl.starjuice.net", {"csr" => "...csr..."}
64
+ end
65
+
66
+ it "returns 303 See Other" do
67
+ expect(last_response.status).to eql 303
68
+ end
69
+
70
+ it "offers the same resource URI as Location" do
71
+ expect(last_response.headers['Location']).to eql "/certificate/axl.starjuice.net"
72
+ end
73
+
74
+ it "describes the HTTP status in the body" do
75
+ expect(last_response.body).to eql '303 See Other'
76
+ end
77
+
78
+ it "describes the body as text/plain" do
79
+ expect(last_response.headers['Content-Type']).to eql 'text/plain'
80
+ end
81
+
82
+ end
83
+
84
+ context "on denied" do
85
+
86
+ let (:response) { Certmeister::Response.denied("not enough mojo") }
87
+ before(:each) do
88
+ post "/certificate/axl.starjuice.net", {"csr" => "...csr..."}
89
+ end
90
+
91
+ it "returns 403 Forbidden" do
92
+ expect(last_response.status).to eql 403
93
+ end
94
+
95
+ it "describes the HTTP status in the body" do
96
+ expect(last_response.body).to eql '403 Forbidden (not enough mojo)'
97
+ end
98
+
99
+ it "describes the body as text/plain" do
100
+ expect(last_response.headers['Content-Type']).to eql 'text/plain'
101
+ end
102
+
103
+ end
104
+
105
+ context "on error" do
106
+
107
+ let (:response) { Certmeister::Response.error("all is lost") }
108
+
109
+ before(:each) do
110
+ post "/certificate/axl.starjuice.net", {"csr" => "...csr..."}
111
+ end
112
+
113
+ it "returns 500 Internal Server Error" do
114
+ expect(last_response.status).to eql 500
115
+ end
116
+
117
+ it "describes the HTTP status in the body" do
118
+ expect(last_response.body).to eql "500 Internal Server Error (all is lost)"
119
+ end
120
+
121
+ it "describes the body as text/plain" do
122
+ expect(last_response.headers['Content-Type']).to eql 'text/plain'
123
+ end
124
+
125
+ end
126
+
127
+ end
128
+
129
+ describe "GET /certificate/:cn" do
130
+
131
+ context "parameter handling" do
132
+
133
+ let (:response) { Certmeister::Response.hit("...crt...") }
134
+ before(:each) do
135
+ get "/certificate/axl.starjuice.net", {"psk" => "...secret..."}, {"REMOTE_ADDR" => "192.168.1.2"}
136
+ end
137
+
138
+ it "copies the cn into the params" do
139
+ expect(ca).to have_received(:fetch).with hash_including(cn: "axl.starjuice.net")
140
+ end
141
+
142
+ it "copies the ip into the params" do
143
+ expect(ca).to have_received(:fetch).with hash_including(ip: "192.168.1.2")
144
+ end
145
+
146
+ it "passes the form params into the CA" do
147
+ expect(ca).to have_received(:fetch).with hash_including(psk: "...secret...")
148
+ end
149
+
150
+ end
151
+
152
+ context "on hit" do
153
+
154
+ let (:response) { Certmeister::Response.hit("...crt...") }
155
+ before(:each) do
156
+ get "/certificate/axl.starjuice.net"
157
+ end
158
+
159
+ it "returns 200 OK" do
160
+ expect(last_response.status).to eql 200
161
+ end
162
+
163
+ it "provides the PEM-encoded X.509 certificate in the body" do
164
+ expect(last_response.body).to eql "...crt..."
165
+ end
166
+
167
+ it "describes the body as application/x-pem-file" do
168
+ expect(last_response.headers['Content-Type']).to eql 'application/x-pem-file'
169
+ end
170
+
171
+ end
172
+
173
+ context "on miss" do
174
+
175
+ let (:response) { Certmeister::Response.miss }
176
+ before(:each) do
177
+ get "/certificate/axl.starjuice.net"
178
+ end
179
+
180
+ it "returns 404 Not Found" do
181
+ expect(last_response.status).to eql 404
182
+ end
183
+
184
+ it "describes the HTTP status in the body" do
185
+ expect(last_response.body).to eql "404 Not Found"
186
+ end
187
+
188
+ it "describes the body as text/plain" do
189
+ expect(last_response.headers['Content-Type']).to eql "text/plain"
190
+ end
191
+
192
+ end
193
+
194
+ context "on denied" do
195
+
196
+ let (:response) { Certmeister::Response.denied("your fu is weak") }
197
+ before(:each) do
198
+ get "/certificate/axl.starjuice.net"
199
+ end
200
+
201
+ it "returns 403 Forbidden" do
202
+ expect(last_response.status).to eql 403
203
+ end
204
+
205
+ it "describes the HTTP status in the body" do
206
+ expect(last_response.body).to eql '403 Forbidden (your fu is weak)'
207
+ end
208
+
209
+ it "describes the body as text/plain" do
210
+ expect(last_response.headers['Content-Type']).to eql 'text/plain'
211
+ end
212
+
213
+ end
214
+
215
+ context "on error" do
216
+
217
+ let (:response) { Certmeister::Response.error("i have fallen") }
218
+
219
+ before(:each) do
220
+ get "/certificate/axl.starjuice.net"
221
+ end
222
+
223
+ it "returns 500 Internal Server Error" do
224
+ expect(last_response.status).to eql 500
225
+ end
226
+
227
+ it "describes the HTTP status in the body" do
228
+ expect(last_response.body).to eql "500 Internal Server Error (i have fallen)"
229
+ end
230
+
231
+ it "describes the body as text/plain" do
232
+ expect(last_response.headers['Content-Type']).to eql 'text/plain'
233
+ end
234
+
235
+ end
236
+
237
+ end
238
+
239
+ describe "DELETE /certificate/:cn" do
240
+
241
+ context "parameter handling" do
242
+
243
+ let (:response) { Certmeister::Response.hit }
244
+ before(:each) do
245
+ delete "/certificate/axl.starjuice.net", {"authoritah" => "...warhammer..."}, {"REMOTE_ADDR" => "192.168.1.2"}
246
+ end
247
+
248
+ it "copies the cn into the params" do
249
+ expect(ca).to have_received(:remove).with hash_including(cn: "axl.starjuice.net")
250
+ end
251
+
252
+ it "copies the ip into the params" do
253
+ expect(ca).to have_received(:remove).with hash_including(ip: "192.168.1.2")
254
+ end
255
+
256
+ it "passes the form params into the CA" do
257
+ expect(ca).to have_received(:remove).with hash_including(authoritah: "...warhammer...")
258
+ end
259
+
260
+ end
261
+
262
+ context "on hit" do
263
+
264
+ let (:response) { Certmeister::Response.hit }
265
+ before(:each) do
266
+ delete "/certificate/axl.starjuice.net"
267
+ end
268
+
269
+ it "returns 200 OK" do
270
+ expect(last_response.status).to eql 200
271
+ end
272
+
273
+ it "describes the HTTP status in the body" do
274
+ expect(last_response.body).to eql "200 OK"
275
+ end
276
+
277
+ it "describes the body as text/plain" do
278
+ expect(last_response.headers['Content-Type']).to eql 'text/plain'
279
+ end
280
+
281
+ end
282
+
283
+ context "on miss" do
284
+
285
+ let (:response) { Certmeister::Response.miss }
286
+ before(:each) do
287
+ delete "/certificate/axl.starjuice.net"
288
+ end
289
+
290
+ it "returns 404 Not Found" do
291
+ expect(last_response.status).to eql 404
292
+ end
293
+
294
+ it "describes the HTTP status in the body" do
295
+ expect(last_response.body).to eql "404 Not Found"
296
+ end
297
+
298
+ it "describes the body as text/plain" do
299
+ expect(last_response.headers['Content-Type']).to eql "text/plain"
300
+ end
301
+
302
+ end
303
+
304
+ context "on denied" do
305
+
306
+ let (:response) { Certmeister::Response.denied("y u no boss") }
307
+ before(:each) do
308
+ delete "/certificate/axl.starjuice.net"
309
+ end
310
+
311
+ it "returns 403 Forbidden" do
312
+ expect(last_response.status).to eql 403
313
+ end
314
+
315
+ it "describes the HTTP status in the body" do
316
+ expect(last_response.body).to eql '403 Forbidden (y u no boss)'
317
+ end
318
+
319
+ it "describes the body as text/plain" do
320
+ expect(last_response.headers['Content-Type']).to eql 'text/plain'
321
+ end
322
+
323
+ end
324
+
325
+ context "on error" do
326
+
327
+ let (:response) { Certmeister::Response.error("shuffled off this mortal coil") }
328
+
329
+ before(:each) do
330
+ delete "/certificate/axl.starjuice.net"
331
+ end
332
+
333
+ it "returns 500 Internal Server Error" do
334
+ expect(last_response.status).to eql 500
335
+ end
336
+
337
+ it "describes the HTTP status in the body" do
338
+ expect(last_response.body).to eql "500 Internal Server Error (shuffled off this mortal coil)"
339
+ end
340
+
341
+ it "describes the body as text/plain" do
342
+ expect(last_response.headers['Content-Type']).to eql 'text/plain'
343
+ end
344
+
345
+ end
346
+
347
+ end
348
+
349
+ describe "other verbs on /certificate/:cn" do
350
+
351
+ context "(e.g. HEAD)" do
352
+
353
+ before(:each) do
354
+ head "/certificate/axl.starjuice.net"
355
+ end
356
+
357
+ it "returns 405 Method Not Allowed" do
358
+ expect(last_response.status).to eql 405
359
+ end
360
+
361
+ it "describes the HTTP status in the body" do
362
+ expect(last_response.body).to eql '405 Method Not Allowed'
363
+ end
364
+
365
+ it "describes the body as text/plain" do
366
+ expect(last_response.headers['Content-Type']).to eql 'text/plain'
367
+ end
368
+
369
+ end
370
+
371
+ end
372
+
373
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: certmeister-rack
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ platform: ruby
6
+ authors:
7
+ - Sheldon Hearn
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-02-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: certmeister
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.3.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.3.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: rack
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '1.5'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '1.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rack-test
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '0.6'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '0.6'
55
+ description: This gem provides a rack application to offer an HTTP service around
56
+ certmeister, the conditional autosigning certificate authority.
57
+ email:
58
+ - sheldonh@starjuice.net
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - lib/certmeister/rack/app.rb
64
+ - lib/certmeister/rack/symbolic_hash_accessor.rb
65
+ - spec/certmeister/rack/app_spec.rb
66
+ homepage: https://github.com/sheldonh/certmeister
67
+ licenses:
68
+ - MIT
69
+ metadata: {}
70
+ post_install_message:
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - '>='
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubyforge_project:
86
+ rubygems_version: 2.2.1
87
+ signing_key:
88
+ specification_version: 4
89
+ summary: Rack application for certmeister
90
+ test_files:
91
+ - spec/certmeister/rack/app_spec.rb