Skip to content

Commit 6233278

Browse files
authored
Merge pull request #333 from mauromol/fix-response-issuer-retrieval
Fix extraction of the response issuer
2 parents 211e54b + 7692f56 commit 6233278

File tree

3 files changed

+151
-27
lines changed

3 files changed

+151
-27
lines changed

core/src/main/java/com/onelogin/saml2/authn/SamlResponse.java

Lines changed: 67 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -707,38 +707,89 @@ public List<String> getAudiences() throws XPathExpressionException {
707707
}
708708

709709
/**
710-
* Gets the Issuers (from Response and Assertion).
710+
* Gets the Response Issuer.
711711
*
712-
* @return the issuers of the assertion/response
712+
* @return the Response Issuer, or <code>null</code> if not specified
713713
*
714714
* @throws XPathExpressionException
715715
* @throws ValidationError
716+
* if multiple Response issuers were found
717+
* @see #getAssertionIssuer()
718+
* @see #getIssuers()
716719
*/
717-
public List<String> getIssuers() throws XPathExpressionException, ValidationError {
718-
List<String> issuers = new ArrayList<String>();
719-
String value;
720+
public String getResponseIssuer() throws XPathExpressionException, ValidationError {
720721
NodeList responseIssuer = Util.query(samlResponseDocument, "/samlp:Response/saml:Issuer");
721-
if (responseIssuer.getLength() > 1) {
722+
if (responseIssuer.getLength() > 0) {
722723
if (responseIssuer.getLength() == 1) {
723-
value = responseIssuer.item(0).getTextContent();
724-
if (!issuers.contains(value)) {
725-
issuers.add(value);
726-
}
724+
return responseIssuer.item(0).getTextContent();
727725
} else {
728726
throw new ValidationError("Issuer of the Response is multiple.", ValidationError.ISSUER_MULTIPLE_IN_RESPONSE);
729727
}
730728
}
731-
729+
return null;
730+
}
731+
732+
/**
733+
* Gets the Assertion Issuer.
734+
*
735+
* @return the Assertion Issuer
736+
*
737+
* @throws XPathExpressionException
738+
* @throws ValidationError
739+
* if no Assertion Issuer could be found, or if multiple Assertion
740+
* issuers were found
741+
* @see #getResponseIssuer()
742+
* @see #getIssuers()
743+
*/
744+
public String getAssertionIssuer() throws XPathExpressionException, ValidationError {
732745
NodeList assertionIssuer = this.queryAssertion("/saml:Issuer");
733746
if (assertionIssuer.getLength() == 1) {
734-
value = assertionIssuer.item(0).getTextContent();
735-
if (!issuers.contains(value)) {
736-
issuers.add(value);
737-
}
747+
return assertionIssuer.item(0).getTextContent();
738748
} else {
739749
throw new ValidationError("Issuer of the Assertion not found or multiple.", ValidationError.ISSUER_NOT_FOUND_IN_ASSERTION);
740750
}
741-
751+
}
752+
753+
/**
754+
* Gets the Issuers (from Response and Assertion). If the same issuer appears
755+
* both in the Response and in the Assertion (as it should), the returned list
756+
* will contain it just once. Hence, the returned list should always return one
757+
* element and in particular:
758+
* <ul>
759+
* <li>it will never contain zero elements (it means an Assertion Issuer could
760+
* not be found, hence a {@link ValidationError} will be thrown instead)
761+
* <li>if it contains more than one element, it means that the response is
762+
* invalid and one of the returned issuers won't pass the check performed by
763+
* {@link #isValid(String)} (which requires both issuers to be equal to the
764+
* Identity Provider entity id)
765+
* </ul>
766+
* <p>
767+
* Warning: as a consequence of the above, if this response status code is not a
768+
* successful one, this method will throw a {@link ValidationError} because it
769+
* won't find any Assertion Issuer. In this case, if you need to retrieve the
770+
* Response Issuer any way, you must use {@link #getResponseIssuer()} instead.
771+
*
772+
* @return the issuers of the assertion/response
773+
*
774+
* @throws XPathExpressionException
775+
* @throws ValidationError
776+
* if multiple Response Issuers or multiple Assertion Issuers were
777+
* found, or if no Assertion Issuer could be found
778+
* @see #getResponseIssuer()
779+
* @see #getAssertionIssuer()
780+
* @deprecated use {@link #getResponseIssuer()} and/or
781+
* {@link #getAssertionIssuer()}; the contract of this method is
782+
* quite controversial
783+
*/
784+
@Deprecated
785+
public List<String> getIssuers() throws XPathExpressionException, ValidationError {
786+
List<String> issuers = new ArrayList<String>();
787+
String responseIssuer = getResponseIssuer();
788+
if(responseIssuer != null)
789+
issuers.add(responseIssuer);
790+
String assertionIssuer = getAssertionIssuer();
791+
if(!issuers.contains(assertionIssuer))
792+
issuers.add(assertionIssuer);
742793
return issuers;
743794
}
744795

core/src/test/java/com/onelogin/saml2/test/authn/AuthnResponseTest.java

Lines changed: 83 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -846,7 +846,7 @@ public void testGetAudiences() throws IOException, Error, XPathExpressionExcepti
846846
}
847847

848848
/**
849-
* Tests the getIssuers method of SamlResponse
849+
* Tests the getIssuers methods of SamlResponse
850850
*
851851
* @throws Error
852852
* @throws IOException
@@ -861,46 +861,118 @@ public void testGetAudiences() throws IOException, Error, XPathExpressionExcepti
861861
@Test
862862
public void testGetIssuers() throws IOException, Error, XPathExpressionException, ParserConfigurationException, SAXException, SettingsException, ValidationError {
863863
Saml2Settings settings = new SettingsBuilder().fromFile("config/config.my.properties").build();
864-
String samlResponseEncoded = Util.getFileAsString("data/responses/response1.xml.base64");
864+
String samlResponseEncoded = Util.getFileAsString("data/responses/valid_encrypted_assertion.xml.base64");
865865
SamlResponse samlResponse = new SamlResponse(settings, newHttpRequest(samlResponseEncoded));
866+
String expectedIssuer = "http://idp.example.com/";
866867
List<String> expectedIssuers = new ArrayList<String>();
867-
expectedIssuers.add("http://idp.example.com/");
868-
samlResponseEncoded = Util.getFileAsString("data/responses/valid_encrypted_assertion.xml.base64");
869-
samlResponse = new SamlResponse(settings, newHttpRequest(samlResponseEncoded));
868+
expectedIssuers.add(expectedIssuer);
869+
assertEquals(expectedIssuer, samlResponse.getResponseIssuer());
870+
assertEquals(expectedIssuer, samlResponse.getAssertionIssuer());
870871
assertEquals(expectedIssuers, samlResponse.getIssuers());
871872

872873
expectedIssuers.remove(0);
873-
expectedIssuers.add("https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php");
874+
expectedIssuer = "https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php";
875+
expectedIssuers.add(expectedIssuer);
874876

875877
samlResponseEncoded = Util.getFileAsString("data/responses/signed_message_encrypted_assertion.xml.base64");
876878
samlResponse = new SamlResponse(settings, newHttpRequest(samlResponseEncoded));
879+
assertEquals(expectedIssuer, samlResponse.getResponseIssuer());
880+
assertEquals(expectedIssuer, samlResponse.getAssertionIssuer());
877881
assertEquals(expectedIssuers, samlResponse.getIssuers());
878882

879883
samlResponseEncoded = Util.getFileAsString("data/responses/double_signed_encrypted_assertion.xml.base64");
880884
samlResponse = new SamlResponse(settings, newHttpRequest(samlResponseEncoded));
885+
assertEquals(expectedIssuer, samlResponse.getResponseIssuer());
886+
assertEquals(expectedIssuer, samlResponse.getAssertionIssuer());
881887
assertEquals(expectedIssuers, samlResponse.getIssuers());
882888

883889
samlResponseEncoded = Util.getFileAsString("data/responses/signed_encrypted_assertion.xml.base64");
884890
samlResponse = new SamlResponse(settings, newHttpRequest(samlResponseEncoded));
891+
assertEquals(expectedIssuer, samlResponse.getResponseIssuer());
892+
assertEquals(expectedIssuer, samlResponse.getAssertionIssuer());
885893
assertEquals(expectedIssuers, samlResponse.getIssuers());
886894

887895
samlResponseEncoded = Util.getFileAsString("data/responses/double_signed_response.xml.base64");
888896
samlResponse = new SamlResponse(settings, newHttpRequest(samlResponseEncoded));
897+
assertEquals(expectedIssuer, samlResponse.getResponseIssuer());
898+
assertEquals(expectedIssuer, samlResponse.getAssertionIssuer());
889899
assertEquals(expectedIssuers, samlResponse.getIssuers());
890900

891901
samlResponseEncoded = Util.getFileAsString("data/responses/signed_assertion_response.xml.base64");
892902
samlResponse = new SamlResponse(settings, newHttpRequest(samlResponseEncoded));
903+
assertEquals(expectedIssuer, samlResponse.getResponseIssuer());
904+
assertEquals(expectedIssuer, samlResponse.getAssertionIssuer());
893905
assertEquals(expectedIssuers, samlResponse.getIssuers());
894906

907+
expectedIssuer = "https://app.onelogin.com/saml/metadata/13590";
895908
expectedIssuers = new ArrayList<String>();
896-
expectedIssuers.add("https://app.onelogin.com/saml/metadata/13590");
909+
expectedIssuers.add(expectedIssuer);
897910
samlResponseEncoded = Util.getFileAsString("data/responses/invalids/no_issuer_response.xml.base64");
898911
samlResponse = new SamlResponse(settings, newHttpRequest(samlResponseEncoded));
912+
assertNull(expectedIssuer, samlResponse.getResponseIssuer());
913+
assertEquals(expectedIssuer, samlResponse.getAssertionIssuer());
899914
assertEquals(expectedIssuers, samlResponse.getIssuers());
900915
}
901916

902917
/**
903-
* Tests the getIssuers method of SamlResponse
918+
* Tests the getIssuers methods of SamlResponse
919+
* <p>
920+
* Case: different issuers for response and assertion
921+
*
922+
* @throws Error
923+
* @throws IOException
924+
* @throws ValidationError
925+
* @throws SettingsException
926+
* @throws SAXException
927+
* @throws ParserConfigurationException
928+
* @throws XPathExpressionException
929+
*
930+
* @see com.onelogin.saml2.authn.SamlResponse#getIssuers
931+
*/
932+
@Test
933+
public void testGetIssuersDifferentIssuers() throws IOException, Error, XPathExpressionException, ParserConfigurationException, SAXException, SettingsException, ValidationError {
934+
Saml2Settings settings = new SettingsBuilder().fromFile("config/config.my.properties").build();
935+
String samlResponseEncoded = Util.getFileAsString("data/responses/invalids/different_issuers.xml.base64");
936+
SamlResponse samlResponse = new SamlResponse(settings, newHttpRequest(samlResponseEncoded));
937+
List<String> expectedIssuers = new ArrayList<String>();
938+
String expectedResponseIssuer = "https://response-issuer.com";
939+
String expectedAssertionIssuer = "https://assertion-issuer.com";
940+
expectedIssuers.add(expectedResponseIssuer);
941+
expectedIssuers.add(expectedAssertionIssuer);
942+
assertEquals(expectedResponseIssuer, samlResponse.getResponseIssuer());
943+
assertEquals(expectedAssertionIssuer, samlResponse.getAssertionIssuer());
944+
assertEquals(expectedIssuers, samlResponse.getIssuers());
945+
}
946+
947+
/**
948+
* Tests the getAssertionIssuer method of SamlResponse
949+
* <p>
950+
* Case: Issuer of the assertion not found
951+
*
952+
* @throws Error
953+
* @throws IOException
954+
* @throws ValidationError
955+
* @throws SettingsException
956+
* @throws SAXException
957+
* @throws ParserConfigurationException
958+
* @throws XPathExpressionException
959+
*
960+
* @see com.onelogin.saml2.authn.SamlResponse#getIssuers
961+
*/
962+
@Test
963+
public void testGetAssertionIssuerNoInAssertion() throws IOException, Error, XPathExpressionException, ParserConfigurationException, SAXException, SettingsException, ValidationError {
964+
Saml2Settings settings = new SettingsBuilder().fromFile("config/config.my.properties").build();
965+
String samlResponseEncoded = Util.getFileAsString("data/responses/invalids/no_issuer_assertion.xml.base64");
966+
SamlResponse samlResponse = new SamlResponse(settings, newHttpRequest(samlResponseEncoded));
967+
968+
expectedEx.expect(ValidationError.class);
969+
expectedEx.expectMessage("Issuer of the Assertion not found or multiple.");
970+
samlResponse.getAssertionIssuer();
971+
}
972+
973+
/**
974+
* Tests the getIssuers methods of SamlResponse
975+
* <p>
904976
* Case: Issuer of the assertion not found
905977
*
906978
* @throws Error
@@ -919,11 +991,12 @@ public void testGetIssuersNoInAssertion() throws IOException, Error, XPathExpres
919991
String samlResponseEncoded = Util.getFileAsString("data/responses/invalids/no_issuer_assertion.xml.base64");
920992
SamlResponse samlResponse = new SamlResponse(settings, newHttpRequest(samlResponseEncoded));
921993

994+
samlResponse.getResponseIssuer(); // this should not fail
922995
expectedEx.expect(ValidationError.class);
923996
expectedEx.expectMessage("Issuer of the Assertion not found or multiple.");
924997
samlResponse.getIssuers();
925998
}
926-
999+
9271000
/**
9281001
* Tests the getSessionIndex method of SamlResponse
9291002
*
@@ -1811,8 +1884,7 @@ public void testIsInValidIssuer() throws IOException, Error, XPathExpressionExce
18111884
settings.setStrict(true);
18121885
samlResponse = new SamlResponse(settings, newHttpRequest(samlResponseEncoded));
18131886
assertFalse(samlResponse.isValid());
1814-
assertEquals("No Signature found. SAML Response rejected", samlResponse.getError());
1815-
1887+
assertEquals("Invalid issuer in the Assertion/Response. Was 'http://invalid.isser.example.com/', but expected 'http://idp.example.com/'", samlResponse.getError());
18161888
}
18171889

18181890
/**
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgSUQ9IkdPU0FNTFIxMjkwMTE3NDU3MTc5NCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTAtMTEtMThUMjE6NTc6MzdaIiBEZXN0aW5hdGlvbj0ie3JlY2lwaWVudH0iPg0KICA8c2FtbDpJc3N1ZXI+aHR0cHM6Ly9yZXNwb25zZS1pc3N1ZXIuY29tPC9zYW1sOklzc3Vlcj4NCiAgPHNhbWxwOlN0YXR1cz4NCiAgICA8c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+DQogIDxzYW1sOkFzc2VydGlvbiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIFZlcnNpb249IjIuMCIgSUQ9InBmeGE0NjU3NGRmLWIzYjAtYTA2YS0yM2M4LTYzNjQxMzE5ODc3MiIgSXNzdWVJbnN0YW50PSIyMDEwLTExLTE4VDIxOjU3OjM3WiI+DQogICAgPHNhbWw6SXNzdWVyPmh0dHBzOi8vYXNzZXJ0aW9uLWlzc3Vlci5jb208L3NhbWw6SXNzdWVyPg0KICAgIDxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICAgICAgPGRzOlNpZ25lZEluZm8+DQogICAgICAgIDxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgICAgICAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZnhhNDY1NzRkZi1iM2IwLWEwNmEtMjNjOC02MzY0MTMxOTg3NzIiPg0KICAgICAgICAgIDxkczpUcmFuc2Zvcm1zPg0KICAgICAgICAgICAgPGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+DQogICAgICAgICAgICA8ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgICAgICAgPC9kczpUcmFuc2Zvcm1zPg0KICAgICAgICAgIDxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIvPg0KICAgICAgICAgIDxkczpEaWdlc3RWYWx1ZT5wSlE3TVMvZWs0S1JSV0dtdi9INDNSZUhZTXM9PC9kczpEaWdlc3RWYWx1ZT4NCiAgICAgICAgPC9kczpSZWZlcmVuY2U+DQogICAgICA8L2RzOlNpZ25lZEluZm8+DQogICAgICA8ZHM6U2lnbmF0dXJlVmFsdWU+eWl2ZUtjUGREcHVETmo2c2hyUTNBQndyL2NBM0NyeUQycGhHL3hMWnN6S1d4VTUvbWxhS3Q4ZXdiWk9kS0t2dE9zMnBIQnk1RHVhM2s5NEFGK3p4R3llbDVnT293bW95WEpyK0FPcitrUE8wdmxpMVY4bzNoUFBVWndSZ1NYNlE5cFMxQ3FRZ2hLaUVhc1J5eWxxcUpVYVBZem1Pek9FOC9YbE1rd2lXbU8wPTwvZHM6U2lnbmF0dXJlVmFsdWU+DQogICAgICA8ZHM6S2V5SW5mbz4NCiAgICAgICAgPGRzOlg1MDlEYXRhPg0KICAgICAgICAgIDxkczpYNTA5Q2VydGlmaWNhdGU+TUlJQnJUQ0NBYUdnQXdJQkFnSUJBVEFEQmdFQU1HY3hDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJREFwRFlXeHBabTl5Ym1saE1SVXdFd1lEVlFRSERBeFRZVzUwWVNCTmIyNXBZMkV4RVRBUEJnTlZCQW9NQ0U5dVpVeHZaMmx1TVJrd0Z3WURWUVFEREJCaGNIQXViMjVsYkc5bmFXNHVZMjl0TUI0WERURXdNRE13T1RBNU5UZzBOVm9YRFRFMU1ETXdPVEE1TlRnME5Wb3daekVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnTUNrTmhiR2xtYjNKdWFXRXhGVEFUQmdOVkJBY01ERk5oYm5SaElFMXZibWxqWVRFUk1BOEdBMVVFQ2d3SVQyNWxURzluYVc0eEdUQVhCZ05WQkFNTUVHRndjQzV2Ym1Wc2IyZHBiaTVqYjIwd2daOHdEUVlKS29aSWh2Y05BUUVCQlFBRGdZMEFNSUdKQW9HQkFPalN1MWZqUHk4ZDV3NFF5TDEremQ0aEl3MU1ra2ZmNFdZL1RMRzhPWmtVNVlUU1dtbUhQRDVrdllINXVvWFMvNnFRODFxWHBSMndWOENUb3daSlVMZzA5ZGRSZFJuOFFzcWoxRnlPQzVzbEUzeTJiWjJvRnVhNzJvZi80OWZwdWpuRlQ2S25RNjFDQk1xbERvVFFxT1Q2MnZHSjhuUDZNWld2QTZzeHF1ZDVBZ01CQUFFd0F3WUJBQU1CQUE9PTwvZHM6WDUwOUNlcnRpZmljYXRlPg0KICAgICAgICA8L2RzOlg1MDlEYXRhPg0KICAgICAgPC9kczpLZXlJbmZvPg0KICAgIDwvZHM6U2lnbmF0dXJlPg0KICAgIDxzYW1sOlN1YmplY3Q+DQogICAgICA8c2FtbDpOYW1lSUQgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPnN1cHBvcnRAb25lbG9naW4uY29tPC9zYW1sOk5hbWVJRD4NCiAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj4NCiAgICAgICAgPHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDEwLTExLTE4VDIyOjAyOjM3WiIgUmVjaXBpZW50PSJ7cmVjaXBpZW50fSIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPg0KICAgIDwvc2FtbDpTdWJqZWN0Pg0KICAgIDxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDEwLTExLTE4VDIxOjUyOjM3WiIgTm90T25PckFmdGVyPSIyMDEwLTExLTE4VDIyOjAyOjM3WiI+DQogICAgICA8c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgICAgICA8c2FtbDpBdWRpZW5jZT57YXVkaWVuY2V9PC9zYW1sOkF1ZGllbmNlPg0KICAgICAgPC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+DQogICAgPC9zYW1sOkNvbmRpdGlvbnM+DQogICAgPHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDEwLTExLTE4VDIxOjU3OjM3WiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAxMC0xMS0xOVQyMTo1NzozN1oiIFNlc3Npb25JbmRleD0iXzUzMWMzMmQyODNiZGZmN2UwNGU0ODdiY2RiYzRkZDhkIj4NCiAgICAgIDxzYW1sOkF1dGhuQ29udGV4dD4NCiAgICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+DQogICAgICA8L3NhbWw6QXV0aG5Db250ZXh0Pg0KICAgIDwvc2FtbDpBdXRoblN0YXRlbWVudD4NCiAgICA8c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj5kZW1vPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPg0KICAgICAgPC9zYW1sOkF0dHJpYnV0ZT4NCiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJhbm90aGVyX3ZhbHVlIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj52YWx1ZTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgPC9zYW1sOkFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+DQo=

0 commit comments

Comments
 (0)