Files
ma1sd/src/main/java/io/kamax/mxisd/matrix/HomeserverFederationResolver.java

217 lines
8.0 KiB
Java

/*
* 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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.matrix;
import com.google.gson.JsonObject;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.matrix.json.InvalidJsonException;
import io.kamax.mxisd.dns.FederationDnsOverwrite;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xbill.DNS.*;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
public class HomeserverFederationResolver {
private static final Logger log = LoggerFactory.getLogger(HomeserverFederationResolver.class);
private FederationDnsOverwrite dns;
private CloseableHttpClient client;
public HomeserverFederationResolver(FederationDnsOverwrite dns, CloseableHttpClient client) {
this.dns = dns;
this.client = client;
}
private String getDefaultScheme() {
return "https";
}
private int getDefaultPort() {
return 8448;
}
private String getDnsSrvPrefix() {
return "_matrix._tcp.";
}
private String buildSrvRecordName(String domain) {
return getDnsSrvPrefix() + domain;
}
private Optional<URL> resolveOverwrite(String domain) {
Optional<String> entryOpt = dns.findHost(domain);
if (!entryOpt.isPresent()) {
log.info("No DNS overwrite for {}", domain);
return Optional.empty();
}
try {
return Optional.of(new URL(entryOpt.get()));
} catch (MalformedURLException e) {
log.warn("Skipping homeserver Federation DNS overwrite for {} - not a valid URL: {}", domain, entryOpt.get());
return Optional.empty();
}
}
private Optional<URL> resolveLiteral(String domain) {
if (domain.contains("[") && domain.contains("]")) {
// This is an IPv6
if (domain.contains("]:")) {
// With a custom port, we return as is
return Optional.of(build(domain));
} else {
return Optional.of(build(domain + ":" + getDefaultPort()));
}
}
if (domain.contains(":")) {
// This is a domain or IPv4 with an explicit port, we return as is
return Optional.of(build(domain));
}
// At this point, we do not account for the provided string to be an IPv4 without a port. We will therefore
// perform well-known lookup and SRV record. While this is not needed, we don't expect the SRV to return anything
// and the well-known shouldn't either, but it might, leading to a wrong destination potentially.
//
// We accept this risk as mxisd is not meant to be used without DNS domain as per FAQ. We also provide resolution
// override facilities. Therefore, we accept to not handle this case until we get report of such unwanted behaviour
// that still fix mxisd use case and can't be resolved via override.
return Optional.empty();
}
private Optional<URL> resolveWellKnown(String domain) {
log.debug("Performing Well-known lookup for {}", domain);
HttpGet wnReq = new HttpGet("https://" + domain + "/.well-known/matrix/server");
try (CloseableHttpResponse wnRes = client.execute(wnReq)) {
int status = wnRes.getStatusLine().getStatusCode();
if (status == 200) {
try {
JsonObject body = GsonUtil.parseObj(EntityUtils.toString(wnRes.getEntity()));
String server = GsonUtil.getStringOrNull(body, "m.server");
if (StringUtils.isNotBlank(server)) {
log.debug("Found well-known entry: {}", server);
return Optional.of(build(server));
}
} catch (InvalidJsonException e) {
log.info("Could not parse well-known resource: {}", e.getMessage());
}
} else {
log.info("Well-known did not return status code 200 but {}, ignoring", status);
}
return Optional.empty();
} catch (IOException e) {
throw new RuntimeException("Error while trying to lookup well-known for " + domain, e);
}
}
private Optional<URL> resolveDnsSrv(String domain) {
log.debug("Performing SRV lookup for {}", domain);
String lookupDns = buildSrvRecordName(domain);
log.debug("Lookup name: {}", lookupDns);
try {
List<SRVRecord> srvRecords = new ArrayList<>();
Record[] rawRecords = new Lookup(lookupDns, Type.SRV).run();
if (Objects.isNull(rawRecords) || rawRecords.length == 0) {
log.debug("No SRV record for {}", domain);
return Optional.empty();
}
for (Record record : rawRecords) {
if (Type.SRV == record.getType()) {
srvRecords.add((SRVRecord) record);
} else {
log.debug("Ignoring non-SRV record: {}", record.toString());
}
}
if (srvRecords.size() < 1) {
log.warn("DNS SRV records were found for {} but none is usable", lookupDns);
return Optional.empty();
}
srvRecords.sort(Comparator.comparingInt(SRVRecord::getPriority));
SRVRecord record = srvRecords.get(0);
return Optional.of(build(record.getTarget().toString(true) + ":" + record.getPort()));
} catch (TextParseException e) {
log.warn("Unable to perform DNS SRV query for {}: {}", lookupDns, e.getMessage());
}
return Optional.empty();
}
public URL build(String authority) {
try {
return new URL(getDefaultScheme() + "://" + authority);
} catch (MalformedURLException e) {
throw new RuntimeException("Could not build URL for " + authority, e);
}
}
public URL resolve(String domain) {
Optional<URL> s1 = resolveOverwrite(domain);
if (s1.isPresent()) {
URL dest = s1.get();
log.info("Resolution of {} via DNS overwrite to {}", domain, dest);
return dest;
}
Optional<URL> s2 = resolveLiteral(domain);
if (s2.isPresent()) {
URL dest = s2.get();
log.info("Resolution of {} as IP literal or IP/hostname with explicit port to {}", domain, dest);
return dest;
}
Optional<URL> s3 = resolveWellKnown(domain);
if (s3.isPresent()) {
URL dest = s3.get();
log.info("Resolution of {} via well-known to {}", domain, dest);
return dest;
}
// The domain needs to be resolved
Optional<URL> s4 = resolveDnsSrv(domain);
if (s4.isPresent()) {
URL dest = s4.get();
log.info("Resolution of {} via DNS SRV record to {}", domain, dest);
return dest;
}
URL dest = build(domain + ":" + getDefaultPort());
log.info("Resolution of {} to {}", domain, dest);
return dest;
}
}