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