001/*
002 * Copyright 2015-2024 the original author or authors
003 *
004 * This software is licensed under the Apache License, Version 2.0,
005 * the GNU Lesser General Public License version 2 or later ("LGPL")
006 * and the WTFPL.
007 * You may choose either license to govern your use of this software only
008 * upon the condition that you accept all of the terms of either
009 * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
010 */
011package org.minidns.hla;
012
013import java.io.IOException;
014import java.net.InetAddress;
015import java.util.ArrayList;
016import java.util.Collection;
017import java.util.Collections;
018import java.util.IdentityHashMap;
019import java.util.List;
020import java.util.Map;
021import java.util.Set;
022
023import org.minidns.AbstractDnsClient.IpVersionSetting;
024import org.minidns.MiniDnsException.NullResultException;
025import org.minidns.dnsname.DnsName;
026import org.minidns.hla.srv.SrvServiceProto;
027import org.minidns.record.A;
028import org.minidns.record.AAAA;
029import org.minidns.record.InternetAddressRR;
030import org.minidns.record.SRV;
031import org.minidns.util.SrvUtil;
032
033public class SrvResolverResult extends ResolverResult<SRV> {
034
035    private final ResolverApi resolver;
036    private final IpVersionSetting ipVersion;
037    private final SrvServiceProto srvServiceProto;
038
039    private List<ResolvedSrvRecord> sortedSrvResolvedAddresses;
040
041    SrvResolverResult(ResolverResult<SRV> srvResult, SrvServiceProto srvServiceProto, ResolverApi resolver) throws NullResultException {
042        super(srvResult.question, srvResult.result, srvResult.unverifiedReasons);
043        this.resolver = resolver;
044        this.ipVersion = resolver.getClient().getPreferedIpVersion();
045        this.srvServiceProto = srvServiceProto;
046    }
047
048    /**
049     * Get a list ordered by priority and weight of the resolved SRV records. This method will throw if there was an
050     * error response or if subsequent {@link A} or {@link AAAA} resource record lookups fail. It will return
051     * {@code null} in case the service is decidedly not available at this domain.
052     *
053     * @return a list ordered by priority and weight of the related SRV records.
054     * @throws IOException in case an I/O error occurs.
055     */
056    public List<ResolvedSrvRecord> getSortedSrvResolvedAddresses() throws IOException {
057        if (sortedSrvResolvedAddresses != null) {
058            return sortedSrvResolvedAddresses;
059        }
060
061        throwIseIfErrorResponse();
062
063        if (isServiceDecidedlyNotAvailableAtThisDomain()) {
064            return null;
065        }
066
067        List<SRV> srvRecords = SrvUtil.sortSrvRecords(getAnswers());
068
069        List<ResolvedSrvRecord> res = new ArrayList<>(srvRecords.size());
070        for (SRV srvRecord : srvRecords) {
071            ResolverResult<A> aRecordsResult = null;
072            ResolverResult<AAAA> aaaaRecordsResult = null;
073            Set<A> aRecords = Collections.emptySet();
074            if (ipVersion.v4) {
075                aRecordsResult = resolver.resolve(srvRecord.target, A.class);
076                if (aRecordsResult.wasSuccessful() && !aRecordsResult.hasUnverifiedReasons()) {
077                    aRecords = aRecordsResult.getAnswers();
078                }
079            }
080
081            Set<AAAA> aaaaRecords = Collections.emptySet();
082            if (ipVersion.v6) {
083                aaaaRecordsResult = resolver.resolve(srvRecord.target, AAAA.class);
084                if (aaaaRecordsResult.wasSuccessful() && !aaaaRecordsResult.hasUnverifiedReasons()) {
085                    aaaaRecords = aaaaRecordsResult.getAnswers();
086                }
087            }
088
089            if (aRecords.isEmpty() && aaaaRecords.isEmpty()) {
090                // TODO Possibly check for (C|D)NAME usage and throw a meaningful exception that it is not allowed for
091                // the target of an SRV to be an alias as per RFC 2782.
092                /*
093                ResolverResult<CNAME> cnameRecordResult = resolve(srvRecord.name, CNAME.class);
094                if (cnameRecordResult.wasSuccessful()) {
095                }
096                */
097                continue;
098            }
099
100            List<InternetAddressRR<? extends InetAddress>> srvAddresses = new ArrayList<>(aRecords.size() + aaaaRecords.size());
101            switch (ipVersion) {
102            case v4only:
103                srvAddresses.addAll(aRecords);
104                break;
105            case v6only:
106                srvAddresses.addAll(aaaaRecords);
107                break;
108            case v4v6:
109                srvAddresses.addAll(aRecords);
110                srvAddresses.addAll(aaaaRecords);
111                break;
112            case v6v4:
113                srvAddresses.addAll(aaaaRecords);
114                srvAddresses.addAll(aRecords);
115                break;
116            }
117
118            ResolvedSrvRecord resolvedSrvAddresses = new ResolvedSrvRecord(question.name, srvServiceProto, srvRecord, srvAddresses,
119                    aRecordsResult, aaaaRecordsResult);
120            res.add(resolvedSrvAddresses);
121        }
122
123        sortedSrvResolvedAddresses = res;
124
125        return res;
126    }
127
128    public boolean isServiceDecidedlyNotAvailableAtThisDomain() {
129        Set<SRV> answers = getAnswers();
130        if (answers.size() != 1) {
131            return false;
132        }
133
134        SRV singleAnswer = answers.iterator().next();
135        return !singleAnswer.isServiceAvailable();
136    }
137
138    public static final class ResolvedSrvRecord {
139        public final DnsName name;
140        public final SrvServiceProto srvServiceProto;
141        public final SRV srv;
142        public final List<InternetAddressRR<? extends InetAddress>> addresses;
143        public final ResolverResult<A> aRecordsResult;
144        public final ResolverResult<AAAA> aaaaRecordsResult;
145
146        /**
147         * The port announced by the SRV RR. This is simply a shortcut for <code>srv.port</code>.
148         */
149        public final int port;
150
151        private ResolvedSrvRecord(DnsName name, SrvServiceProto srvServiceProto, SRV srv,
152                List<InternetAddressRR<? extends InetAddress>> addresses, ResolverResult<A> aRecordsResult,
153                ResolverResult<AAAA> aaaaRecordsResult) {
154            this.name = name;
155            this.srvServiceProto = srvServiceProto;
156            this.srv = srv;
157            this.addresses = Collections.unmodifiableList(addresses);
158            this.port = srv.port;
159            this.aRecordsResult = aRecordsResult;
160            this.aaaaRecordsResult = aaaaRecordsResult;
161        }
162    }
163
164    /**
165     * Convenience method to sort multiple resolved SRV RRs. This is for example required by XEP-0368, where
166     * {@link org.minidns.hla.srv.SrvService#xmpp_client} and {@link org.minidns.hla.srv.SrvService#xmpps_client} may be
167     * sorted together.
168     *
169     * @param resolvedSrvRecordCollections a collection of resolved SRV records.
170     * @return a list ordered by priority and weight of the related SRV records.
171     */
172    @SafeVarargs
173    public static List<ResolvedSrvRecord> sortMultiple(Collection<ResolvedSrvRecord>... resolvedSrvRecordCollections) {
174        int srvRecordsCount = 0;
175        for (Collection<ResolvedSrvRecord> resolvedSrvRecords : resolvedSrvRecordCollections) {
176            if (resolvedSrvRecords == null) {
177                continue;
178            }
179            srvRecordsCount += resolvedSrvRecords.size();
180        }
181
182        List<SRV> srvToSort = new ArrayList<>(srvRecordsCount);
183        Map<SRV, ResolvedSrvRecord> identityMap = new IdentityHashMap<>(srvRecordsCount);
184        for (Collection<ResolvedSrvRecord> resolvedSrvRecords : resolvedSrvRecordCollections) {
185            if (resolvedSrvRecords == null) {
186                continue;
187            }
188            for (ResolvedSrvRecord resolvedSrvRecord : resolvedSrvRecords) {
189                srvToSort.add(resolvedSrvRecord.srv);
190                identityMap.put(resolvedSrvRecord.srv, resolvedSrvRecord);
191            }
192        }
193
194        List<SRV> sortedSrvs = SrvUtil.sortSrvRecords(srvToSort);
195        assert sortedSrvs.size() == srvRecordsCount;
196
197        List<ResolvedSrvRecord> res = new ArrayList<>(srvRecordsCount);
198        for (SRV sortedSrv : sortedSrvs) {
199            ResolvedSrvRecord resolvedSrvRecord = identityMap.get(sortedSrv);
200            res.add(resolvedSrvRecord);
201        }
202
203        return res;
204    }
205}