Skip to content

Commit a8a9631

Browse files
committedAug 9, 2018
KEYCLOAK-6832 Unify Destination attribute handling
1 parent 2efc7eb commit a8a9631

File tree

8 files changed

+177
-74
lines changed

8 files changed

+177
-74
lines changed
 

‎adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
import org.keycloak.rotation.KeyLocator;
8585
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
8686
import org.keycloak.saml.processing.core.util.XMLEncryptionUtil;
87+
import org.keycloak.saml.validators.DestinationValidator;
8788

8889
/**
8990
*
@@ -97,6 +98,7 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
9798
protected final SamlSessionStore sessionStore;
9899
protected final SamlDeployment deployment;
99100
protected AuthChallenge challenge;
101+
private final DestinationValidator destinationValidator = DestinationValidator.forProtocolMap(null);
100102

101103
public AbstractSamlAuthenticationHandler(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) {
102104
this.facade = facade;
@@ -145,7 +147,7 @@ protected AuthOutcome handleSamlRequest(String samlRequest, String relayState) {
145147
holder = SAMLRequestParser.parseRequestPostBinding(samlRequest);
146148
}
147149
RequestAbstractType requestAbstractType = (RequestAbstractType) holder.getSamlObject();
148-
if (!requestUri.equals(requestAbstractType.getDestination().toString())) {
150+
if (! destinationValidator.validate(requestUri, requestAbstractType.getDestination())) {
149151
log.error("expected destination '" + requestUri + "' got '" + requestAbstractType.getDestination() + "'");
150152
return AuthOutcome.FAILED;
151153
}
@@ -186,7 +188,7 @@ protected AuthOutcome handleSamlResponse(String samlResponse, String relayState,
186188
}
187189
final StatusResponseType statusResponse = (StatusResponseType) holder.getSamlObject();
188190
// validate destination
189-
if (!requestUri.equals(statusResponse.getDestination())) {
191+
if (! destinationValidator.validate(requestUri, statusResponse.getDestination())) {
190192
log.error("Request URI '" + requestUri + "' does not match SAML request destination '" + statusResponse.getDestination() + "'");
191193
return AuthOutcome.FAILED;
192194
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* Copyright 2018 Red Hat, Inc. and/or its affiliates
3+
* and other contributors as indicated by the @author tags.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.keycloak.saml.validators;
18+
19+
import java.net.URI;
20+
import java.net.URISyntaxException;
21+
import java.util.HashMap;
22+
import java.util.Map;
23+
import java.util.Objects;
24+
import java.util.regex.Matcher;
25+
import java.util.regex.Pattern;
26+
27+
/**
28+
* Check that Destination field in SAML request/response is either unset or matches the expected one.
29+
* @author hmlnarik
30+
*/
31+
public class DestinationValidator {
32+
33+
private static final Pattern PROTOCOL_MAP_PATTERN = Pattern.compile("\\s*([a-zA-Z][a-zA-Z\\d+-.]*)\\s*=\\s*(\\d+)\\s*");
34+
private static final String[] DEFAULT_PROTOCOL_TO_PORT_MAP = new String[] { "http=80", "https=443" };
35+
36+
private final Map<String, Integer> knownPorts;
37+
private final Map<Integer, String> knownProtocols;
38+
39+
private DestinationValidator(Map<String, Integer> knownPorts, Map<Integer, String> knownProtocols) {
40+
this.knownPorts = knownPorts;
41+
this.knownProtocols = knownProtocols;
42+
}
43+
44+
public static DestinationValidator forProtocolMap(String[] protocolMappings) {
45+
if (protocolMappings == null) {
46+
protocolMappings = DEFAULT_PROTOCOL_TO_PORT_MAP;
47+
}
48+
49+
Map<String, Integer> knownPorts = new HashMap<>();
50+
Map<Integer, String> knownProtocols = new HashMap<>();
51+
52+
for (String protocolMapping : protocolMappings) {
53+
Matcher m = PROTOCOL_MAP_PATTERN.matcher(protocolMapping);
54+
if (m.matches()) {
55+
Integer port = Integer.valueOf(m.group(2));
56+
String proto = m.group(1);
57+
58+
knownPorts.put(proto, port);
59+
knownProtocols.put(port, proto);
60+
}
61+
}
62+
63+
return new DestinationValidator(knownPorts, knownProtocols);
64+
}
65+
66+
public boolean validate(String expectedDestination, String actualDestination) {
67+
try {
68+
return validate(expectedDestination == null ? null : URI.create(expectedDestination), actualDestination);
69+
} catch (IllegalArgumentException ex) {
70+
return false;
71+
}
72+
}
73+
74+
public boolean validate(String expectedDestination, URI actualDestination) {
75+
try {
76+
return validate(expectedDestination == null ? null : URI.create(expectedDestination), actualDestination);
77+
} catch (IllegalArgumentException ex) {
78+
return false;
79+
}
80+
}
81+
82+
public boolean validate(URI expectedDestination, String actualDestination) {
83+
try {
84+
return validate(expectedDestination, actualDestination == null ? null : URI.create(actualDestination));
85+
} catch (IllegalArgumentException ex) {
86+
return false;
87+
}
88+
}
89+
90+
public boolean validate(URI expectedDestination, URI actualDestination) {
91+
if (actualDestination == null) {
92+
return true; // destination is optional
93+
}
94+
95+
if (expectedDestination == null) {
96+
return false; // expected destination is mandatory
97+
}
98+
99+
if (Objects.equals(expectedDestination, actualDestination)) {
100+
return true;
101+
}
102+
103+
Integer portByScheme = knownPorts.get(expectedDestination.getScheme());
104+
String protocolByPort = knownProtocols.get(expectedDestination.getPort());
105+
106+
URI updatedUri = null;
107+
try {
108+
if (expectedDestination.getPort() < 0 && portByScheme != null) {
109+
updatedUri = new URI(
110+
expectedDestination.getScheme(),
111+
expectedDestination.getUserInfo(),
112+
expectedDestination.getHost(),
113+
portByScheme,
114+
expectedDestination.getPath(),
115+
expectedDestination.getQuery(),
116+
expectedDestination.getFragment()
117+
);
118+
} else if (expectedDestination.getPort() >= 0 && Objects.equals(protocolByPort, expectedDestination.getScheme())) {
119+
updatedUri = new URI(
120+
expectedDestination.getScheme(),
121+
expectedDestination.getUserInfo(),
122+
expectedDestination.getHost(),
123+
-1,
124+
expectedDestination.getPath(),
125+
expectedDestination.getQuery(),
126+
expectedDestination.getFragment()
127+
);
128+
}
129+
} catch (URISyntaxException ex) {
130+
return false;
131+
}
132+
133+
return Objects.equals(updatedUri, actualDestination);
134+
}
135+
136+
}

‎services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java

+6-3
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
import org.keycloak.rotation.HardcodedKeyLocator;
8888
import org.keycloak.rotation.KeyLocator;
8989
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
90+
import org.keycloak.saml.validators.DestinationValidator;
9091
import org.w3c.dom.Element;
9192

9293
import java.util.*;
@@ -111,6 +112,7 @@ public class SAMLEndpoint {
111112
protected SAMLIdentityProviderConfig config;
112113
protected IdentityProvider.AuthenticationCallback callback;
113114
protected SAMLIdentityProvider provider;
115+
private final DestinationValidator destinationValidator;
114116

115117
@Context
116118
private KeycloakSession session;
@@ -122,11 +124,12 @@ public class SAMLEndpoint {
122124
private HttpHeaders headers;
123125

124126

125-
public SAMLEndpoint(RealmModel realm, SAMLIdentityProvider provider, SAMLIdentityProviderConfig config, IdentityProvider.AuthenticationCallback callback) {
127+
public SAMLEndpoint(RealmModel realm, SAMLIdentityProvider provider, SAMLIdentityProviderConfig config, IdentityProvider.AuthenticationCallback callback, DestinationValidator destinationValidator) {
126128
this.realm = realm;
127129
this.config = config;
128130
this.callback = callback;
129131
this.provider = provider;
132+
this.destinationValidator = destinationValidator;
130133
}
131134

132135
@GET
@@ -238,7 +241,7 @@ protected Response handleSamlRequest(String samlRequest, String relayState) {
238241
SAMLDocumentHolder holder = extractRequestDocument(samlRequest);
239242
RequestAbstractType requestAbstractType = (RequestAbstractType) holder.getSamlObject();
240243
// validate destination
241-
if (requestAbstractType.getDestination() != null && !session.getContext().getUri().getAbsolutePath().equals(requestAbstractType.getDestination())) {
244+
if (! destinationValidator.validate(session.getContext().getUri().getAbsolutePath(), requestAbstractType.getDestination())) {
242245
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
243246
event.detail(Details.REASON, "invalid_destination");
244247
event.error(Errors.INVALID_SAML_RESPONSE);
@@ -456,7 +459,7 @@ public Response handleSamlResponse(String samlResponse, String relayState, Strin
456459
SAMLDocumentHolder holder = extractResponseDocument(samlResponse);
457460
StatusResponseType statusResponse = (StatusResponseType)holder.getSamlObject();
458461
// validate destination
459-
if (statusResponse.getDestination() != null && !session.getContext().getUri().getAbsolutePath().toString().equals(statusResponse.getDestination())) {
462+
if (! destinationValidator.validate(session.getContext().getUri().getAbsolutePath(), statusResponse.getDestination())) {
460463
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
461464
event.detail(Details.REASON, "invalid_destination");
462465
event.error(Errors.INVALID_SAML_RESPONSE);

‎services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java

+5-2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.keycloak.saml.common.constants.GeneralConstants;
3636
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
3737
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
38+
import org.keycloak.saml.validators.DestinationValidator;
3839
import org.keycloak.sessions.AuthenticationSessionModel;
3940

4041
import javax.ws.rs.core.MediaType;
@@ -50,13 +51,15 @@
5051
*/
5152
public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityProviderConfig> {
5253
protected static final Logger logger = Logger.getLogger(SAMLIdentityProvider.class);
53-
public SAMLIdentityProvider(KeycloakSession session, SAMLIdentityProviderConfig config) {
54+
private final DestinationValidator destinationValidator;
55+
public SAMLIdentityProvider(KeycloakSession session, SAMLIdentityProviderConfig config, DestinationValidator destinationValidator) {
5456
super(session, config);
57+
this.destinationValidator = destinationValidator;
5558
}
5659

5760
@Override
5861
public Object callback(RealmModel realm, AuthenticationCallback callback, EventBuilder event) {
59-
return new SAMLEndpoint(realm, this, getConfig(), callback);
62+
return new SAMLEndpoint(realm, this, getConfig(), callback, destinationValidator);
6063
}
6164

6265
@Override

‎services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java

+11-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
package org.keycloak.broker.saml;
1818

19+
import org.keycloak.Config.Scope;
1920
import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
2021
import org.keycloak.dom.saml.v2.metadata.EndpointType;
2122
import org.keycloak.dom.saml.v2.metadata.EntitiesDescriptorType;
@@ -29,6 +30,7 @@
2930
import org.keycloak.saml.common.exceptions.ParsingException;
3031
import org.keycloak.saml.common.util.DocumentUtil;
3132
import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
33+
import org.keycloak.saml.validators.DestinationValidator;
3234
import org.w3c.dom.Element;
3335

3436
import javax.xml.namespace.QName;
@@ -44,14 +46,16 @@ public class SAMLIdentityProviderFactory extends AbstractIdentityProviderFactory
4446

4547
public static final String PROVIDER_ID = "saml";
4648

49+
private DestinationValidator destinationValidator;
50+
4751
@Override
4852
public String getName() {
4953
return "SAML v2.0";
5054
}
5155

5256
@Override
5357
public SAMLIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
54-
return new SAMLIdentityProvider(session, new SAMLIdentityProviderConfig(model));
58+
return new SAMLIdentityProvider(session, new SAMLIdentityProviderConfig(model), destinationValidator);
5559
}
5660

5761
@Override
@@ -159,4 +163,10 @@ public String getId() {
159163
return PROVIDER_ID;
160164
}
161165

166+
@Override
167+
public void init(Scope config) {
168+
super.init(config);
169+
170+
this.destinationValidator = DestinationValidator.forProtocolMap(config.getArray("knownProtocols"));
171+
}
162172
}

‎services/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java

+4-29
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
package org.keycloak.protocol.saml;
1919

2020
import org.keycloak.Config;
21-
import org.keycloak.OAuth2Constants;
2221
import org.keycloak.events.EventBuilder;
2322
import org.keycloak.models.ClientModel;
2423
import org.keycloak.models.ClientScopeModel;
@@ -33,48 +32,31 @@
3332
import org.keycloak.protocol.saml.mappers.UserPropertyAttributeStatementMapper;
3433
import org.keycloak.representations.idm.CertificateRepresentation;
3534
import org.keycloak.representations.idm.ClientRepresentation;
36-
import org.keycloak.representations.idm.ClientScopeRepresentation;
3735
import org.keycloak.saml.SignatureAlgorithm;
3836
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
3937
import org.keycloak.saml.processing.core.saml.v2.constants.X500SAMLProfileConstants;
4038

39+
import org.keycloak.saml.validators.DestinationValidator;
4140
import javax.xml.crypto.dsig.CanonicalizationMethod;
4241
import java.util.ArrayList;
4342
import java.util.HashMap;
4443
import java.util.List;
4544
import java.util.Map;
46-
import java.util.regex.Matcher;
47-
import java.util.regex.Pattern;
4845

4946
/**
5047
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
5148
* @version $Revision: 1 $
5249
*/
5350
public class SamlProtocolFactory extends AbstractLoginProtocolFactory {
5451

55-
private static final Pattern PROTOCOL_MAP_PATTERN = Pattern.compile("\\s*([a-zA-Z][a-zA-Z\\d+-.]*)\\s*=\\s*(\\d+)\\s*");
56-
private static final String[] DEFAULT_PROTOCOL_TO_PORT_MAP = new String[] { "http=80", "https=443" };
57-
5852
public static final String SCOPE_ROLE_LIST = "role_list";
5953
private static final String ROLE_LIST_CONSENT_TEXT = "${samlRoleListScopeConsentText}";
6054

61-
private final Map<Integer, String> knownPorts = new HashMap<>();
62-
private final Map<String, Integer> knownProtocols = new HashMap<>();
63-
64-
private void addToProtocolPortMaps(String protocolMapping) {
65-
Matcher m = PROTOCOL_MAP_PATTERN.matcher(protocolMapping);
66-
if (m.matches()) {
67-
Integer port = Integer.valueOf(m.group(2));
68-
String proto = m.group(1);
69-
70-
knownPorts.put(port, proto);
71-
knownProtocols.put(proto, port);
72-
}
73-
}
55+
private DestinationValidator destinationValidator;
7456

7557
@Override
7658
public Object createProtocolEndpoint(RealmModel realm, EventBuilder event) {
77-
return new SamlService(realm, event, knownProtocols, knownPorts);
59+
return new SamlService(realm, event, destinationValidator);
7860
}
7961

8062
@Override
@@ -87,14 +69,7 @@ public void init(Config.Scope config) {
8769
//PicketLinkCoreSTS sts = PicketLinkCoreSTS.instance();
8870
//sts.installDefaultConfiguration();
8971

90-
String[] protocolMappings = config.getArray("knownProtocols");
91-
if (protocolMappings == null) {
92-
protocolMappings = DEFAULT_PROTOCOL_TO_PORT_MAP;
93-
}
94-
95-
for (String protocolMapping : protocolMappings) {
96-
addToProtocolPortMaps(protocolMapping);
97-
}
72+
this.destinationValidator = DestinationValidator.forProtocolMap(config.getArray("knownProtocols"));
9873
}
9974

10075
@Override

‎services/src/main/java/org/keycloak/protocol/saml/SamlService.java

+8-35
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@
8585
import org.keycloak.rotation.KeyLocator;
8686
import org.keycloak.saml.SPMetadataDescriptor;
8787
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
88+
import org.keycloak.saml.validators.DestinationValidator;
8889
import org.keycloak.sessions.AuthenticationSessionModel;
89-
import java.util.Map;
9090

9191
/**
9292
* Resource class for the saml connect token service
@@ -98,13 +98,11 @@ public class SamlService extends AuthorizationEndpointBase {
9898

9999
protected static final Logger logger = Logger.getLogger(SamlService.class);
100100

101-
private final Map<String, Integer> knownPorts;
102-
private final Map<Integer, String> knownProtocols;
101+
private final DestinationValidator destinationValidator;
103102

104-
public SamlService(RealmModel realm, EventBuilder event, Map<String, Integer> knownPorts, Map<Integer, String> knownProtocols) {
103+
public SamlService(RealmModel realm, EventBuilder event, DestinationValidator destinationValidator) {
105104
super(realm, event);
106-
this.knownPorts = knownPorts;
107-
this.knownProtocols = knownProtocols;
105+
this.destinationValidator = destinationValidator;
108106
}
109107

110108
public abstract class BindingProtocol {
@@ -147,7 +145,7 @@ protected Response handleSamlResponse(String samlResponse, String relayState) {
147145

148146
StatusResponseType statusResponse = (StatusResponseType) holder.getSamlObject();
149147
// validate destination
150-
if (statusResponse.getDestination() != null && !session.getContext().getUri().getAbsolutePath().toString().equals(statusResponse.getDestination())) {
148+
if (! destinationValidator.validate(session.getContext().getUri().getAbsolutePath(), statusResponse.getDestination())) {
151149
event.detail(Details.REASON, "invalid_destination");
152150
event.error(Errors.INVALID_SAML_LOGOUT_RESPONSE);
153151
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
@@ -272,7 +270,7 @@ protected Response loginRequest(String relayState, AuthnRequestType requestAbstr
272270
event.error(Errors.INVALID_SAML_AUTHN_REQUEST);
273271
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
274272
}
275-
if (! isValidDestination(requestAbstractType.getDestination())) {
273+
if (! destinationValidator.validate(session.getContext().getUri().getAbsolutePath(), requestAbstractType.getDestination())) {
276274
event.detail(Details.REASON, "invalid_destination");
277275
event.error(Errors.INVALID_SAML_AUTHN_REQUEST);
278276
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
@@ -376,7 +374,7 @@ protected Response logoutRequest(LogoutRequestType logoutRequest, ClientModel cl
376374
event.error(Errors.INVALID_SAML_LOGOUT_REQUEST);
377375
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
378376
}
379-
if (! isValidDestination(logoutRequest.getDestination())) {
377+
if (! destinationValidator.validate(logoutRequest.getDestination(), session.getContext().getUri().getAbsolutePath())) {
380378
event.detail(Details.REASON, "invalid_destination");
381379
event.error(Errors.INVALID_SAML_LOGOUT_REQUEST);
382380
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
@@ -696,35 +694,10 @@ public AuthenticationSessionModel getOrCreateLoginSessionForIdpInitiatedSso(Keyc
696694
@NoCache
697695
@Consumes({"application/soap+xml",MediaType.TEXT_XML})
698696
public Response soapBinding(InputStream inputStream) {
699-
SamlEcpProfileService bindingService = new SamlEcpProfileService(realm, event, knownPorts, knownProtocols);
697+
SamlEcpProfileService bindingService = new SamlEcpProfileService(realm, event, destinationValidator);
700698

701699
ResteasyProviderFactory.getInstance().injectProperties(bindingService);
702700

703701
return bindingService.authenticate(inputStream);
704702
}
705-
706-
private boolean isValidDestination(URI destination) {
707-
if (destination == null) {
708-
return true; // destination is optional
709-
}
710-
711-
URI expected = session.getContext().getUri().getAbsolutePath();
712-
713-
if (Objects.equals(expected, destination)) {
714-
return true;
715-
}
716-
717-
Integer portByScheme = knownPorts.get(expected.getScheme());
718-
if (expected.getPort() < 0 && portByScheme != null) {
719-
return Objects.equals(session.getContext().getUri().getRequestUriBuilder().port(portByScheme).build(), destination);
720-
}
721-
722-
String protocolByPort = knownProtocols.get(expected.getPort());
723-
if (expected.getPort() >= 0 && Objects.equals(protocolByPort, expected.getScheme())) {
724-
return Objects.equals(session.getContext().getUri().getRequestUriBuilder().port(-1).build(), destination);
725-
}
726-
727-
return false;
728-
}
729-
730703
}

‎services/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
3636
import org.keycloak.saml.common.exceptions.ConfigurationException;
3737
import org.keycloak.saml.common.exceptions.ProcessingException;
38+
import org.keycloak.saml.validators.DestinationValidator;
3839
import org.keycloak.sessions.AuthenticationSessionModel;
3940
import org.w3c.dom.Document;
4041

@@ -54,8 +55,8 @@ public class SamlEcpProfileService extends SamlService {
5455
private static final String NS_PREFIX_SAML_PROTOCOL = "samlp";
5556
private static final String NS_PREFIX_SAML_ASSERTION = "saml";
5657

57-
public SamlEcpProfileService(RealmModel realm, EventBuilder event, Map<String, Integer> knownPorts, Map<Integer, String> knownProtocols) {
58-
super(realm, event, knownPorts, knownProtocols);
58+
public SamlEcpProfileService(RealmModel realm, EventBuilder event, DestinationValidator destinationValidator) {
59+
super(realm, event, destinationValidator);
5960
}
6061

6162
public Response authenticate(InputStream inputStream) {

0 commit comments

Comments
 (0)
Please sign in to comment.