Add authorization handler.

This commit is contained in:
Anatoly Sablin
2019-10-01 23:52:01 +03:00
parent 5521c0c338
commit bc8795e940
10 changed files with 154 additions and 59 deletions

View File

@@ -23,6 +23,7 @@ package io.kamax.mxisd;
import io.kamax.mxisd.config.MatrixConfig; import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.config.MxisdConfig; import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.http.undertow.handler.ApiHandler; import io.kamax.mxisd.http.undertow.handler.ApiHandler;
import io.kamax.mxisd.http.undertow.handler.AuthorizationHandler;
import io.kamax.mxisd.http.undertow.handler.InternalInfoHandler; import io.kamax.mxisd.http.undertow.handler.InternalInfoHandler;
import io.kamax.mxisd.http.undertow.handler.OptionsHandler; import io.kamax.mxisd.http.undertow.handler.OptionsHandler;
import io.kamax.mxisd.http.undertow.handler.SaneHandler; import io.kamax.mxisd.http.undertow.handler.SaneHandler;
@@ -87,15 +88,10 @@ public class HttpMxisd {
public void start() { public void start() {
m.start(); m.start();
HttpHandler helloHandler = SaneHandler.around(new HelloHandler());
HttpHandler asUserHandler = SaneHandler.around(new AsUserHandler(m.getAs())); HttpHandler asUserHandler = SaneHandler.around(new AsUserHandler(m.getAs()));
HttpHandler asTxnHandler = SaneHandler.around(new AsTransactionHandler(m.getAs())); HttpHandler asTxnHandler = SaneHandler.around(new AsTransactionHandler(m.getAs()));
HttpHandler asNotFoundHandler = SaneHandler.around(new AsNotFoundHandler(m.getAs())); HttpHandler asNotFoundHandler = SaneHandler.around(new AsNotFoundHandler(m.getAs()));
HttpHandler storeInvHandler = SaneHandler
.around(new StoreInviteHandler(m.getConfig().getServer(), m.getInvite(), m.getKeyManager()));
final RoutingHandler handler = Handlers.routing() final RoutingHandler handler = Handlers.routing()
.add("OPTIONS", "/**", SaneHandler.around(new OptionsHandler())) .add("OPTIONS", "/**", SaneHandler.around(new OptionsHandler()))
@@ -110,8 +106,10 @@ public class HttpMxisd {
// Account endpoints // Account endpoints
.post(AccountRegisterHandler.Path, SaneHandler.around(new AccountRegisterHandler(m.getAccMgr()))) .post(AccountRegisterHandler.Path, SaneHandler.around(new AccountRegisterHandler(m.getAccMgr())))
.get(AccountGetUserInfoHandler.Path, SaneHandler.around(new AccountGetUserInfoHandler(m.getAccMgr()))) .get(AccountGetUserInfoHandler.Path,
.post(AccountLogoutHandler.Path, SaneHandler.around(new AccountLogoutHandler(m.getAccMgr()))) SaneHandler.around(AuthorizationHandler.around(m.getAccMgr(), new AccountGetUserInfoHandler(m.getAccMgr()))))
.post(AccountLogoutHandler.Path,
SaneHandler.around(AuthorizationHandler.around(m.getAccMgr(), new AccountLogoutHandler(m.getAccMgr()))))
// Directory endpoints // Directory endpoints
.post(UserDirectorySearchHandler.Path, SaneHandler.around(new UserDirectorySearchHandler(m.getDirectory()))) .post(UserDirectorySearchHandler.Path, SaneHandler.around(new UserDirectorySearchHandler(m.getDirectory())))
@@ -155,7 +153,7 @@ public class HttpMxisd {
} }
private void keyEndpoints(RoutingHandler routingHandler) { private void keyEndpoints(RoutingHandler routingHandler) {
addEndpoints(routingHandler, Methods.GET, addEndpoints(routingHandler, Methods.GET, false,
new KeyGetHandler(m.getKeyManager()), new KeyGetHandler(m.getKeyManager()),
new RegularKeyIsValidHandler(m.getKeyManager()), new RegularKeyIsValidHandler(m.getKeyManager()),
new EphemeralKeyIsValidHandler(m.getKeyManager()) new EphemeralKeyIsValidHandler(m.getKeyManager())
@@ -163,14 +161,17 @@ public class HttpMxisd {
} }
private void identityEndpoints(RoutingHandler routingHandler) { private void identityEndpoints(RoutingHandler routingHandler) {
// Legacy v1
routingHandler.get(SingleLookupHandler.Path, sane(new SingleLookupHandler(m.getConfig(), m.getIdentity(), m.getSign()))); routingHandler.get(SingleLookupHandler.Path, sane(new SingleLookupHandler(m.getConfig(), m.getIdentity(), m.getSign())));
routingHandler.post(BulkLookupHandler.Path, sane(new BulkLookupHandler(m.getIdentity()))); routingHandler.post(BulkLookupHandler.Path, sane(new BulkLookupHandler(m.getIdentity())));
addEndpoints(routingHandler, Methods.GET,
new HelloHandler(), addEndpoints(routingHandler, Methods.GET, false, new HelloHandler());
addEndpoints(routingHandler, Methods.GET, true,
new SessionValidationGetHandler(m.getSession(), m.getConfig()), new SessionValidationGetHandler(m.getSession(), m.getConfig()),
new SessionTpidGetValidatedHandler(m.getSession()) new SessionTpidGetValidatedHandler(m.getSession())
); );
addEndpoints(routingHandler, Methods.POST, addEndpoints(routingHandler, Methods.POST, true,
new StoreInviteHandler(m.getConfig().getServer(), m.getInvite(), m.getKeyManager()), new StoreInviteHandler(m.getConfig().getServer(), m.getInvite(), m.getKeyManager()),
new SessionStartHandler(m.getSession()), new SessionStartHandler(m.getSession()),
new SessionValidationPostHandler(m.getSession()), new SessionValidationPostHandler(m.getSession()),
@@ -180,19 +181,20 @@ public class HttpMxisd {
); );
} }
private void addEndpoints(RoutingHandler routingHandler, HttpString method, ApiHandler... handlers) { private void addEndpoints(RoutingHandler routingHandler, HttpString method, boolean useAuthorization, ApiHandler... handlers) {
for (ApiHandler handler : handlers) { for (ApiHandler handler : handlers) {
attachHandler(routingHandler, method, handler, sane(handler)); attachHandler(routingHandler, method, handler, useAuthorization, sane(handler));
} }
} }
private void attachHandler(RoutingHandler routingHandler, HttpString method, ApiHandler apiHandler, HttpHandler httpHandler) { private void attachHandler(RoutingHandler routingHandler, HttpString method, ApiHandler apiHandler, boolean useAuthorization, HttpHandler httpHandler) {
final MatrixConfig matrixConfig = m.getConfig().getMatrix(); MatrixConfig matrixConfig = m.getConfig().getMatrix();
if (matrixConfig.isV1()) { if (matrixConfig.isV1()) {
routingHandler.add(method, apiHandler.getPath(IdentityServiceAPI.V1), httpHandler); routingHandler.add(method, apiHandler.getPath(IdentityServiceAPI.V1), httpHandler);
} }
if (matrixConfig.isV2()) { if (matrixConfig.isV2()) {
routingHandler.add(method, apiHandler.getPath(IdentityServiceAPI.V2), httpHandler); HttpHandler wrappedHandler = useAuthorization ? AuthorizationHandler.around(m.getAccMgr(), httpHandler) : httpHandler;
routingHandler.add(method, apiHandler.getPath(IdentityServiceAPI.V2), wrappedHandler);
} }
} }

View File

@@ -8,6 +8,7 @@ import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.exception.BadRequestException; import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.exception.InvalidCredentialsException; import io.kamax.mxisd.exception.InvalidCredentialsException;
import io.kamax.mxisd.exception.NotFoundException; import io.kamax.mxisd.exception.NotFoundException;
import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginGetHandler;
import io.kamax.mxisd.matrix.HomeserverFederationResolver; import io.kamax.mxisd.matrix.HomeserverFederationResolver;
import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.IStorage;
import io.kamax.mxisd.storage.ormlite.dao.AccountDao; import io.kamax.mxisd.storage.ormlite.dao.AccountDao;
@@ -48,7 +49,9 @@ public class AccountManager {
Objects.requireNonNull(openIdToken.getTokenType(), "Missing required token type"); Objects.requireNonNull(openIdToken.getTokenType(), "Missing required token type");
Objects.requireNonNull(openIdToken.getMatrixServerName(), "Missing required matrix domain"); Objects.requireNonNull(openIdToken.getMatrixServerName(), "Missing required matrix domain");
LOGGER.info("Registration from the server: {}", openIdToken.getMatrixServerName());
String userId = getUserId(openIdToken); String userId = getUserId(openIdToken);
LOGGER.info("UserId: {}", userId);
String token = UUID.randomUUID().toString(); String token = UUID.randomUUID().toString();
AccountDao account = new AccountDao(openIdToken.getAccessToken(), openIdToken.getTokenType(), AccountDao account = new AccountDao(openIdToken.getAccessToken(), openIdToken.getTokenType(),
@@ -63,13 +66,16 @@ public class AccountManager {
private String getUserId(OpenIdToken openIdToken) { private String getUserId(OpenIdToken openIdToken) {
String homeserverURL = resolver.resolve(openIdToken.getMatrixServerName()).toString(); String homeserverURL = resolver.resolve(openIdToken.getMatrixServerName()).toString();
LOGGER.info("Domain resolved: {} => {}", openIdToken.getMatrixServerName(), homeserverURL);
HttpGet getUserInfo = new HttpGet( HttpGet getUserInfo = new HttpGet(
"https://" + homeserverURL + "/_matrix/federation/v1/openid/userinfo?access_token=" + openIdToken.getAccessToken()); "https://" + homeserverURL + "/_matrix/federation/v1/openid/userinfo?access_token=" + openIdToken.getAccessToken());
String userId; String userId;
try (CloseableHttpResponse response = httpClient.execute(getUserInfo)) { try (CloseableHttpResponse response = httpClient.execute(getUserInfo)) {
int statusCode = response.getStatusLine().getStatusCode(); int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == HttpStatus.SC_OK) { if (statusCode == HttpStatus.SC_OK) {
JsonObject body = GsonUtil.parseObj(EntityUtils.toString(response.getEntity())); String content = EntityUtils.toString(response.getEntity());
LOGGER.trace("Response: {}", content);
JsonObject body = GsonUtil.parseObj(content);
userId = GsonUtil.getStringOrThrow(body, "sub"); userId = GsonUtil.getStringOrThrow(body, "sub");
} else { } else {
LOGGER.error("Wrong response status: {}", statusCode); LOGGER.error("Wrong response status: {}", statusCode);
@@ -107,12 +113,29 @@ public class AccountManager {
} }
public String getUserId(String token) { public String getUserId(String token) {
return storage.findUserId(token).orElseThrow(NotFoundException::new); return storage.findAccount(token).orElseThrow(NotFoundException::new).getUserId();
}
public AccountDao findAccount(String token) {
AccountDao accountDao = storage.findAccount(token).orElse(null);
if (LOGGER.isInfoEnabled()) {
if (accountDao != null) {
LOGGER.info("Found account for user: {}", accountDao.getUserId());
} else {
LOGGER.warn("Account not found.");
}
}
return accountDao;
} }
public void logout(String token) { public void logout(String token) {
String userId = storage.findUserId(token).orElseThrow(InvalidCredentialsException::new); String userId = storage.findAccount(token).orElseThrow(InvalidCredentialsException::new).getUserId();
LOGGER.info("Logout: {}", userId); LOGGER.info("Logout: {}", userId);
deleteAccount(token);
}
public void deleteAccount(String token) {
storage.deleteToken(token); storage.deleteToken(token);
} }

View File

@@ -0,0 +1,69 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2018 Kamax Sarl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.http.undertow.handler;
import io.kamax.mxisd.auth.AccountManager;
import io.kamax.mxisd.exception.InvalidCredentialsException;
import io.kamax.mxisd.storage.ormlite.dao.AccountDao;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AuthorizationHandler extends BasicHttpHandler {
private static final Logger log = LoggerFactory.getLogger(AuthorizationHandler.class);
private final AccountManager accountManager;
private final HttpHandler child;
public static AuthorizationHandler around(AccountManager accountManager, HttpHandler child) {
return new AuthorizationHandler(accountManager, child);
}
private AuthorizationHandler(AccountManager accountManager, HttpHandler child) {
this.accountManager = accountManager;
this.child = child;
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
String token = findAccessToken(exchange).orElse(null);
if (token == null) {
log.error("Unauthorized request from: {}", exchange.getHostAndPort());
throw new InvalidCredentialsException();
}
AccountDao account = accountManager.findAccount(token);
if (account == null) {
log.error("Account not found from request from: {}", exchange.getHostAndPort());
throw new InvalidCredentialsException();
}
if (account.getExpiresIn() < System.currentTimeMillis()) {
log.error("Account for '{}' from: {}", account.getUserId(), exchange.getHostAndPort());
accountManager.deleteAccount(token);
throw new InvalidCredentialsException();
}
log.trace("Access for '{}' allowed", account.getUserId());
child.handleRequest(exchange);
}
}

View File

@@ -28,6 +28,7 @@ import io.kamax.mxisd.exception.AccessTokenNotFoundException;
import io.kamax.mxisd.exception.HttpMatrixException; import io.kamax.mxisd.exception.HttpMatrixException;
import io.kamax.mxisd.exception.InternalServerError; import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.proxy.Response; import io.kamax.mxisd.proxy.Response;
import io.kamax.mxisd.util.OptionalUtil;
import io.kamax.mxisd.util.RestClientUtils; import io.kamax.mxisd.util.RestClientUtils;
import io.undertow.server.HttpHandler; import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange; import io.undertow.server.HttpServerExchange;
@@ -55,6 +56,24 @@ public abstract class BasicHttpHandler implements HttpHandler {
private static final Logger log = LoggerFactory.getLogger(BasicHttpHandler.class); private static final Logger log = LoggerFactory.getLogger(BasicHttpHandler.class);
protected final static String headerName = "Authorization";
protected final static String headerValuePrefix = "Bearer ";
private final static String parameterName = "access_token";
Optional<String> findAccessTokenInHeaders(HttpServerExchange exchange) {
return Optional.ofNullable(exchange.getRequestHeaders().getFirst(headerName))
.filter(header -> StringUtils.startsWith(header, headerValuePrefix))
.map(header -> header.substring(headerValuePrefix.length()));
}
Optional<String> findAccessTokenInQuery(HttpServerExchange exchange) {
return Optional.ofNullable(exchange.getQueryParameters().getOrDefault(parameterName, new LinkedList<>()).peekFirst());
}
public Optional<String> findAccessToken(HttpServerExchange exchange) {
return OptionalUtil.findFirst(() -> findAccessTokenInHeaders(exchange), () -> findAccessTokenInQuery(exchange));
}
protected String getAccessToken(HttpServerExchange exchange) { protected String getAccessToken(HttpServerExchange exchange) {
return Optional.ofNullable(exchange.getRequestHeaders().getFirst("Authorization")) return Optional.ofNullable(exchange.getRequestHeaders().getFirst("Authorization"))
.flatMap(v -> { .flatMap(v -> {

View File

@@ -21,35 +21,11 @@
package io.kamax.mxisd.http.undertow.handler; package io.kamax.mxisd.http.undertow.handler;
import io.kamax.mxisd.exception.AccessTokenNotFoundException; import io.kamax.mxisd.exception.AccessTokenNotFoundException;
import io.kamax.mxisd.util.OptionalUtil;
import io.undertow.server.HttpServerExchange; import io.undertow.server.HttpServerExchange;
import org.apache.commons.lang3.StringUtils;
import java.util.LinkedList;
import java.util.Optional;
public abstract class HomeserverProxyHandler extends BasicHttpHandler { public abstract class HomeserverProxyHandler extends BasicHttpHandler {
protected final static String headerName = "Authorization";
protected final static String headerValuePrefix = "Bearer ";
private final static String parameterName = "access_token";
Optional<String> findAccessTokenInHeaders(HttpServerExchange exchange) {
return Optional.ofNullable(exchange.getRequestHeaders().getFirst(headerName))
.filter(header -> StringUtils.startsWith(header, headerValuePrefix))
.map(header -> header.substring(headerValuePrefix.length()));
}
Optional<String> findAccessTokenInQuery(HttpServerExchange exchange) {
return Optional.ofNullable(exchange.getQueryParameters().getOrDefault(parameterName, new LinkedList<>()).peekFirst());
}
public Optional<String> findAccessToken(HttpServerExchange exchange) {
return OptionalUtil.findFirst(() -> findAccessTokenInHeaders(exchange), () -> findAccessTokenInQuery(exchange));
}
public String getAccessToken(HttpServerExchange exchange) { public String getAccessToken(HttpServerExchange exchange) {
return findAccessToken(exchange).orElseThrow(AccessTokenNotFoundException::new); return findAccessToken(exchange).orElseThrow(AccessTokenNotFoundException::new);
} }
} }

View File

@@ -22,6 +22,7 @@ package io.kamax.mxisd.http.undertow.handler.auth.v2;
import io.kamax.matrix.json.GsonUtil; import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.auth.AccountManager; import io.kamax.mxisd.auth.AccountManager;
import io.kamax.mxisd.exception.InvalidCredentialsException;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.undertow.server.HttpServerExchange; import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -31,7 +32,7 @@ public class AccountGetUserInfoHandler extends BasicHttpHandler {
public static final String Path = "/_matrix/identity/v2/account"; public static final String Path = "/_matrix/identity/v2/account";
private static final Logger log = LoggerFactory.getLogger(AccountGetUserInfoHandler.class); private static final Logger LOGGER = LoggerFactory.getLogger(AccountGetUserInfoHandler.class);
private final AccountManager accountManager; private final AccountManager accountManager;
@@ -41,13 +42,11 @@ public class AccountGetUserInfoHandler extends BasicHttpHandler {
@Override @Override
public void handleRequest(HttpServerExchange exchange) { public void handleRequest(HttpServerExchange exchange) {
String token = getQueryParameter(exchange, "access_token"); LOGGER.info("Get User Info.");
if (token == null) { String token = findAccessToken(exchange).orElseThrow(InvalidCredentialsException::new);
token = getAccessToken(exchange);
}
String userId = accountManager.getUserId(token); String userId = accountManager.getUserId(token);
LOGGER.info("Account found: {}", userId);
respond(exchange, GsonUtil.makeObj("user_id", userId)); respond(exchange, GsonUtil.makeObj("user_id", userId));
} }
} }

View File

@@ -21,6 +21,7 @@
package io.kamax.mxisd.http.undertow.handler.auth.v2; package io.kamax.mxisd.http.undertow.handler.auth.v2;
import io.kamax.mxisd.auth.AccountManager; import io.kamax.mxisd.auth.AccountManager;
import io.kamax.mxisd.exception.InvalidCredentialsException;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.undertow.server.HttpServerExchange; import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -28,9 +29,9 @@ import org.slf4j.LoggerFactory;
public class AccountLogoutHandler extends BasicHttpHandler { public class AccountLogoutHandler extends BasicHttpHandler {
public static final String Path = "/_matrix/identity/v2/account"; public static final String Path = "/_matrix/identity/v2/account/logout";
private static final Logger log = LoggerFactory.getLogger(AccountLogoutHandler.class); private static final Logger LOGGER = LoggerFactory.getLogger(AccountLogoutHandler.class);
private final AccountManager accountManager; private final AccountManager accountManager;
@@ -40,10 +41,8 @@ public class AccountLogoutHandler extends BasicHttpHandler {
@Override @Override
public void handleRequest(HttpServerExchange exchange) { public void handleRequest(HttpServerExchange exchange) {
String token = getQueryParameter(exchange, "access_token"); LOGGER.info("Logout.");
if (token == null) { String token = findAccessToken(exchange).orElseThrow(InvalidCredentialsException::new);
token = getAccessToken(exchange);
}
accountManager.logout(token); accountManager.logout(token);

View File

@@ -28,11 +28,13 @@ import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.Date;
public class AccountRegisterHandler extends BasicHttpHandler { public class AccountRegisterHandler extends BasicHttpHandler {
public static final String Path = "/_matrix/identity/v2/account/register"; public static final String Path = "/_matrix/identity/v2/account/register";
private static final Logger log = LoggerFactory.getLogger(AccountRegisterHandler.class); private static final Logger LOGGER = LoggerFactory.getLogger(AccountRegisterHandler.class);
private final AccountManager accountManager; private final AccountManager accountManager;
@@ -43,6 +45,12 @@ public class AccountRegisterHandler extends BasicHttpHandler {
@Override @Override
public void handleRequest(HttpServerExchange exchange) { public void handleRequest(HttpServerExchange exchange) {
OpenIdToken openIdToken = parseJsonTo(exchange, OpenIdToken.class); OpenIdToken openIdToken = parseJsonTo(exchange, OpenIdToken.class);
if (LOGGER.isInfoEnabled()) {
LOGGER.info("Registration from domain: {}, expired at {}", openIdToken.getMatrixServerName(),
new Date(openIdToken.getExpiredIn()));
}
String token = accountManager.register(openIdToken); String token = accountManager.register(openIdToken);
respond(exchange, GsonUtil.makeObj("token", token)); respond(exchange, GsonUtil.makeObj("token", token));
} }

View File

@@ -55,7 +55,7 @@ public interface IStorage {
void insertToken(AccountDao accountDao); void insertToken(AccountDao accountDao);
Optional<String> findUserId(String accessToken); Optional<AccountDao> findAccount(String token);
void deleteToken(String accessToken); void deleteToken(String accessToken);
} }

View File

@@ -255,7 +255,7 @@ public class OrmLiteSqlStorage implements IStorage {
} }
@Override @Override
public Optional<String> findUserId(String token) { public Optional<AccountDao> findAccount(String token) {
return withCatcher(() -> { return withCatcher(() -> {
List<AccountDao> accounts = accountDao.queryForEq("token", token); List<AccountDao> accounts = accountDao.queryForEq("token", token);
if (accounts.isEmpty()) { if (accounts.isEmpty()) {
@@ -264,7 +264,7 @@ public class OrmLiteSqlStorage implements IStorage {
if (accounts.size() != 1) { if (accounts.size() != 1) {
throw new RuntimeException("Unexpected rows for access token: " + accounts.size()); throw new RuntimeException("Unexpected rows for access token: " + accounts.size());
} }
return Optional.of(accounts.get(0).getUserId()); return Optional.of(accounts.get(0));
}); });
} }