ruby-saml-mod 0.1.30 → 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.
@@ -0,0 +1,89 @@
1
+ require 'spec_helper'
2
+
3
+ def verify_query_string_signature(settings, forward_url)
4
+ url = URI.parse(forward_url)
5
+ signed_data, signature = url.query.split('&Signature=')
6
+ cert = OpenSSL::X509::Certificate.new(File.read(settings.xmlsec_certificate))
7
+ cert.public_key.verify(OpenSSL::Digest::SHA1.new, Base64.decode64(CGI.unescape(signature)), signed_data)
8
+ end
9
+
10
+ # see http://stackoverflow.com/questions/1361892/how-to-decompress-gzip-string-in-ruby
11
+ def inflate(string)
12
+ zstream = Zlib::Inflate.new(-Zlib::MAX_WBITS)
13
+ buf = zstream.inflate(string)
14
+ zstream.finish
15
+ zstream.close
16
+ buf
17
+ end
18
+
19
+ describe Onelogin::Saml::LogoutRequest do
20
+ let(:settings) do
21
+ Onelogin::Saml::Settings.new(
22
+ :xmlsec_certificate => fixture_path("test1-cert.pem"),
23
+ :xmlsec_privatekey => fixture_path("test1-key.pem"),
24
+ :idp_slo_target_url => "http://idp.example.com/saml2",
25
+ :idp_cert_fingerprint => 'def18dbed547cdf3d52b627f41637c443045fe33',
26
+ :name_identifier_format => Onelogin::Saml::NameIdentifiers::UNSPECIFIED
27
+ )
28
+ end
29
+
30
+ let(:name_qualifier) { 'foo' }
31
+ let(:name_id) { 'bar'}
32
+ let(:session_index) { 'baz' }
33
+
34
+ let(:logout_request) do
35
+ Onelogin::Saml::LogoutRequest::generate(
36
+ name_qualifier,
37
+ name_id,
38
+ session_index,
39
+ settings
40
+ )
41
+ end
42
+
43
+ let(:forward_url) { logout_request.forward_url }
44
+
45
+ it "includes destination in the saml:LogoutRequest attributes" do
46
+ logout_xml = LibXML::XML::Document.string(logout_request.xml)
47
+ logout_xml.find_first('/samlp:LogoutRequest', Onelogin::NAMESPACES).attributes['Destination'].should == "http://idp.example.com/saml2"
48
+ end
49
+
50
+ it "properly sets the Format attribute NameID based on settings" do
51
+ logout_xml = LibXML::XML::Document.string(logout_request.xml)
52
+ logout_xml.find_first('/samlp:LogoutRequest/saml:NameID', Onelogin::NAMESPACES).attributes['Format'].should == Onelogin::Saml::NameIdentifiers::UNSPECIFIED
53
+ end
54
+
55
+ it "does not include the signature in the request xml" do
56
+ logout_xml = LibXML::XML::Document.string(logout_request.xml)
57
+ logout_xml.find_first('/samlp:LogoutRequest/ds:Signature', Onelogin::NAMESPACES).should be_nil
58
+ end
59
+
60
+ it "can sign the generated query string" do
61
+ expect(verify_query_string_signature(settings, forward_url)).to be_true
62
+ end
63
+
64
+ it "properly signs when the IDP URL already contains a query string" do
65
+ settings = Onelogin::Saml::Settings.new(
66
+ :xmlsec_certificate => fixture_path("test1-cert.pem"),
67
+ :xmlsec_privatekey => fixture_path("test1-key.pem"),
68
+ :idp_slo_target_url => "http://idp.example.com/saml2?existing=param",
69
+ :idp_cert_fingerprint => 'def18dbed547cdf3d52b627f41637c443045fe33',
70
+ :name_identifier_format => Onelogin::Saml::NameIdentifiers::UNSPECIFIED
71
+ )
72
+ request = Onelogin::Saml::LogoutRequest.generate(name_qualifier, name_id, session_index, settings)
73
+ expect(request.forward_url).to match(%r{^http://idp.example.com/saml2\?existing=param&})
74
+ expect(verify_query_string_signature(settings, request.forward_url)).to be_true
75
+ end
76
+
77
+ it "parses a logout request" do
78
+ xml = Zlib::Deflate.deflate(File.read(fixture_path("logout_request.xml")), 9)[2..-5]
79
+
80
+ xmlb64 = Base64.encode64(xml)
81
+ settings = Onelogin::Saml::Settings.new
82
+ request = Onelogin::Saml::LogoutRequest::parse(xmlb64)
83
+
84
+ expect(request.id).to eq '_cbb63e9741259e3f1c98a1ae38ac5ac25889720b32'
85
+ expect(request.issuer).to eq 'http://saml.example.com:8080/opensso'
86
+ expect(request.name_id).to eq '_6a171f538d4f733ae95eca74ce264cfb602808c850'
87
+ expect(request.session_index).to eq '_b976de57fcf0f707de297069f33a6b0248827d96a9'
88
+ end
89
+ end
@@ -0,0 +1,76 @@
1
+ require 'spec_helper'
2
+ require 'rexml/document'
3
+ require 'cgi'
4
+
5
+ describe Onelogin::Saml::LogoutResponse do
6
+ let(:id) { Onelogin::Saml::LogoutResponse.generate_unique_id(42) }
7
+ let(:issue_instant) { Onelogin::Saml::LogoutResponse.get_timestamp }
8
+ let(:in_response_to) { Onelogin::Saml::LogoutResponse.generate_unique_id(42) }
9
+ let(:idp_slo_target_url) { 'http://idp.example.com/saml2' }
10
+ let(:issuer) { 'http://idp.example.com/saml2' }
11
+ let(:session) { {} }
12
+
13
+ let(:settings) do
14
+ Onelogin::Saml::Settings.new(
15
+ idp_slo_target_url: idp_slo_target_url,
16
+ issuer: issuer
17
+ )
18
+ end
19
+
20
+ let(:xml) do
21
+ allow(Onelogin::Saml::LogoutResponse).to receive(:generate_unique_id).and_return(id)
22
+ allow(Onelogin::Saml::LogoutResponse).to receive(:get_timestamp).and_return(issue_instant)
23
+
24
+ Onelogin::Saml::LogoutResponse::generate(in_response_to, settings).document
25
+ end
26
+
27
+ it "includes destination in the saml:LogoutRequest attributes" do
28
+ value = xml.find_first('/samlp:LogoutResponse', Onelogin::NAMESPACES).attributes['Destination']
29
+ expect(value).to eq "http://idp.example.com/saml2"
30
+ end
31
+
32
+ it "includes id in the saml:LogoutRequest attributes" do
33
+ value = xml.find_first('/samlp:LogoutResponse', Onelogin::NAMESPACES).attributes['ID']
34
+ expect(value).to eq id
35
+ end
36
+
37
+ it "includes issue_instant in the saml:LogoutRequest attributes" do
38
+ value = xml.find_first('/samlp:LogoutResponse', Onelogin::NAMESPACES).attributes['IssueInstant']
39
+ expect(value).to eq issue_instant
40
+ end
41
+
42
+ it "includes in_response_to in the saml:LogoutRequest attributes" do
43
+ value = xml.find_first('/samlp:LogoutResponse', Onelogin::NAMESPACES).attributes['InResponseTo']
44
+ expect(value).to eq in_response_to
45
+ end
46
+
47
+ it "includes issuer tag" do
48
+ value = xml.find_first("/samlp:LogoutResponse/saml:Issuer", Onelogin::NAMESPACES).content
49
+ expect(value).to eq issuer
50
+ end
51
+
52
+ it "includes status code tag" do
53
+ value = xml.find_first("/samlp:LogoutResponse/samlp:Status/samlp:StatusCode", Onelogin::NAMESPACES).attributes['Value']
54
+ expect(value).to eq Onelogin::Saml::StatusCodes::SUCCESS_URI
55
+ end
56
+
57
+ it "includes status message tag" do
58
+ value = xml.find_first("/samlp:LogoutResponse/samlp:Status/samlp:StatusMessage", Onelogin::NAMESPACES).content
59
+ expect(value).to eq Onelogin::Saml::LogoutResponse::STATUS_MESSAGE
60
+ end
61
+
62
+ it "should use namespaces correctly to look up attributes" do
63
+ xml = Zlib::Deflate.deflate(File.read(fixture_path("logout_response.xml")), 9)[2..-5]
64
+
65
+ xmlb64 = Base64.encode64(xml)
66
+ settings = Onelogin::Saml::Settings.new(:idp_cert_fingerprint => 'def18dbed547cdf3d52b627f41637c443045fe33')
67
+ response = Onelogin::Saml::LogoutResponse::parse(xmlb64, settings)
68
+
69
+ expect(response.id).to eq '_cbb63e9741259e3f1c98a1ae38ac5ac25889720b32'
70
+ expect(response.issuer).to eq 'http://saml.example.com:8080/opensso'
71
+ expect(response.in_response_to).to eq "_72424ea37e28763e351189529639b9c2b150ff37e5"
72
+ expect(response.destination).to eq "http://saml.example.com:8080/opensso/SingleLogoutService"
73
+ expect(response.status_code).to eq Onelogin::Saml::StatusCodes::SUCCESS_URI
74
+ expect(response.status_message).to eq "Successfully logged out from service"
75
+ end
76
+ end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+
3
+ describe Onelogin::Saml::MetaData do
4
+ before do
5
+ @settings = Onelogin::Saml::Settings.new(:issuer => "yourmom", :sp_slo_url => 'http://example.com/logout')
6
+ end
7
+
8
+ it "should have correct consumer service with one endpoint" do
9
+ @settings.assertion_consumer_service_url = 'http://example.com/consume'
10
+ doc = REXML::Document.new Onelogin::Saml::MetaData.create(@settings)
11
+ service = REXML::XPath.first(doc, "//AssertionConsumerService/")
12
+ service.attributes["index"].should == "0"
13
+ service.attributes["Location"].should == 'http://example.com/consume'
14
+ end
15
+
16
+ it "should have correct consumer service with multiple endpoints" do
17
+ @settings.assertion_consumer_service_url = ['http://example.com/consume', 'http://example.com/alt_consume']
18
+ doc = REXML::Document.new Onelogin::Saml::MetaData.create(@settings)
19
+ services = REXML::XPath.match(doc, "//AssertionConsumerService/")
20
+ services[0].attributes["index"].should == "0"
21
+ services[0].attributes["Location"].should == @settings.assertion_consumer_service_url[0]
22
+ services[1].attributes["index"].should == "1"
23
+ services[1].attributes["Location"].should == @settings.assertion_consumer_service_url[1]
24
+ end
25
+
26
+ it "publishes the public key for both encryption and signing" do
27
+ settings = Onelogin::Saml::Settings.new(
28
+ :xmlsec_certificate => fixture_path("test1-cert.pem"),
29
+ :xmlsec_privatekey => fixture_path("test1-key.pem"),
30
+ :idp_slo_target_url => "http://idp.example.com/saml2",
31
+ :idp_cert_fingerprint => 'def18dbed547cdf3d52b627f41637c443045fe33'
32
+ )
33
+ doc = REXML::Document.new Onelogin::Saml::MetaData.create(settings)
34
+ key_descriptors = REXML::XPath.match(doc, "//KeyDescriptor")
35
+ key_descriptors.should have(2).keys
36
+ key_descriptors[0].attributes["use"].should == "encryption"
37
+ key_descriptors[1].attributes["use"].should == "signing"
38
+ end
39
+ end
@@ -0,0 +1,193 @@
1
+ require 'spec_helper'
2
+
3
+ describe Onelogin::Saml::Response do
4
+ describe "decrypting assertions" do
5
+ before :each do
6
+ @xmlb64 = Base64.encode64(File.read(fixture_path("test1-response.xml")))
7
+ @settings = Onelogin::Saml::Settings.new(
8
+ :xmlsec_certificate => fixture_path("test1-cert.pem"),
9
+ :xmlsec_privatekey => fixture_path("test1-key.pem"),
10
+ :idp_cert_fingerprint => 'def18dbed547cdf3d52b627f41637c443045fe33'
11
+ )
12
+ end
13
+
14
+ it "should find the right attributes from an encrypted assertion" do
15
+ @response = Onelogin::Saml::Response.new(@xmlb64, @settings)
16
+ @response.should be_is_valid
17
+
18
+ @response.name_id.should == "zach@zwily.com"
19
+ @response.name_qualifier.should == "http://saml.example.com:8080/opensso"
20
+ @response.session_index.should == "s2c57ee92b5ca08e93d751987d591c58acc68d2501"
21
+ @response.status_code.should == "urn:oasis:names:tc:SAML:2.0:status:Success"
22
+ @response.status_message.strip.should == ""
23
+ end
24
+
25
+ it "should not be able to decrypt without the proper key" do
26
+ @settings.xmlsec_privatekey = fixture_path("wrong-key.pem")
27
+ XMLSecurity.mute do
28
+ @response = Onelogin::Saml::Response.new(@xmlb64, @settings)
29
+ end
30
+ document = REXML::Document.new(@response.decrypted_document.to_s)
31
+ REXML::XPath.first(document, "/samlp:Response/saml:Assertion").should be_nil
32
+ @response.name_qualifier.should be_nil
33
+ end
34
+
35
+ it "should be able to decrypt using additional private keys" do
36
+ @settings.xmlsec_privatekey = fixture_path("wrong-key.pem")
37
+ @settings.xmlsec_additional_privatekeys = [fixture_path("test1-key.pem")]
38
+ XMLSecurity.mute do
39
+ @response = Onelogin::Saml::Response.new(@xmlb64, @settings)
40
+ end
41
+ document = REXML::Document.new(@response.decrypted_document.to_s)
42
+ REXML::XPath.first(document, "/samlp:Response/saml:Assertion").should_not be_nil
43
+ REXML::XPath.first(document, "/samlp:Response/saml:Assertion/ds:Signature/ds:SignedInfo/ds:Reference/ds:DigestValue").text.should == "eMQal6uuWKMbUMbOwBfrFH90bzE="
44
+ @response.name_qualifier.should == "http://saml.example.com:8080/opensso"
45
+ @response.session_index.should == "s2c57ee92b5ca08e93d751987d591c58acc68d2501"
46
+ @response.status_code.should == "urn:oasis:names:tc:SAML:2.0:status:Success"
47
+ @response.status_message.strip.should == ""
48
+ end
49
+ end
50
+
51
+ it "should not verify when XSLT transforms are being used" do
52
+ @xmlb64 = Base64.encode64(File.read(fixture_path("test4-response.xml")))
53
+ @settings = Onelogin::Saml::Settings.new(:idp_cert_fingerprint => 'bc71f7bacb36011694405dd0e2beafcc069de45f')
54
+ @response = Onelogin::Saml::Response.new(@xmlb64, @settings)
55
+
56
+ XMLSecurity.mute do
57
+ @response.should_not be_is_valid
58
+ end
59
+
60
+ TestServer.requests.should == []
61
+ end
62
+
63
+ it "should not allow external reference URIs" do
64
+ @xmlb64 = Base64.encode64(File.read(fixture_path("test5-response.xml")))
65
+ @settings = Onelogin::Saml::Settings.new(:idp_cert_fingerprint => 'bc71f7bacb36011694405dd0e2beafcc069de45f')
66
+ @response = Onelogin::Saml::Response.new(@xmlb64, @settings)
67
+
68
+ XMLSecurity.mute do
69
+ @response.should_not be_is_valid
70
+ end
71
+
72
+ TestServer.requests.should == []
73
+ end
74
+
75
+ it "should use namespaces correctly to look up attributes" do
76
+ @xmlb64 = Base64.encode64(File.read(fixture_path("test2-response.xml")))
77
+ @settings = Onelogin::Saml::Settings.new(:idp_cert_fingerprint => 'def18dbed547cdf3d52b627f41637c443045fe33')
78
+ @response = Onelogin::Saml::Response.new(@xmlb64)
79
+ @response.disable_signature_validation!(@settings)
80
+ @response.process(@settings)
81
+ @response.name_id.should == "zach@example.com"
82
+ @response.name_qualifier.should == "http://saml.example.com:8080/opensso"
83
+ @response.session_index.should == "s2c57ee92b5ca08e93d751987d591c58acc68d2501"
84
+ @response.status_code.should == "urn:oasis:names:tc:SAML:2.0:status:Success"
85
+ @response.saml_attributes['eduPersonAffiliation'].should == 'member'
86
+ @response.saml_attributes['eduPersonPrincipalName'].should == 'user@example.edu'
87
+ @response.status_message.should == ""
88
+ @response.fingerprint_from_idp.should == 'def18dbed547cdf3d52b627f41637c443045fe33'
89
+ @response.issuer.should == 'http://saml.example.com:8080/opensso'
90
+ end
91
+
92
+ it "should protect against xml signature wrapping attacks targeting nameid" do
93
+ @xmlb64 = Base64.encode64(File.read(fixture_path("xml_signature_wrapping_attack_response_nameid.xml")))
94
+ @settings = Onelogin::Saml::Settings.new(:idp_cert_fingerprint => 'afe71c28ef740bc87425be13a2263d37971da1f9')
95
+ @response = Onelogin::Saml::Response.new(@xmlb64)
96
+ @response.process(@settings)
97
+ @response.should be_is_valid
98
+ @response.name_id.should == "_3b3e7714b72e29dc4290321a075fa0b73333a4f25f"
99
+ end
100
+
101
+ it "should protect against xml signature wrapping attacks targeting attributes" do
102
+ @xmlb64 = Base64.encode64(File.read(fixture_path("xml_signature_wrapping_attack_response_attributes.xml")))
103
+ @settings = Onelogin::Saml::Settings.new(:idp_cert_fingerprint => 'afe71c28ef740bc87425be13a2263d37971da1f9')
104
+ @response = Onelogin::Saml::Response.new(@xmlb64)
105
+ @response.process(@settings)
106
+ @response.should be_is_valid
107
+ @response.saml_attributes['eduPersonAffiliation'].should == 'member'
108
+ @response.saml_attributes['eduPersonPrincipalName'].should == 'student@example.edu'
109
+ end
110
+
111
+ it "should protect against xml signature wrapping attacks with duplicate IDs" do
112
+ @xmlb64 = Base64.encode64(File.read(fixture_path('xml_signature_wrapping_attack_duplicate_ids.xml')))
113
+ @settings = Onelogin::Saml::Settings.new(:idp_cert_fingerprint => '7292914fc5bffa6f3fe1e43fd47c205395fecfa2')
114
+ @response = Onelogin::Saml::Response.new(@xmlb64)
115
+ @response.process(@settings)
116
+ @response.should_not be_is_valid
117
+ end
118
+
119
+ it "should allow non-ascii characters in attributes" do
120
+ @xmlb64 = Base64.encode64(File.read(fixture_path("test6-response.xml")))
121
+ @settings = Onelogin::Saml::Settings.new(:idp_cert_fingerprint => 'afe71c28ef740bc87425be13a2263d37971da1f9')
122
+ @response = Onelogin::Saml::Response.new(@xmlb64, @settings)
123
+ @response.should be_is_valid
124
+ @response.status_code.should == "urn:oasis:names:tc:SAML:2.0:status:Success"
125
+ @response.saml_attributes['eduPersonAffiliation'].should == 'member'
126
+ @response.saml_attributes['givenName'].should == 'Canvas'
127
+ @response.saml_attributes['displayName'].should == 'Canvas Üser'
128
+ @response.fingerprint_from_idp.should == 'afe71c28ef740bc87425be13a2263d37971da1f9'
129
+ end
130
+
131
+ it "should map OIDs to known attributes" do
132
+ @xmlb64 = Base64.encode64(File.read(fixture_path("test3-response.xml")))
133
+ @settings = Onelogin::Saml::Settings.new(:idp_cert_fingerprint => 'afe71c28ef740bc87425be13a2263d37971da1f9')
134
+ @response = Onelogin::Saml::Response.new(@xmlb64, @settings)
135
+ @response.should be_is_valid
136
+ @response.status_code.should == "urn:oasis:names:tc:SAML:2.0:status:Success"
137
+ @response.saml_attributes['eduPersonAffiliation'].should == 'member'
138
+ @response.saml_attributes['eduPersonPrincipalName'].should == 'student@example.edu'
139
+ @response.fingerprint_from_idp.should == 'afe71c28ef740bc87425be13a2263d37971da1f9'
140
+ end
141
+
142
+ it "should not throw an exception when an empty string is passed as the doc" do
143
+ settings = Onelogin::Saml::Settings.new
144
+ lambda {
145
+ r = Onelogin::Saml::Response.new('foo', settings)
146
+ r.should_not be_is_valid
147
+ }.should_not raise_error
148
+ lambda {
149
+ r = Onelogin::Saml::Response.new('', settings)
150
+ r.should_not be_is_valid
151
+ }.should_not raise_error
152
+ end
153
+
154
+ describe "forward_urls" do
155
+ let(:name_qualifier) { 'foo' }
156
+ let(:name_id) { 'bar'}
157
+ let(:session_index) { 'baz' }
158
+
159
+ it "should should append the saml request to a url" do
160
+ settings = Onelogin::Saml::Settings.new(
161
+ :xmlsec_certificate => fixture_path("test1-cert.pem"),
162
+ :xmlsec_privatekey => fixture_path("test1-key.pem"),
163
+ :idp_sso_target_url => "http://example.com/login.php",
164
+ :idp_slo_target_url => "http://example.com/logout.php"
165
+ )
166
+
167
+ request = Onelogin::Saml::AuthRequest::generate(settings)
168
+ prefix = "http://example.com/login.php?SAMLRequest="
169
+ expect(request.forward_url[0...prefix.size]).to eql(prefix)
170
+
171
+ request = Onelogin::Saml::LogoutRequest::generate(name_qualifier, name_id, session_index, settings)
172
+ prefix = "http://example.com/logout.php?SAMLRequest="
173
+ expect(request.forward_url[0...prefix.size]).to eql(prefix)
174
+ end
175
+
176
+ it "should append the saml request to a url with query parameters" do
177
+ settings = Onelogin::Saml::Settings.new(
178
+ :xmlsec_certificate => fixture_path("test1-cert.pem"),
179
+ :xmlsec_privatekey => fixture_path("test1-key.pem"),
180
+ :idp_sso_target_url => "http://example.com/login.php?param=foo",
181
+ :idp_slo_target_url => "http://example.com/logout.php?param=foo"
182
+ )
183
+
184
+ request = Onelogin::Saml::AuthRequest::generate(settings)
185
+ prefix = "http://example.com/login.php?param=foo&SAMLRequest="
186
+ expect(request.forward_url[0...prefix.size]).to eql(prefix)
187
+
188
+ request = Onelogin::Saml::LogoutRequest::generate(name_qualifier, name_id, session_index, settings)
189
+ prefix = "http://example.com/logout.php?param=foo&SAMLRequest="
190
+ expect(request.forward_url[0...prefix.size]).to eql(prefix)
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,33 @@
1
+ require 'rexml/document'
2
+ require 'cgi'
3
+ require 'uri'
4
+
5
+ require File.expand_path(File.dirname(__FILE__) + '/../lib/onelogin/saml.rb')
6
+
7
+ Dir[File.expand_path(File.dirname(__FILE__) + '/support/**/*.rb')].each { |f| require f }
8
+
9
+ RSpec.configure do |config|
10
+ FIXTURE_PATH = File.expand_path(File.dirname(__FILE__) + '/fixtures')
11
+
12
+ def fixture_path(filename)
13
+ "#{FIXTURE_PATH}/#{filename}"
14
+ end
15
+
16
+ config.before(:suite) do
17
+ TestServer.start(ENV['TEST_SERVER_PORT'] || 2345)
18
+ end
19
+
20
+ config.after(:each) do
21
+ TestServer.reset
22
+ end
23
+
24
+ config.after(:suite) do
25
+ TestServer.stop
26
+ end
27
+
28
+ config.treat_symbols_as_metadata_keys_with_true_values = true
29
+ config.run_all_when_everything_filtered = true
30
+ config.filter_run :focus
31
+ config.order = 'random'
32
+ config.color = true
33
+ end
@@ -0,0 +1,73 @@
1
+ require 'socket'
2
+ require 'net/http'
3
+
4
+ class TestServer
5
+ class << self
6
+ def start(port)
7
+ @port = port
8
+ @requests = []
9
+ @process = Process.fork {
10
+ @server = TCPServer.open(port)
11
+ work
12
+ }
13
+ end
14
+
15
+ def stop
16
+ Process.kill("TERM", @process)
17
+ end
18
+
19
+ def get(path)
20
+ Net::HTTP.get_response(URI("http://127.0.0.1:#{@port}#{path}"))
21
+ end
22
+
23
+ def requests
24
+ get('/requests').body.split("\n")
25
+ end
26
+
27
+ def reset
28
+ get('/reset')
29
+ end
30
+
31
+ def work
32
+ loop do
33
+ socket = @server.accept
34
+
35
+ request = socket.gets
36
+ response = process(request)
37
+
38
+ socket.print([
39
+ "HTTP/1.1 200 OK",
40
+ "Content-Type: application/xml",
41
+ "Content-Length: #{response.bytesize}",
42
+ "Connection: close",
43
+ "",
44
+ response
45
+ ].join("\r\n"))
46
+
47
+ socket.close
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def process(request)
54
+ _, path, _ = request.split(' ')
55
+
56
+ case path
57
+ when '/requests'
58
+ @requests.join("\n")
59
+ when '/reset'
60
+ @requests = []
61
+ '<status>OK</status>'
62
+ when '/exploit'
63
+ <<-RESPONSE
64
+ <!DOCTYPE Response [<!ENTITY file PUBLIC 'p' 'file:///etc/hostname'>]>
65
+ <Response>&file;</Response>
66
+ RESPONSE
67
+ else
68
+ @requests << request.chomp
69
+ '<status>RECORDED</status>'
70
+ end
71
+ end
72
+ end
73
+ end