Attempt to support invites, working in progress

This commit is contained in:
Maxime Dor
2017-09-06 04:17:46 +02:00
parent c05ca68de4
commit cd6960fa80
27 changed files with 999 additions and 143 deletions

View File

@@ -0,0 +1,15 @@
package io.kamax.mxisd.invitation;
import io.kamax.matrix._MatrixID;
public interface IThreePidInvite {
_MatrixID getSender();
String getMedium();
String getAddress();
String getRoomId();
}

View File

@@ -0,0 +1,11 @@
package io.kamax.mxisd.invitation;
public interface IThreePidInviteReply {
IThreePidInvite getInvite();
String getToken();
String getDisplayName();
}

View File

@@ -0,0 +1,226 @@
package io.kamax.mxisd.invitation;
import com.google.gson.Gson;
import io.kamax.matrix.ThreePid;
import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.exception.MappingAlreadyExistsException;
import io.kamax.mxisd.invitation.sender.IInviteSender;
import io.kamax.mxisd.lookup.SingleLookupRequest;
import io.kamax.mxisd.lookup.ThreePidMapping;
import io.kamax.mxisd.lookup.strategy.LookupStrategy;
import io.kamax.mxisd.signature.SignatureManager;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.ssl.SSLContextBuilder;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.xbill.DNS.Lookup;
import org.xbill.DNS.SRVRecord;
import org.xbill.DNS.TextParseException;
import org.xbill.DNS.Type;
import javax.annotation.PostConstruct;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class InvitationManager {
private Logger log = LoggerFactory.getLogger(InvitationManager.class);
private Map<ThreePid, IThreePidInviteReply> invitations = new ConcurrentHashMap<>();
@Autowired
private LookupStrategy lookupMgr;
@Autowired
private SignatureManager signMgr;
private Map<String, IInviteSender> senders;
private CloseableHttpClient client;
private Gson gson;
@PostConstruct
private void postConstruct() {
gson = new Gson();
try {
HttpClientBuilder b = HttpClientBuilder.create();
// setup a Trust Strategy that allows all certificates.
//
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
public boolean isTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
return true;
}
}).build();
b.setSslcontext(sslContext);
// don't check Hostnames, either.
// -- use SSLConnectionSocketFactory.getDefaultHostnameVerifier(), if you don't want to weaken
HostnameVerifier hostnameVerifier = SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER;
// here's the special part:
// -- need to create an SSL Socket Factory, to use our weakened "trust strategy";
// -- and create a Registry, to register it.
//
SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext, hostnameVerifier);
Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", sslSocketFactory)
.build();
// now, we create connection-manager using our Registry.
// -- allows multi-threaded use
PoolingHttpClientConnectionManager connMgr = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
b.setConnectionManager(connMgr);
// finally, build the HttpClient;
// -- done!
client = b.build();
} catch (Exception e) {
// FIXME do better...
throw new RuntimeException(e);
}
}
String getSrvRecordName(String domain) {
return "_matrix._tcp." + domain;
}
// TODO use caching mechanism
// TODO export in matrix-java-sdk
Optional<String> findHomeserverForDomain(String domain) {
log.debug("Performing SRV lookup for {}", domain);
String lookupDns = getSrvRecordName(domain);
log.info("Lookup name: {}", lookupDns);
try {
SRVRecord[] records = (SRVRecord[]) new Lookup(lookupDns, Type.SRV).run();
if (records != null) {
Arrays.sort(records, Comparator.comparingInt(SRVRecord::getPriority));
for (SRVRecord record : records) {
log.info("Found SRV record: {}", record.toString());
return Optional.of("https://" + record.getTarget().toString(true) + ":" + record.getPort());
}
} else {
log.info("No SRV record for {}", lookupDns);
}
} catch (TextParseException e) {
log.warn("Unable to perform DNS SRV query for {}: {}", lookupDns, e.getMessage());
}
log.info("Performing basic lookup using domain name {}", domain);
return Optional.of("https://" + domain + ":8448");
}
@Autowired
public InvitationManager(List<IInviteSender> senderList) {
senders = new HashMap<>();
senderList.forEach(sender -> senders.put(sender.getMedium(), sender));
}
public synchronized IThreePidInviteReply storeInvite(IThreePidInvite invitation) { // TODO better sync
IInviteSender sender = senders.get(invitation.getMedium());
if (sender == null) {
throw new BadRequestException("Medium type " + invitation.getMedium() + " is not supported");
}
ThreePid pid = new ThreePid(invitation.getMedium(), invitation.getAddress());
log.info("Storing invite for {}:{} from {} in room {}", pid.getMedium(), pid.getAddress(), invitation.getSender(), invitation.getRoomId());
if (invitations.containsKey(pid)) {
log.info("Invite is already pending for {}:{}, returning data", pid.getMedium(), pid.getAddress());
return invitations.get(pid);
}
SingleLookupRequest request = new SingleLookupRequest();
request.setType(invitation.getMedium());
request.setThreePid(invitation.getAddress());
request.setRecursive(true);
request.setRequester("Internal");
Optional<?> result = lookupMgr.findRecursive(request);
if (result.isPresent()) {
log.info("Mapping for {}:{} already exists, refusing to store invite", pid.getMedium(), pid.getAddress());
throw new MappingAlreadyExistsException();
}
String token = RandomStringUtils.randomAlphanumeric(64);
String displayName = invitation.getAddress().substring(0, 3) + "...";
IThreePidInviteReply reply = new ThreePidInviteReply(invitation, token, displayName);
log.info("Performing invite to {}:{}", pid.getMedium(), pid.getAddress());
sender.send(reply);
invitations.put(pid, reply);
log.info("A new invite has been created for {}:{}", pid.getMedium(), pid.getAddress());
return reply;
}
public void publishMappingIfInvited(ThreePidMapping threePid) {
ThreePid key = new ThreePid(threePid.getMedium(), threePid.getValue());
IThreePidInviteReply reply = invitations.get(key);
if (reply == null) {
log.info("{}:{} does not have a pending invite, no mapping to publish", threePid.getMedium(), threePid.getValue());
return;
}
log.info("{}:{} has an invite pending, publishing mapping", threePid.getMedium(), threePid.getValue());
String domain = reply.getInvite().getSender().getDomain();
log.info("Discovering HS for domain {}", domain);
Optional<String> hsUrlOpt = findHomeserverForDomain(domain);
if (!hsUrlOpt.isPresent()) {
log.warn("No HS found for domain {} - ignoring publishing", domain);
} else {
HttpPost req = new HttpPost(hsUrlOpt.get() + "/_matrix/federation/v1/3pid/onbind");
JSONObject obj = new JSONObject(); // TODO use Gson instead
obj.put("mxisd", threePid.getMxid());
obj.put("token", reply.getToken());
String mapping = gson.toJson(signMgr.signMessage(obj.toString())); // FIXME we shouldn't need to be doign this
JSONObject content = new JSONObject(); // TODO use Gson instead
content.put("invites", Collections.singletonList(mapping));
content.put("medium", threePid.getMedium());
content.put("address", threePid.getValue());
content.put("mxid", threePid.getMxid());
StringEntity entity = new StringEntity(content.toString(), StandardCharsets.UTF_8);
entity.setContentType("application/json");
req.setEntity(entity);
try {
log.info("Posting onBind event to {}", req.getURI());
CloseableHttpResponse response = client.execute(req);
response.close();
} catch (IOException e) {
log.warn("Unable to tell HS {} about invite being mapped", domain, e);
}
}
invitations.remove(key);
}
}

View File

@@ -0,0 +1,39 @@
package io.kamax.mxisd.invitation;
import io.kamax.matrix._MatrixID;
public class ThreePidInvite implements IThreePidInvite {
private _MatrixID sender;
private String medium;
private String address;
private String roomId;
public ThreePidInvite(_MatrixID sender, String medium, String address, String roomId) {
this.sender = sender;
this.medium = medium;
this.address = address;
this.roomId = roomId;
}
@Override
public _MatrixID getSender() {
return sender;
}
@Override
public String getMedium() {
return medium;
}
@Override
public String getAddress() {
return address;
}
@Override
public String getRoomId() {
return roomId;
}
}

View File

@@ -0,0 +1,30 @@
package io.kamax.mxisd.invitation;
public class ThreePidInviteReply implements IThreePidInviteReply {
private IThreePidInvite invite;
private String token;
private String displayName;
public ThreePidInviteReply(IThreePidInvite invite, String token, String displayName) {
this.invite = invite;
this.token = token;
this.displayName = displayName;
}
@Override
public IThreePidInvite getInvite() {
return invite;
}
@Override
public String getToken() {
return token;
}
@Override
public String getDisplayName() {
return displayName;
}
}

View File

@@ -0,0 +1,82 @@
package io.kamax.mxisd.invitation.sender;
import com.sun.mail.smtp.SMTPTransport;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.mxisd.config.invite.sender.EmailSenderConfig;
import io.kamax.mxisd.exception.ConfigurationException;
import io.kamax.mxisd.invitation.IThreePidInviteReply;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Date;
@Component
public class EmailInviteSender implements IInviteSender {
private Logger log = LoggerFactory.getLogger(EmailInviteSender.class);
@Autowired
private EmailSenderConfig cfg;
private Session session;
private InternetAddress sender;
@PostConstruct
private void postConstruct() {
try {
session = Session.getInstance(System.getProperties());
sender = new InternetAddress(cfg.getEmail(), cfg.getName());
} catch (UnsupportedEncodingException e) {
// What are we supposed to do with this?!
throw new ConfigurationException(e);
}
}
@Override
public String getMedium() {
return ThreePidMedium.Email.getId();
}
@Override
public void send(IThreePidInviteReply invite) {
if (!ThreePidMedium.Email.is(invite.getInvite().getMedium())) {
throw new IllegalArgumentException(invite.getInvite().getMedium() + " is not a supported 3PID type");
}
try {
MimeMessage msg = new MimeMessage(session, new FileInputStream(cfg.getContentPath()));
msg.setHeader("X-Mailer", "mxisd");
msg.setSentDate(new Date());
msg.setRecipients(Message.RecipientType.TO, invite.getInvite().getAddress());
msg.setFrom(sender);
log.info("Sending invite to {} via SMTP using {}:{}", invite.getInvite().getAddress(), cfg.getHost(), cfg.getPort());
SMTPTransport transport = (SMTPTransport) session.getTransport("smtp");
transport.setStartTLS(cfg.getTls() > 0);
transport.setRequireStartTLS(cfg.getTls() > 1);
log.info("Connecting to {}:{}", cfg.getHost(), cfg.getPort());
transport.connect(cfg.getHost(), cfg.getPort(), cfg.getLogin(), cfg.getPassword());
try {
transport.sendMessage(msg, InternetAddress.parse(invite.getInvite().getAddress()));
log.info("Invite to {} was sent", invite.getInvite().getAddress());
} finally {
transport.close();
}
} catch (IOException | MessagingException e) {
throw new RuntimeException("Unable to send e-mail invite to " + invite.getInvite().getAddress(), e);
}
}
}

View File

@@ -0,0 +1,11 @@
package io.kamax.mxisd.invitation.sender;
import io.kamax.mxisd.invitation.IThreePidInviteReply;
public interface IInviteSender {
String getMedium();
void send(IThreePidInviteReply invite);
}