ruby-saml-mod 0.1.30 → 0.2.0

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