diff --git a/src/main/java/io/kamax/mxisd/HttpMxisd.java b/src/main/java/io/kamax/mxisd/HttpMxisd.java index a17cd87..362db5d 100644 --- a/src/main/java/io/kamax/mxisd/HttpMxisd.java +++ b/src/main/java/io/kamax/mxisd/HttpMxisd.java @@ -33,6 +33,7 @@ import io.kamax.mxisd.http.undertow.handler.directory.v1.UserDirectorySearchHand import io.kamax.mxisd.http.undertow.handler.identity.v1.*; import io.kamax.mxisd.http.undertow.handler.profile.v1.InternalProfileHandler; import io.kamax.mxisd.http.undertow.handler.profile.v1.ProfileHandler; +import io.kamax.mxisd.http.undertow.handler.register.v1.Register3pidRequestTokenHandler; import io.kamax.mxisd.http.undertow.handler.status.StatusHandler; import io.undertow.Handlers; import io.undertow.Undertow; @@ -97,6 +98,9 @@ public class HttpMxisd { .get(ProfileHandler.Path, SaneHandler.around(new ProfileHandler(m.getProfile()))) .get(InternalProfileHandler.Path, SaneHandler.around(new InternalProfileHandler(m.getProfile()))) + // Registration endpoints + .post(Register3pidRequestTokenHandler.Path, SaneHandler.around(new Register3pidRequestTokenHandler(m.getReg(), m.getClientDns(), m.getHttpClient()))) + // Application Service endpoints .get("/_matrix/app/v1/users/**", asNotFoundHandler) .get("/users/**", asNotFoundHandler) // Legacy endpoint diff --git a/src/main/java/io/kamax/mxisd/Mxisd.java b/src/main/java/io/kamax/mxisd/Mxisd.java index 31f37f9..ea9c3eb 100644 --- a/src/main/java/io/kamax/mxisd/Mxisd.java +++ b/src/main/java/io/kamax/mxisd/Mxisd.java @@ -44,6 +44,7 @@ import io.kamax.mxisd.notification.NotificationHandlers; import io.kamax.mxisd.notification.NotificationManager; import io.kamax.mxisd.profile.ProfileManager; import io.kamax.mxisd.profile.ProfileProviders; +import io.kamax.mxisd.registration.RegistrationManager; import io.kamax.mxisd.session.SessionManager; import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.crypto.Ed25519KeyManager; @@ -66,6 +67,7 @@ public class Mxisd { private Ed25519KeyManager keyMgr; private SignatureManager signMgr; + private ClientDnsOverwrite clientDns; // Features private AuthManager authMgr; @@ -76,6 +78,7 @@ public class Mxisd { private AppSvcManager asHander; private SessionManager sessMgr; private NotificationManager notifMgr; + private RegistrationManager regMgr; public Mxisd(MxisdConfig cfg) { this.cfg = cfg.build(); @@ -94,7 +97,7 @@ public class Mxisd { store = new OrmLiteSqlStorage(cfg); keyMgr = CryptoFactory.getKeyManager(cfg.getKey()); signMgr = CryptoFactory.getSignatureManager(keyMgr); - ClientDnsOverwrite clientDns = new ClientDnsOverwrite(cfg.getDns().getOverwrite()); + clientDns = new ClientDnsOverwrite(cfg.getDns().getOverwrite()); FederationDnsOverwrite fedDns = new FederationDnsOverwrite(cfg.getDns().getOverwrite()); Synapse synapse = new Synapse(cfg.getSynapseSql()); BridgeFetcher bridgeFetcher = new BridgeFetcher(cfg.getLookup().getRecursive().getBridge(), srvFetcher); @@ -109,6 +112,7 @@ public class Mxisd { invMgr = new InvitationManager(cfg, store, idStrategy, keyMgr, signMgr, fedDns, notifMgr); authMgr = new AuthManager(cfg, AuthProviders.get(), idStrategy, invMgr, clientDns, httpClient); dirMgr = new DirectoryManager(cfg.getDirectory(), clientDns, httpClient, DirectoryProviders.get()); + regMgr = new RegistrationManager(httpClient, clientDns, idStrategy, invMgr); asHander = new AppSvcManager(cfg, store, pMgr, notifMgr, synapse); } @@ -120,6 +124,10 @@ public class Mxisd { return httpClient; } + public ClientDnsOverwrite getClientDns() { + return clientDns; + } + public IRemoteIdentityServerFetcher getServerFetcher() { return srvFetcher; } @@ -156,6 +164,10 @@ public class Mxisd { return signMgr; } + public RegistrationManager getReg() { + return regMgr; + } + public AppSvcManager getAs() { return asHander; } diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/register/v1/Register3pidRequestTokenHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/register/v1/Register3pidRequestTokenHandler.java new file mode 100644 index 0000000..af2dbcb --- /dev/null +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/register/v1/Register3pidRequestTokenHandler.java @@ -0,0 +1,101 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 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 . + */ + +package io.kamax.mxisd.http.undertow.handler.register.v1; + +import com.google.gson.JsonObject; +import io.kamax.matrix.ThreePid; +import io.kamax.matrix.ThreePidMedium; +import io.kamax.matrix.json.GsonUtil; +import io.kamax.mxisd.dns.ClientDnsOverwrite; +import io.kamax.mxisd.exception.InternalServerError; +import io.kamax.mxisd.exception.NotAllowedException; +import io.kamax.mxisd.http.io.identity.SessionEmailTokenRequestJson; +import io.kamax.mxisd.http.io.identity.SessionPhoneTokenRequestJson; +import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; +import io.kamax.mxisd.registration.RegistrationManager; +import io.kamax.mxisd.util.RestClientUtils; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.HttpString; +import org.apache.http.Header; +import org.apache.http.HeaderElement; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URI; + +public class Register3pidRequestTokenHandler extends BasicHttpHandler { + + public static final String Key = "medium"; + public static final String Path = "/_matrix/client/r0/register/{" + Key + "}/requestToken"; + + private static final Logger log = LoggerFactory.getLogger(Register3pidRequestTokenHandler.class); + + private final RegistrationManager mgr; + private final ClientDnsOverwrite dns; + private final CloseableHttpClient client; + + public Register3pidRequestTokenHandler(RegistrationManager mgr, ClientDnsOverwrite dns, CloseableHttpClient client) { + this.mgr = mgr; + this.dns = dns; // FIXME this shouldn't be in here but in the manager + this.client = client; // FIXME this shouldn't be in here but in the manager + } + + @Override + public void handleRequest(HttpServerExchange exchange) { + JsonObject body = parseJsonObject(exchange); + + String medium = getPathVariable(exchange, Key); + String address = GsonUtil.findString(body, "address").orElse(""); + if (ThreePidMedium.Email.is(medium)) { + address = GsonUtil.get().fromJson(body, SessionEmailTokenRequestJson.class).getValue(); + } else if (ThreePidMedium.PhoneNumber.is(medium)) { + address = GsonUtil.get().fromJson(body, SessionPhoneTokenRequestJson.class).getValue(); + } else { + log.warn("Unsupported 3PID medium. We attempted to extract the address but the call might fail"); + } + + ThreePid tpid = new ThreePid(medium, address); + if (!mgr.allow(tpid)) { + throw new NotAllowedException("Your " + medium + " address cannot be used for registration"); + } + + String target = dns.transform(URI.create(exchange.getRequestURL())).toString(); + log.info("Requesting remote: {}", target); + HttpPost req = RestClientUtils.post(target, GsonUtil.get(), body); + try (CloseableHttpResponse res = client.execute(req)) { + exchange.setStatusCode(res.getStatusLine().getStatusCode()); + for (Header h : res.getAllHeaders()) { + for (HeaderElement el : h.getElements()) { + exchange.getResponseHeaders().add(HttpString.tryFromString(h.getName()), el.getValue()); + } + } + res.getEntity().writeTo(exchange.getOutputStream()); + exchange.endExchange(); + } catch (IOException e) { + throw new InternalServerError(e); + } + } + +} diff --git a/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java b/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java index 7dca267..36938f3 100644 --- a/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java +++ b/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java @@ -23,6 +23,7 @@ package io.kamax.mxisd.invitation; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import io.kamax.matrix.MatrixID; +import io.kamax.matrix.ThreePid; import io.kamax.matrix.json.GsonUtil; import io.kamax.mxisd.config.InvitationConfig; import io.kamax.mxisd.config.MxisdConfig; @@ -266,6 +267,22 @@ public class InvitationManager { return reply; } + public boolean hasInvite(ThreePid tpid) { + for (IThreePidInviteReply reply : invitations.values()) { + if (!StringUtils.equals(tpid.getMedium(), reply.getInvite().getMedium())) { + continue; + } + + if (!StringUtils.equals(tpid.getAddress(), reply.getInvite().getAddress())) { + continue; + } + + return true; + } + + return false; + } + public void lookupMappingsForInvites() { if (!invitations.isEmpty()) { log.info("Checking for existing mapping for pending invites"); diff --git a/src/main/java/io/kamax/mxisd/registration/RegistrationManager.java b/src/main/java/io/kamax/mxisd/registration/RegistrationManager.java new file mode 100644 index 0000000..63ecbc9 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/registration/RegistrationManager.java @@ -0,0 +1,102 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 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 . + */ + +package io.kamax.mxisd.registration; + +import com.google.gson.JsonObject; +import io.kamax.matrix.ThreePid; +import io.kamax.matrix.json.GsonUtil; +import io.kamax.mxisd.dns.ClientDnsOverwrite; +import io.kamax.mxisd.exception.NotImplementedException; +import io.kamax.mxisd.exception.RemoteHomeServerException; +import io.kamax.mxisd.invitation.InvitationManager; +import io.kamax.mxisd.lookup.strategy.LookupStrategy; +import io.kamax.mxisd.util.RestClientUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URI; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class RegistrationManager { + + private static final Logger log = LoggerFactory.getLogger(RegistrationManager.class); + + private final CloseableHttpClient client; + private final ClientDnsOverwrite dns; + private final LookupStrategy lookup; + private final InvitationManager invMgr; + + private Map sessions = new ConcurrentHashMap<>(); + + public RegistrationManager(CloseableHttpClient client, ClientDnsOverwrite dns, LookupStrategy lookup, InvitationManager invMgr) { + this.client = client; + this.dns = dns; + this.lookup = lookup; + this.invMgr = invMgr; + } + + private String resolveProxyUrl(URI target) { + URIBuilder builder = dns.transform(target); + String urlToLogin = builder.toString(); + log.info("Proxy resolution: {} to {}", target.toString(), urlToLogin); + return urlToLogin; + } + + public RegistrationReply execute(URI target, JsonObject request) { + HttpPost registerProxyRq = RestClientUtils.post(resolveProxyUrl(target), GsonUtil.get(), request); + try (CloseableHttpResponse response = client.execute(registerProxyRq)) { + int status = response.getStatusLine().getStatusCode(); + if (status == 200) { + // The user managed to register. We check if it had a session + String sessionId = GsonUtil.findObj(request, "auth").flatMap(auth -> GsonUtil.findString(auth, "session")).orElse(""); + if (StringUtils.isEmpty(sessionId)) { + // No session ID was provided. This is an edge case we do not support for now as investigation is needed + // to ensure how and when this happens. + + HttpPost newSessReq = RestClientUtils.post(resolveProxyUrl(target), GsonUtil.get(), new JsonObject()); + try (CloseableHttpResponse newSessRes = client.execute(newSessReq)) { + RegistrationReply reply = new RegistrationReply(); + reply.setStatus(newSessRes.getStatusLine().getStatusCode()); + reply.setBody(GsonUtil.parseObj(EntityUtils.toString(newSessRes.getEntity()))); + return reply; + } + } + } + + throw new NotImplementedException("Registration"); + } catch (IOException e) { + throw new RemoteHomeServerException(e.getMessage()); + } + } + + public boolean allow(ThreePid tpid) { + return invMgr.hasInvite(tpid); + } + +} diff --git a/src/main/java/io/kamax/mxisd/registration/RegistrationReply.java b/src/main/java/io/kamax/mxisd/registration/RegistrationReply.java new file mode 100644 index 0000000..b2e2dde --- /dev/null +++ b/src/main/java/io/kamax/mxisd/registration/RegistrationReply.java @@ -0,0 +1,46 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 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 . + */ + +package io.kamax.mxisd.registration; + +import com.google.gson.JsonObject; + +public class RegistrationReply { + + private int status; + private JsonObject body; + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public JsonObject getBody() { + return body; + } + + public void setBody(JsonObject body) { + this.body = body; + } + +}