certmeister-rack 0.3.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
+ 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