use of it.unibo.arces.wot.sepa.commons.exceptions.SEPASecurityException in project SEPA by arces-wot.
the class SPARQL11SEProtocol method executeSPARQL11SEPrimitive.
protected Response executeSPARQL11SEPrimitive(SPARQL11SEPrimitive op, Object request) {
// Create the HTTPS request
URI uri;
String path = null;
int port = 0;
// Headers and body
String contentType = null;
ByteArrayEntity body = null;
String accept = null;
String authorization = null;
switch(op) {
case SUBSCRIBE:
SubscribeRequest subscribe = (SubscribeRequest) request;
return wsClient.subscribe(subscribe.getSPARQL());
case UNSUBSCRIBE:
UnsubscribeRequest unsubscribe = (UnsubscribeRequest) request;
return wsClient.unsubscribe(unsubscribe.getSubscribeUUID());
// }
default:
break;
}
switch(op) {
case REGISTER:
path = properties.getRegisterPath();
port = properties.getHttpsPort();
accept = "application/json";
contentType = "application/json";
String identity = (String) request;
try {
body = new ByteArrayEntity(new RegistrationRequest(identity).toString().getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
return new ErrorResponse(500, e.getMessage());
}
break;
case REQUESTTOKEN:
String basic;
try {
basic = properties.getBasicAuthorization();
} catch (SEPASecurityException e2) {
return new ErrorResponse(500, e2.getMessage());
}
if (basic == null)
return new ErrorResponse(0, 401, "Basic authorization in null. Register first");
path = properties.getTokenRequestPath();
port = properties.getHttpsPort();
authorization = "Basic " + basic;
contentType = "application/json";
accept = "application/json";
break;
case SECUREUPDATE:
path = properties.getSecurePath() + properties.getUpdatePath();
port = properties.getHttpsPort();
accept = "text/plain";
contentType = "application/x-www-form-urlencoded";
try {
authorization = "Bearer " + properties.getAccessToken();
} catch (SEPASecurityException e2) {
return new ErrorResponse(500, e2.getMessage());
}
String encodedContent;
try {
encodedContent = URLEncoder.encode(((UpdateRequest) request).getSPARQL(), "UTF-8");
} catch (UnsupportedEncodingException e) {
return new ErrorResponse(500, e.getMessage());
}
body = new ByteArrayEntity(("update=" + encodedContent).getBytes());
body.setContentType(contentType);
break;
case SECUREQUERY:
path = properties.getSecurePath() + properties.getQueryPath();
port = properties.getHttpsPort();
accept = "application/sparql-results+json";
contentType = "application/sparql-query";
try {
authorization = "Bearer " + properties.getAccessToken();
} catch (SEPASecurityException e2) {
return new ErrorResponse(500, e2.getMessage());
}
try {
body = new ByteArrayEntity(((QueryRequest) request).getSPARQL().getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
return new ErrorResponse(500, e.getMessage());
}
break;
default:
break;
}
// POST request
try {
uri = new URI("https", null, properties.getHost(), port, path, null, null);
} catch (URISyntaxException e1) {
return new ErrorResponse(500, e1.getMessage());
}
HttpUriRequest httpRequest = new HttpPost(uri);
if (contentType != null)
httpRequest.setHeader("Content-Type", contentType);
if (accept != null)
httpRequest.setHeader("Accept", accept);
if (authorization != null)
httpRequest.setHeader("Authorization", authorization);
if (body != null)
((HttpPost) httpRequest).setEntity(body);
logger.debug("Request: " + httpRequest);
// HTTP request execution
CloseableHttpClient httpclient;
try {
httpclient = new SSLSecurityManager("TLSv1", "sepa.jks", "sepa2017", "sepa2017").getSSLHttpClient();
} catch (KeyManagementException | UnrecoverableKeyException | NoSuchAlgorithmException | KeyStoreException | CertificateException | IOException e) {
return new ErrorResponse(500, e.getMessage());
}
CloseableHttpResponse response = null;
String jsonResponse = null;
try {
long timing = System.nanoTime();
try {
response = httpclient.execute(httpRequest);
} catch (IOException e) {
return new ErrorResponse(500, e.getMessage());
}
timing = System.nanoTime() - timing;
logger.debug("Response: " + response);
if (op.equals(SPARQL11SEPrimitive.REGISTER))
logger.debug("REGISTER " + timing / 1000000 + " ms");
else if (op.equals(SPARQL11SEPrimitive.REQUESTTOKEN))
logger.debug("TOKEN " + timing / 1000000 + " ms");
else if (op.equals(SPARQL11SEPrimitive.SECUREQUERY))
logger.debug("SECURE_QUERY " + timing / 1000000 + " ms");
else if (op.equals(SPARQL11SEPrimitive.SECUREUPDATE))
logger.debug("SECURE_UPDATE " + timing / 1000000 + " ms");
HttpEntity entity = response.getEntity();
try {
jsonResponse = EntityUtils.toString(entity, Charset.forName("UTF-8"));
} catch (ParseException | IOException e) {
return new ErrorResponse(500, e.getMessage());
}
try {
EntityUtils.consume(entity);
} catch (IOException e) {
return new ErrorResponse(500, e.getMessage());
}
} finally {
try {
response.close();
} catch (IOException e) {
return new ErrorResponse(500, e.getMessage());
}
}
// Parsing the response
try {
return parseSPARQL11SEResponse(jsonResponse, op);
} catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException | BadPaddingException e) {
return new ErrorResponse(500, e.getMessage());
}
}
use of it.unibo.arces.wot.sepa.commons.exceptions.SEPASecurityException in project SEPA by arces-wot.
the class SEPATest method main.
public static void main(String[] args) {
boolean ret = false;
try {
properties = new SPARQL11SEProperties("client.jpar");
} catch (SEPAPropertiesException e2) {
logger.fatal("JSAP exception: " + e2.getMessage());
System.exit(1);
}
try {
client = new SPARQL11SEProtocol(properties, handler);
} catch (SEPAProtocolException e2) {
logger.fatal(e2.getLocalizedMessage());
System.exit(1);
}
logger.warn("**********************************************************");
logger.warn("*** SPARQL 1.1 SE Protocol Service test suite ***");
logger.warn("**********************************************************");
logger.warn("*** WARNING: the RDF store content will be ERASED ***");
logger.warn("*** Do you want to continue (yes/no)? ***");
logger.warn("**********************************************************");
logger.warn("SPARQL 1.1 SE Protocol Service parameters: " + client.toString());
Scanner scanner = new Scanner(System.in);
// "\\z" means end of input
scanner.useDelimiter("\\n");
String input = scanner.next();
if (!input.equals("yes")) {
scanner.close();
logger.info("Bye bye! :-)");
System.exit(0);
}
logger.warn("**********************************************************");
logger.warn("*** Are you sure (yes/no)? ***");
logger.warn("**********************************************************");
input = scanner.next();
if (!input.equals("yes")) {
scanner.close();
logger.info("Bye bye! :-)");
System.exit(0);
}
scanner.close();
// UPDATE
ret = updateTest("prefix test:<http://www.vaimee.com/test#> delete {?s ?p ?o} insert {test:Sub test:Pred \"測試\"} where {?s ?p ?o}", false);
results.addResult("Update", ret);
if (ret)
logger.info("Update PASSED");
else
logger.error("Update FAILED");
// QUERY
ret = queryTest("select ?o where {?s ?p ?o}", "測試", false);
results.addResult("Query", ret);
if (ret)
logger.info("Query PASSED");
else
logger.error("Query FAILED");
// SUBSCRIBE
ret = subscribeTest("select ?o where {?s ?p ?o}", false);
results.addResult("Subscribe - request", ret);
if (ret)
logger.info("Subscribe PASSED");
else
logger.error("Subscribe FAILED");
// PING
ret = waitPing();
results.addResult("Subscribe - ping", ret);
if (ret)
logger.info("Ping received PASSED");
else
logger.error("Ping recevied FAILED");
// TRIGGER A NOTIFICATION
ret = updateTest("prefix test:<http://www.vaimee.com/test#> delete {?s ?p ?o} insert {test:Sub test:Pred \"ვაიმეე\"} where {?s ?p ?o}", false);
results.addResult("Subscribe - triggering", ret);
if (ret)
logger.info("Triggering update PASSED");
else
logger.error("Triggering update FAILED");
// WAIT NOTIFICATION
ret = waitNotification();
results.addResult("Subscribe - notification", ret);
if (ret)
logger.info("Notification PASSED");
else
logger.error("Notification FAILED");
// UNSUBSCRIBE
ret = unsubscribeTest(spuid, false);
results.addResult("Unsubscribe - request", ret);
if (ret)
logger.info("Unsubscribe PASSED");
else
logger.error("Unsubscribe FAILED");
// PING
ret = !waitPing();
results.addResult("Unsubscribe - ping", ret);
if (ret)
logger.info("Ping not received PASSED");
else
logger.error("Ping not recevied FAILED");
// **********************
// Enable security
// **********************
logger.debug("Switch to secure mode");
// REGISTRATION (registration not allowed)
ret = !registrationTest("RegisterMePlease");
results.addResult("Registration not allowed", ret);
if (ret)
logger.info("Registration not allowed PASSED");
else
logger.error("Registration not allowed FAILED");
// REGISTRATION
ret = registrationTest("SEPATest");
results.addResult("Registration", ret);
if (ret)
logger.info("Registration PASSED");
else
logger.error("Registration FAILED");
// REQUEST ACCESS TOKEN
ret = requestAccessTokenTest();
results.addResult("Access token", ret);
if (ret)
logger.info("Access token PASSED");
else
logger.error("Access token FAILED");
// REQUEST ACCESS TOKEN (not expired);
try {
if (!properties.isTokenExpired())
ret = !requestAccessTokenTest();
else
ret = false;
} catch (SEPASecurityException e2) {
logger.error(e2.getMessage());
ret = false;
}
results.addResult("Access token not expired", ret);
if (ret)
logger.info("Access token (not expired) PASSED");
else
logger.error("Access token (not expired) FAILED");
// REQUEST ACCESS TOKEN (expired);
try {
logger.debug("Waiting token expiring in " + properties.getExpiringSeconds() + " + 2 seconds...");
} catch (SEPASecurityException e2) {
logger.error(e2.getMessage());
}
try {
Thread.sleep((properties.getExpiringSeconds() + 2) * 1000);
} catch (InterruptedException | SEPASecurityException e) {
logger.error(e.getMessage());
}
try {
if (properties.isTokenExpired())
ret = requestAccessTokenTest();
else
ret = false;
} catch (SEPASecurityException e2) {
logger.error(e2.getMessage());
ret = false;
}
results.addResult("Access token expired", ret);
if (ret)
logger.info("Access token (expired) PASSED");
else
logger.error("Access token (expired) FAILED");
// SECURE UPDATE
try {
logger.debug("Waiting token expiring in " + properties.getExpiringSeconds() + " + 2 seconds...");
} catch (SEPASecurityException e2) {
logger.error(e2.getMessage());
}
try {
Thread.sleep((properties.getExpiringSeconds() + 2) * 1000);
} catch (InterruptedException | SEPASecurityException e) {
logger.error(e.getMessage());
}
try {
if (properties.isTokenExpired())
requestAccessTokenTest();
} catch (SEPASecurityException e13) {
logger.error(e13.getMessage());
}
ret = updateTest("prefix test:<http://wot.arces.unibo.it/test#> delete {?s ?p ?o} insert {test:Sub test:Pred \"ვაიმეე\"} where {?s ?p ?o}", true);
results.addResult("Secure update ", ret);
if (ret)
logger.info("Secure update PASSED");
else
logger.error("Secure update FAILED");
// SECURE UPDATE (expired token)
try {
logger.debug("Waiting token expiring in " + properties.getExpiringSeconds() + " + 2 seconds...");
} catch (SEPASecurityException e12) {
logger.error(e12.getMessage());
}
try {
Thread.sleep((properties.getExpiringSeconds() + 2) * 1000);
} catch (InterruptedException | SEPASecurityException e) {
logger.error(e.getMessage());
}
ret = !updateTest("prefix test:<http://wot.arces.unibo.it/test#> delete {?s ?p ?o} insert {test:Sub test:Pred \"vaimee!\"} where {?s ?p ?o}", true);
results.addResult("Secure update (expired)", ret);
if (ret)
logger.info("Secure update (expired) PASSED");
else
logger.error("Secure update (expired) FAILED");
// SECURE QUERY
try {
logger.debug("Waiting token expiring in " + properties.getExpiringSeconds() + " + 2 seconds...");
} catch (SEPASecurityException e11) {
logger.error(e11.getMessage());
}
try {
Thread.sleep((properties.getExpiringSeconds() + 2) * 1000);
} catch (InterruptedException | SEPASecurityException e) {
logger.error(e.getMessage());
}
try {
if (properties.isTokenExpired())
requestAccessTokenTest();
} catch (SEPASecurityException e10) {
logger.error(e10.getMessage());
}
ret = queryTest("select ?o where {?s ?p ?o}", "ვაიმეე", true);
results.addResult("Secure query", ret);
if (ret)
logger.info("Secure query PASSED");
else
logger.error("Secure query FAILED");
// SECURE QUERY (expired token)
try {
logger.debug("Waiting token expiring in " + properties.getExpiringSeconds() + " + 2 seconds...");
} catch (SEPASecurityException e9) {
logger.error(e9.getMessage());
}
try {
Thread.sleep((properties.getExpiringSeconds() + 2) * 1000);
} catch (InterruptedException | SEPASecurityException e) {
logger.error(e.getMessage());
}
ret = !queryTest("select ?o where {?s ?p ?o}", "ვაიმეე", true);
results.addResult("Secure query (expired)", ret);
if (ret)
logger.info("Secure query (expired) PASSED");
else
logger.error("Secure query (expired) FAILED");
// SECURE SUBSCRIBE
try {
logger.debug("Waiting token expiring in " + properties.getExpiringSeconds() + " + 2 seconds...");
} catch (SEPASecurityException e8) {
logger.error(e8.getMessage());
}
try {
Thread.sleep((properties.getExpiringSeconds() + 2) * 1000);
} catch (InterruptedException | SEPASecurityException e) {
logger.error(e.getMessage());
}
try {
if (properties.isTokenExpired())
requestAccessTokenTest();
} catch (SEPASecurityException e7) {
logger.error(e7.getMessage());
}
ret = subscribeTest("select ?o where {?s ?p ?o}", true);
results.addResult("Secure subscribe - request", ret);
if (ret)
logger.info("Secure subscribe PASSED");
else
logger.error("Secure subscribe FAILED");
// PING
ret = waitPing();
results.addResult("Secure subscribe - ping", ret);
if (ret)
logger.info("Secure ping received PASSED");
else
logger.error("Secure ping recevied FAILED");
// TRIGGER A NOTIFICATION
try {
logger.debug("Waiting token expiring in " + properties.getExpiringSeconds() + " + 2 seconds...");
} catch (SEPASecurityException e6) {
logger.error(e6.getMessage());
}
try {
Thread.sleep((properties.getExpiringSeconds() + 2) * 1000);
} catch (InterruptedException | SEPASecurityException e) {
logger.error(e.getMessage());
}
try {
if (properties.isTokenExpired())
requestAccessTokenTest();
} catch (SEPASecurityException e5) {
logger.error(e5.getMessage());
}
ret = updateTest("prefix test:<http://wot.arces.unibo.it/test#> delete {?s ?p ?o} insert {test:Sub test:Pred \"卢卡\"} where {?s ?p ?o}", true);
results.addResult("Secure subscribe - triggering", ret);
if (ret)
logger.info("Secure triggering update PASSED");
else
logger.error("Secure triggering update FAILED");
// NOTIFICATION
ret = waitNotification();
results.addResult("Secure subscribe - notification", ret);
if (ret)
logger.info("Secure subscribe - notification PASSED");
else
logger.error("Secure subscribe - notification FAILED");
// SECURE UNSUBSCRIBE (expired)
try {
logger.debug("Wait token expiring in " + properties.getExpiringSeconds() + " + 2 seconds...");
} catch (SEPASecurityException e4) {
logger.error(e4.getMessage());
}
try {
Thread.sleep((properties.getExpiringSeconds() + 2) * 1000);
} catch (InterruptedException | SEPASecurityException e) {
logger.error(e.getMessage());
}
ret = !unsubscribeTest(spuid, true);
results.addResult("Secure unsubscribe (expired) - request", ret);
if (ret)
logger.info("Secure unsubscribe (expired) - request PASSED");
else
logger.error("Secure unsubscribe (expired) - request FAILED");
// UNSUBSCRIBE
try {
logger.debug("Waiting token expiring in " + properties.getExpiringSeconds() + " + 2 seconds...");
} catch (SEPASecurityException e3) {
logger.error(e3.getMessage());
}
try {
Thread.sleep((properties.getExpiringSeconds() + 2) * 1000);
} catch (InterruptedException | SEPASecurityException e) {
logger.error(e.getMessage());
}
try {
if (properties.isTokenExpired())
requestAccessTokenTest();
} catch (SEPASecurityException e2) {
logger.error(e2.getMessage());
}
ret = unsubscribeTest(spuid, true);
results.addResult("Secure unsubscribe - request", ret);
if (ret)
logger.info("Secure unsubscribe - request PASSED");
else
logger.error("Secure unsubscribe - request FAILED");
// WAITING PING
ret = !waitPing();
results.addResult("Secure unsubscribe - ping", ret);
if (ret)
logger.info("Secure unsubscribe - ping PASSED");
else
logger.error("Secure unsubscribe - ping FAILED");
// SECURE SUBSCRIBE (expired)
try {
logger.info("Wait token expiring in " + properties.getExpiringSeconds() + " + 2 seconds...");
} catch (SEPASecurityException e1) {
logger.error(e1.getMessage());
}
try {
Thread.sleep((properties.getExpiringSeconds() + 2) * 1000);
} catch (InterruptedException | SEPASecurityException e) {
logger.error(e.getMessage());
}
ret = !subscribeTest("select ?o where {?s ?p ?o}", true);
results.addResult("Secure subscribe (expired) - request", ret);
if (ret)
logger.info("Secure subscribe (expired) - request PASSED");
else
logger.error("Secure subscribe (expired) - request FAILED");
results.print();
System.exit(0);
}
use of it.unibo.arces.wot.sepa.commons.exceptions.SEPASecurityException in project SEPA by arces-wot.
the class Gate method authorize.
/**
* <pre>
* Specific to SPARQL 1.1 SE Subscribe request:
* 1. Check if the request contains an "authorization" member.
* 2. Check if the request contains an "authorization" member that start with "Bearer"
* 3. Check if the value of the "authorization" member is a JWT object ==> VALIDATE TOKEN
*
* Token validation:
* 4. Check if the JWT object is signed
* 5. Check if the signature of the JWT object is valid. This is to be checked with AS public signature verification key
* 6. Check the contents of the JWT object
* 7. Check if the value of "iss" is https://wot.arces.unibo.it:8443/oauth/token
* 8. Check if the value of "aud" contains https://wot.arces.unibo.it:8443/sparql
* 9. Accept the request as well as "sub" as the originator of the request and process it as usual
*
* Respond with 401 if not
*
* According to RFC6749, the error member can assume the following values: invalid_request, invalid_client, invalid_grant, unauthorized_client, unsupported_grant_type, invalid_scope.
*
* invalid_request
* The request is missing a required parameter, includes an
* unsupported parameter value (other than grant type),
* repeats a parameter, includes multiple credentials,
* utilizes more than one mechanism for authenticating the
* client, or is otherwise malformed.
*
* invalid_client
* Client authentication failed (e.g., unknown client, no
* client authentication included, or unsupported
* authentication method). The authorization server MAY
* return an HTTP 401 (Unauthorized) status code to indicate
* which HTTP authentication schemes are supported. If the
* client attempted to authenticate via the "Authorization"
* request header field, the authorization server MUST
* respond with an HTTP 401 (Unauthorized) status code and
* include the "WWW-Authenticate" response header field
* matching the authentication scheme used by the client.
*
* invalid_grant
* The provided authorization grant (e.g., authorization
* code, resource owner credentials) or refresh token is
* invalid, expired, revoked, does not match the redirection
* URI used in the authorization request, or was issued to
* another client.
*
* unauthorized_client
* The authenticated client is not authorized to use this
* authorization grant type.
*
* unsupported_grant_type
* The authorization grant type is not supported by the
* authorization server.
*
* </pre>
* @throws SEPASecurityException
*/
protected final ClientAuthorization authorize(String message) throws SEPASecurityException {
if (!authorizationRequired)
return new ClientAuthorization();
JsonObject request;
try {
request = new JsonParser().parse(message).getAsJsonObject();
} catch (Exception e) {
logger.error(e.getMessage());
return new ClientAuthorization("invalid_request", "Failed to parse JSON message: " + message);
}
String bearer = null;
JsonObject subUnsub = null;
if (request.has("subscribe"))
subUnsub = request.get("subscribe").getAsJsonObject();
else if (request.has("unsubscribe"))
subUnsub = request.get("unsubscribe").getAsJsonObject();
if (subUnsub == null) {
logger.error("Neither subscribe or unsuscribe found");
return new ClientAuthorization("invalid_request", "Neither subscribe or unsuscribe found");
}
if (!subUnsub.has("authorization")) {
logger.error("authorization member is missing");
return new ClientAuthorization("invalid_request", "authorization member is missing");
}
try {
bearer = subUnsub.get("authorization").getAsString();
} catch (Exception e) {
logger.error("Authorization member is not a string");
return new ClientAuthorization("invalid_request", "authorization member is not a string");
}
if (!bearer.toUpperCase().startsWith("BEARER ")) {
logger.error("Authorization value MUST be of type Bearer");
return new ClientAuthorization("unsupported_grant_type", "Authorization value MUST be of type Bearer");
}
String jwt = bearer.substring(7);
if (jwt == null) {
logger.error("Token is null");
return new ClientAuthorization("invalid_request", "Token is null");
}
if (jwt.equals("")) {
logger.error("Token is empty");
return new ClientAuthorization("invalid_request", "Token is empty");
}
// Token validation
return Dependability.validateToken(jwt);
}
use of it.unibo.arces.wot.sepa.commons.exceptions.SEPASecurityException in project SEPA by arces-wot.
the class WebsocketServer method onMessage.
@Override
public void onMessage(WebSocket conn, String message) {
GateBeans.onMessage();
// Check path
if (!conn.getResourceDescriptor().equals(path)) {
logger.warn("@onMessage bad resource descriptor: " + conn.getResourceDescriptor() + " Use: " + path);
ErrorResponse response = new ErrorResponse(HttpStatus.SC_NOT_FOUND, "wrong_path", "Bad resource descriptor: " + conn.getResourceDescriptor() + " Use: " + path);
try {
conn.send(response.toString());
} catch (Exception e) {
logger.warn(e.getMessage());
}
return;
}
synchronized (gates) {
try {
if (gates.get(conn) != null)
gates.get(conn).onMessage(message);
else {
logger.error("Gate NOT FOUND: " + conn);
}
} catch (SEPAProtocolException | SEPASecurityException | SEPASparqlParsingException e) {
logger.error(e);
ErrorResponse response = new ErrorResponse(HttpStatus.SC_BAD_REQUEST, "parsing failed", e.getMessage());
try {
conn.send(response.toString());
} catch (Exception e1) {
logger.warn(e1.getMessage());
}
}
}
}
use of it.unibo.arces.wot.sepa.commons.exceptions.SEPASecurityException in project SEPA by arces-wot.
the class Processor method processUpdate.
public synchronized Response processUpdate(InternalUpdateRequest update) {
InternalUpdateRequest preRequest = update;
if (spuManager.doUpdateARQuadsExtraction(update)) {
try {
preRequest = ARQuadsAlgorithm.extractARQuads(update, queryProcessor);
} catch (SEPAProcessingException | SPARQL11ProtocolException | SEPASparqlParsingException e) {
return new ErrorResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, "update_processing", e.getMessage());
}
}
// PRE-UPDATE processing
spuManager.subscriptionsProcessingPreUpdate(preRequest);
// Endpoint UPDATE
Response ret;
try {
ret = updateEndpoint(preRequest);
} catch (SEPASecurityException | IOException e) {
return new ErrorResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, "sparql11endpoint", e.getMessage());
}
// STOP processing?
if (ret.isError()) {
logger.error("*** UPDATE ENDPOINT PROCESSING FAILED *** " + ret);
spuManager.abortSubscriptionsProcessing();
return ret;
}
// POST-UPDATE processing
spuManager.subscriptionsProcessingPostUpdate(ret);
return ret;
}
Aggregations