Java tutorial
/* * mxisd - Matrix Identity Server Daemon * Copyright (C) 2017 Maxime Dor * * https://max.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.session; import com.google.gson.JsonObject; import com.google.i18n.phonenumbers.NumberParseException; import com.google.i18n.phonenumbers.PhoneNumberUtil; import com.google.i18n.phonenumbers.Phonenumber; import io.kamax.matrix.MatrixID; import io.kamax.matrix.ThreePid; import io.kamax.matrix.ThreePidMedium; import io.kamax.matrix._MatrixID; import io.kamax.mxisd.config.MatrixConfig; import io.kamax.mxisd.config.SessionConfig; import io.kamax.mxisd.controller.identity.v1.io.RequestTokenResponse; import io.kamax.mxisd.controller.identity.v1.remote.RemoteIdentityAPIv1; import io.kamax.mxisd.exception.*; import io.kamax.mxisd.lookup.ThreePidValidation; import io.kamax.mxisd.matrix.IdentityServerUtils; import io.kamax.mxisd.notification.NotificationManager; import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.dao.IThreePidSessionDao; import io.kamax.mxisd.threepid.session.IThreePidSession; import io.kamax.mxisd.threepid.session.ThreePidSession; import io.kamax.mxisd.util.GsonParser; import io.kamax.mxisd.util.RestClientUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.RandomStringUtils; import org.apache.commons.lang.StringUtils; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.message.BasicNameValuePair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; import java.util.Optional; import static io.kamax.mxisd.config.SessionConfig.Policy.PolicyTemplate; import static io.kamax.mxisd.config.SessionConfig.Policy.PolicyTemplate.PolicySource; @Component public class SessionMananger { private Logger log = LoggerFactory.getLogger(SessionMananger.class); private SessionConfig cfg; private MatrixConfig mxCfg; private IStorage storage; private NotificationManager notifMgr; private GsonParser parser = new GsonParser(); private PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance(); // FIXME refactor for sessions handling their own stuff // FIXME export into central class, set version private CloseableHttpClient client = HttpClients.custom().setUserAgent("mxisd").build(); @Autowired public SessionMananger(SessionConfig cfg, MatrixConfig mxCfg, IStorage storage, NotificationManager notifMgr) { this.cfg = cfg; this.mxCfg = mxCfg; this.storage = storage; this.notifMgr = notifMgr; } private boolean isLocal(ThreePid tpid) { if (!ThreePidMedium.Email.is(tpid.getMedium())) { // We can only handle E-mails for now return false; } String domain = tpid.getAddress().split("@")[1]; return StringUtils.equalsIgnoreCase(cfg.getMatrixCfg().getDomain(), domain); } private ThreePidSession getSession(String sid, String secret) { Optional<IThreePidSessionDao> dao = storage.getThreePidSession(sid); if (!dao.isPresent() || !StringUtils.equals(dao.get().getSecret(), secret)) { throw new SessionUnknownException(); } return new ThreePidSession(dao.get()); } private ThreePidSession getSessionIfValidated(String sid, String secret) { ThreePidSession session = getSession(sid, secret); if (!session.isValidated()) { throw new SessionNotValidatedException(); } return session; } public String create(String server, ThreePid tpid, String secret, int attempt, String nextLink) { PolicyTemplate policy = cfg.getPolicy().getValidation(); if (!policy.isEnabled()) { throw new NotAllowedException("Validating 3PID is disabled globally"); } synchronized (this) { log.info("Server {} is asking to create session for {} (Attempt #{}) - Next link: {}", server, tpid, attempt, nextLink); Optional<IThreePidSessionDao> dao = storage.findThreePidSession(tpid, secret); if (dao.isPresent()) { ThreePidSession session = new ThreePidSession(dao.get()); log.info("We already have a session for {}: {}", tpid, session.getId()); if (session.getAttempt() < attempt) { log.info( "Received attempt {} is greater than stored attempt {}, sending validation communication", attempt, session.getAttempt()); notifMgr.sendForValidation(session); log.info("Sent validation notification to {}", tpid); session.increaseAttempt(); storage.updateThreePidSession(session.getDao()); } return session.getId(); } else { log.info("No existing session for {}", tpid); boolean isLocal = isLocal(tpid); log.info("Is 3PID bound to local domain? {}", isLocal); // This might need a configuration by medium type? PolicySource policySource = policy.forIf(isLocal); if (!policySource.isEnabled() || (!policySource.toLocal() && !policySource.toRemote())) { log.info("Session for {}: cancelled due to policy", tpid); throw new NotAllowedException( "Validating " + (isLocal ? "local" : "remote") + " 3PID is not allowed"); } String sessionId; do { sessionId = Long.toString(System.currentTimeMillis()); } while (storage.getThreePidSession(sessionId).isPresent()); String token = RandomStringUtils.randomNumeric(6); ThreePidSession session = new ThreePidSession(sessionId, server, tpid, secret, attempt, nextLink, token); log.info("Generated new session {} to validate {} from server {}", sessionId, tpid, server); // This might need a configuration by medium type? if (policySource.toLocal()) { log.info("Session {} for {}: sending local validation notification", sessionId, tpid); notifMgr.sendForValidation(session); } else { log.info("Session {} for {}: sending remote-only validation notification", sessionId, tpid); notifMgr.sendforRemoteValidation(session); } storage.insertThreePidSession(session.getDao()); log.info("Stored session {}", sessionId, tpid, server); return sessionId; } } } public ValidationResult validate(String sid, String secret, String token) { ThreePidSession session = getSession(sid, secret); log.info("Attempting validation for session {} from {}", session.getId(), session.getServer()); boolean isLocal = isLocal(session.getThreePid()); PolicySource policy = cfg.getPolicy().getValidation().forIf(isLocal); if (!policy.isEnabled()) { throw new NotAllowedException("Validating " + (isLocal ? "local" : "remote") + " 3PID is not allowed"); } if (ThreePidMedium.PhoneNumber.is(session.getThreePid().getMedium()) && session.isValidated() && session.isRemote()) { submitRemote(session, token); session.validateRemote(); return new ValidationResult(session, false); } session.validate(token); storage.updateThreePidSession(session.getDao()); log.info("Session {} has been validated locally", session.getId()); if (ThreePidMedium.PhoneNumber.is(session.getThreePid().getMedium()) && session.isValidated() && policy.toRemote()) { createRemote(sid, secret); // FIXME make the message configurable/customizable (templates?) throw new MessageForClientException("You will receive a NEW code from another number. Enter it below"); } // FIXME definitely doable in a nicer way ValidationResult r = new ValidationResult(session, policy.toRemote()); if (!policy.toLocal()) { r.setNextUrl(RemoteIdentityAPIv1.getRequestToken(sid, secret)); } else { session.getNextLink().ifPresent(r::setNextUrl); } return r; } public ThreePidValidation getValidated(String sid, String secret) { ThreePidSession session = getSessionIfValidated(sid, secret); return new ThreePidValidation(session.getThreePid(), session.getValidationTime()); } public void bind(String sid, String secret, String mxidRaw) { _MatrixID mxid = new MatrixID(mxidRaw); ThreePidSession session = getSessionIfValidated(sid, secret); if (!session.isRemote()) { log.info("Session {} for {}: MXID {} was bound locally", sid, session.getThreePid(), mxid); return; } log.info("Session {} for {}: MXID {} bind is remote", sid, session.getThreePid(), mxid); if (!session.isRemoteValidated()) { log.error("Session {} for {}: Not validated remotely", sid, session.getThreePid()); throw new SessionNotValidatedException(); } log.info("Session {} for {}: Performing remote bind", sid, session.getThreePid()); UrlEncodedFormEntity entity = new UrlEncodedFormEntity( Arrays.asList(new BasicNameValuePair("sid", session.getRemoteId()), new BasicNameValuePair("client_secret", session.getRemoteSecret()), new BasicNameValuePair("mxid", mxid.getId())), StandardCharsets.UTF_8); HttpPost bindReq = new HttpPost(session.getRemoteServer() + "/_matrix/identity/api/v1/3pid/bind"); bindReq.setEntity(entity); try (CloseableHttpResponse response = client.execute(bindReq)) { int status = response.getStatusLine().getStatusCode(); if (status < 200 || status >= 300) { String body = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); log.error("Session {} for {}: Remote IS {} failed when trying to bind {} for remote session {}\n{}", sid, session.getThreePid(), session.getRemoteServer(), mxid, session.getRemoteId(), body); throw new RemoteIdentityServerException(body); } log.error("Session {} for {}: MXID {} was bound remotely", sid, session.getThreePid(), mxid); } catch (IOException e) { log.error("Session {} for {}: I/O Error when trying to bind mxid {}", sid, session.getThreePid(), mxid); throw new RemoteIdentityServerException(e.getMessage()); } } public IThreePidSession createRemote(String sid, String secret) { ThreePidSession session = getSessionIfValidated(sid, secret); log.info("Creating remote 3PID session for {} with local session [{}] to {}", session.getThreePid(), sid); boolean isLocal = isLocal(session.getThreePid()); PolicySource policy = cfg.getPolicy().getValidation().forIf(isLocal); if (!policy.isEnabled() || !policy.toRemote()) { throw new NotAllowedException("Validating " + (isLocal ? "local" : "remote") + " 3PID is not allowed"); } log.info("Remote 3PID is allowed by policy"); List<String> servers = mxCfg.getIdentity().getServers(policy.getToRemote().getServer()); if (servers.isEmpty()) { throw new FeatureNotAvailable("Remote 3PID sessions are enabled but server list is " + "misconstrued (invalid ID or empty list"); } String is = servers.get(0); String url = IdentityServerUtils.findIsUrlForDomain(is).orElse(is); log.info("Will use IS endpoint {}", url); String remoteSecret = session.isRemote() ? session.getRemoteSecret() : RandomStringUtils.randomAlphanumeric(16); JsonObject body = new JsonObject(); body.addProperty("client_secret", remoteSecret); body.addProperty(session.getThreePid().getMedium(), session.getThreePid().getAddress()); body.addProperty("send_attempt", session.increaseAndGetRemoteAttempt()); if (ThreePidMedium.PhoneNumber.is(session.getThreePid().getMedium())) { try { Phonenumber.PhoneNumber msisdn = phoneUtil.parse("+" + session.getThreePid().getAddress(), null); String country = phoneUtil.getRegionCodeForNumber(msisdn).toUpperCase(); body.addProperty("phone_number", phoneUtil.format(msisdn, PhoneNumberUtil.PhoneNumberFormat.NATIONAL)); body.addProperty("country", country); } catch (NumberParseException e) { throw new InternalServerError(e); } } else { body.addProperty(session.getThreePid().getMedium(), session.getThreePid().getAddress()); } log.info("Requesting remote session with attempt {}", session.getRemoteAttempt()); HttpPost tokenReq = RestClientUtils.post( url + "/_matrix/identity/api/v1/validate/" + session.getThreePid().getMedium() + "/requestToken", body); try (CloseableHttpResponse response = client.execute(tokenReq)) { int status = response.getStatusLine().getStatusCode(); if (status < 200 || status >= 300) { JsonObject obj = parser.parseOptional(response) .orElseThrow(() -> new RemoteIdentityServerException("Status " + status)); throw new RemoteIdentityServerException( obj.get("errcode").getAsString() + ": " + obj.get("error").getAsString()); } RequestTokenResponse data = new GsonParser().parse(response, RequestTokenResponse.class); log.info("Remote Session ID: {}", data.getSid()); session.setRemoteData(url, data.getSid(), remoteSecret, 1); storage.updateThreePidSession(session.getDao()); log.info("Updated Session {} with remote data", sid); return session; } catch (IOException e) { log.warn("Failed to create remote session with {} for {}: {}", url, session.getThreePid(), e.getMessage()); throw new RemoteIdentityServerException(e.getMessage()); } } private void submitRemote(ThreePidSession session, String token) { UrlEncodedFormEntity entity = new UrlEncodedFormEntity( Arrays.asList(new BasicNameValuePair("sid", session.getRemoteId()), new BasicNameValuePair("client_secret", session.getRemoteSecret()), new BasicNameValuePair("token", token)), StandardCharsets.UTF_8); HttpPost submitReq = new HttpPost(session.getRemoteServer() + "/_matrix/identity/api/v1/submitToken"); submitReq.setEntity(entity); try (CloseableHttpResponse response = client.execute(submitReq)) { JsonObject o = new GsonParser().parse(response.getEntity().getContent()); if (!o.has("success") || !o.get("success").getAsBoolean()) { String errcode = o.get("errcode").getAsString(); throw new RemoteIdentityServerException(errcode + ": " + o.get("error").getAsString()); } log.info("Successfully submitted validation token for {} to {}", session.getThreePid(), session.getRemoteServer()); } catch (IOException e) { throw new RemoteIdentityServerException(e.getMessage()); } } public void validateRemote(String sid, String secret) { ThreePidSession session = getSessionIfValidated(sid, secret); if (!session.isRemote()) { throw new NotAllowedException("Cannot remotely validate a local session"); } log.info("Session {} for {}: Validating remote 3PID session {} on {}", sid, session.getThreePid(), session.getRemoteId(), session.getRemoteServer()); if (session.isRemoteValidated()) { log.info("Session {} for {}: Already remotely validated", sid, session.getThreePid()); return; } HttpGet validateReq = new HttpGet( session.getRemoteServer() + "/_matrix/identity/api/v1/3pid/getValidated3pid?sid=" + session.getRemoteId() + "&client_secret=" + session.getRemoteSecret()); try (CloseableHttpResponse response = client.execute(validateReq)) { int status = response.getStatusLine().getStatusCode(); if (status < 200 || status >= 300) { throw new RemoteIdentityServerException("Remote identity server returned with status " + status); } JsonObject o = new GsonParser().parse(response.getEntity().getContent()); if (o.has("errcode")) { String errcode = o.get("errcode").getAsString(); if (StringUtils.equals("M_SESSION_NOT_VALIDATED", errcode)) { throw new SessionNotValidatedException(); } else if (StringUtils.equals("M_NO_VALID_SESSION", errcode)) { throw new SessionUnknownException(); } else { throw new RemoteIdentityServerException("Unknown error while validating Remote 3PID session: " + errcode + " - " + o.get("error").getAsString()); } } if (o.has("validated_at")) { ThreePid remoteThreePid = new ThreePid(o.get("medium").getAsString(), o.get("address").getAsString()); if (!session.getThreePid().equals(remoteThreePid)) { // sanity check throw new InternalServerError("Local 3PID " + session.getThreePid() + " and remote 3PID " + remoteThreePid + " do not match for session " + session.getId()); } log.info("Session {} for {}: Remotely validated successfully", sid, session.getThreePid()); session.validateRemote(); storage.updateThreePidSession(session.getDao()); log.info("Session {} was updated in storage", sid); } } catch (IOException e) { log.warn("Session {} for {}: Failed to validated remotely on {}: {}", sid, session.getThreePid(), session.getRemoteServer(), e.getMessage()); throw new RemoteIdentityServerException(e.getMessage()); } } }