/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "HTTPSSVC.h" #include "mozilla/net/DNS.h" #include "nsHttp.h" #include "nsHttpHandler.h" #include "nsNetAddr.h" #include "nsNetUtil.h" #include "nsIDNSService.h" namespace mozilla { namespace net { NS_IMPL_ISUPPORTS(SVCBRecord, nsISVCBRecord) class SvcParam : public nsISVCParam, public nsISVCParamAlpn, public nsISVCParamNoDefaultAlpn, public nsISVCParamPort, public nsISVCParamIPv4Hint, public nsISVCParamEchConfig, public nsISVCParamIPv6Hint, public nsISVCParamODoHConfig { NS_DECL_THREADSAFE_ISUPPORTS NS_DECL_NSISVCPARAM NS_DECL_NSISVCPARAMALPN NS_DECL_NSISVCPARAMNODEFAULTALPN NS_DECL_NSISVCPARAMPORT NS_DECL_NSISVCPARAMIPV4HINT NS_DECL_NSISVCPARAMECHCONFIG NS_DECL_NSISVCPARAMIPV6HINT NS_DECL_NSISVCPARAMODOHCONFIG public: explicit SvcParam(const SvcParamType& value) : mValue(value){}; private: virtual ~SvcParam() = default; SvcParamType mValue; }; NS_IMPL_ADDREF(SvcParam) NS_IMPL_RELEASE(SvcParam) NS_INTERFACE_MAP_BEGIN(SvcParam) NS_INTERFACE_MAP_ENTRY(nsISVCParam) NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsISVCParam) NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsISVCParamAlpn, mValue.is()) NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsISVCParamNoDefaultAlpn, mValue.is()) NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsISVCParamPort, mValue.is()) NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsISVCParamIPv4Hint, mValue.is()) NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsISVCParamEchConfig, mValue.is()) NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsISVCParamIPv6Hint, mValue.is()) NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsISVCParamODoHConfig, mValue.is()) NS_INTERFACE_MAP_END NS_IMETHODIMP SvcParam::GetType(uint16_t* aType) { *aType = mValue.match( [](Nothing&) { return SvcParamKeyMandatory; }, [](SvcParamAlpn&) { return SvcParamKeyAlpn; }, [](SvcParamNoDefaultAlpn&) { return SvcParamKeyNoDefaultAlpn; }, [](SvcParamPort&) { return SvcParamKeyPort; }, [](SvcParamIpv4Hint&) { return SvcParamKeyIpv4Hint; }, [](SvcParamEchConfig&) { return SvcParamKeyEchConfig; }, [](SvcParamIpv6Hint&) { return SvcParamKeyIpv6Hint; }, [](SvcParamODoHConfig&) { return SvcParamKeyODoHConfig; }); return NS_OK; } NS_IMETHODIMP SvcParam::GetAlpn(nsTArray& aAlpn) { if (!mValue.is()) { MOZ_ASSERT(false, "Unexpected type for variant"); return NS_ERROR_NOT_AVAILABLE; } aAlpn.AppendElements(mValue.as().mValue); return NS_OK; } NS_IMETHODIMP SvcParam::GetPort(uint16_t* aPort) { if (!mValue.is()) { MOZ_ASSERT(false, "Unexpected type for variant"); return NS_ERROR_NOT_AVAILABLE; } *aPort = mValue.as().mValue; return NS_OK; } NS_IMETHODIMP SvcParam::GetEchconfig(nsACString& aEchConfig) { if (!mValue.is()) { MOZ_ASSERT(false, "Unexpected type for variant"); return NS_ERROR_NOT_AVAILABLE; } aEchConfig = mValue.as().mValue; return NS_OK; } NS_IMETHODIMP SvcParam::GetIpv4Hint(nsTArray>& aIpv4Hint) { if (!mValue.is()) { MOZ_ASSERT(false, "Unexpected type for variant"); return NS_ERROR_NOT_AVAILABLE; } const auto& results = mValue.as().mValue; for (const auto& ip : results) { if (ip.raw.family != AF_INET) { return NS_ERROR_UNEXPECTED; } RefPtr hint = new nsNetAddr(&ip); aIpv4Hint.AppendElement(hint); } return NS_OK; } NS_IMETHODIMP SvcParam::GetIpv6Hint(nsTArray>& aIpv6Hint) { if (!mValue.is()) { MOZ_ASSERT(false, "Unexpected type for variant"); return NS_ERROR_NOT_AVAILABLE; } const auto& results = mValue.as().mValue; for (const auto& ip : results) { if (ip.raw.family != AF_INET6) { return NS_ERROR_UNEXPECTED; } RefPtr hint = new nsNetAddr(&ip); aIpv6Hint.AppendElement(hint); } return NS_OK; } NS_IMETHODIMP SvcParam::GetODoHConfig(nsACString& aODoHConfig) { if (!mValue.is()) { MOZ_ASSERT(false, "Unexpected type for variant"); return NS_ERROR_NOT_AVAILABLE; } aODoHConfig = mValue.as().mValue; return NS_OK; } bool SVCB::operator<(const SVCB& aOther) const { if (gHttpHandler->EchConfigEnabled()) { if (mHasEchConfig && !aOther.mHasEchConfig) { return true; } if (!mHasEchConfig && aOther.mHasEchConfig) { return false; } } return mSvcFieldPriority < aOther.mSvcFieldPriority; } Maybe SVCB::GetPort() const { Maybe port; for (const auto& value : mSvcFieldValue) { if (value.mValue.is()) { port.emplace(value.mValue.as().mValue); if (NS_FAILED(NS_CheckPortSafety(*port, "https"))) { *port = 0; } return port; } } return Nothing(); } bool SVCB::NoDefaultAlpn() const { for (const auto& value : mSvcFieldValue) { if (value.mValue.is()) { return true; } } return false; } void SVCB::GetIPHints(CopyableTArray& aAddresses) const { if (mSvcFieldPriority == 0) { return; } for (const auto& value : mSvcFieldValue) { if (value.mValue.is()) { aAddresses.AppendElements(value.mValue.as().mValue); } else if (value.mValue.is()) { aAddresses.AppendElements(value.mValue.as().mValue); } } } class AlpnComparator { public: bool Equals(const Tuple& aA, const Tuple& aB) const { return Get<1>(aA) == Get<1>(aB); } bool LessThan(const Tuple& aA, const Tuple& aB) const { return Get<1>(aA) > Get<1>(aB); } }; nsTArray> SVCB::GetAllAlpn() const { nsTArray> alpnList; for (const auto& value : mSvcFieldValue) { if (value.mValue.is()) { for (const auto& alpn : value.mValue.as().mValue) { alpnList.AppendElement(MakeTuple(alpn, IsAlpnSupported(alpn))); } } } alpnList.Sort(AlpnComparator()); return alpnList; } SVCBRecord::SVCBRecord(const SVCB& data, Maybe> aAlpn) : mData(data), mAlpn(aAlpn) { mPort = mData.GetPort(); } NS_IMETHODIMP SVCBRecord::GetPriority(uint16_t* aPriority) { *aPriority = mData.mSvcFieldPriority; return NS_OK; } NS_IMETHODIMP SVCBRecord::GetName(nsACString& aName) { aName = mData.mSvcDomainName; return NS_OK; } Maybe SVCBRecord::GetPort() { return mPort; } Maybe> SVCBRecord::GetAlpn() { return mAlpn; } NS_IMETHODIMP SVCBRecord::GetSelectedAlpn(nsACString& aAlpn) { if (!mAlpn) { return NS_ERROR_NOT_AVAILABLE; } aAlpn = Get<0>(*mAlpn); return NS_OK; } NS_IMETHODIMP SVCBRecord::GetEchConfig(nsACString& aEchConfig) { aEchConfig = mData.mEchConfig; return NS_OK; } NS_IMETHODIMP SVCBRecord::GetODoHConfig(nsACString& aODoHConfig) { aODoHConfig = mData.mODoHConfig; return NS_OK; } NS_IMETHODIMP SVCBRecord::GetValues(nsTArray>& aValues) { for (const auto& v : mData.mSvcFieldValue) { RefPtr param = new SvcParam(v.mValue); aValues.AppendElement(param); } return NS_OK; } NS_IMETHODIMP SVCBRecord::GetHasIPHintAddress(bool* aHasIPHintAddress) { *aHasIPHintAddress = mData.mHasIPHints; return NS_OK; } static bool CheckRecordIsUsable(const SVCB& aRecord, nsIDNSService* aDNSService, const nsACString& aHost, uint32_t& aExcludedCount) { if (!aHost.IsEmpty()) { bool excluded = false; if (NS_SUCCEEDED(aDNSService->IsSVCDomainNameFailed( aHost, aRecord.mSvcDomainName, &excluded)) && excluded) { // Skip if the domain name of this record was failed to connect before. ++aExcludedCount; return false; } } Maybe port = aRecord.GetPort(); if (port && *port == 0) { // Found an unsafe port, skip this record. return false; } return true; } static bool CheckAlpnIsUsable(SupportedAlpnRank aAlpnType, bool aNoHttp2, bool aNoHttp3, bool aCheckHttp3ExcludedList, const nsACString& aTargetName, uint32_t& aExcludedCount) { // Skip if this alpn is not supported. if (aAlpnType == SupportedAlpnRank::NOT_SUPPORTED) { return false; } // Skip if we don't want to use http2. if (aNoHttp2 && aAlpnType == SupportedAlpnRank::HTTP_2) { return false; } if (IsHttp3(aAlpnType)) { if (aCheckHttp3ExcludedList && gHttpHandler->IsHttp3Excluded(aTargetName)) { aExcludedCount++; return false; } if (aNoHttp3) { return false; } } return true; } static nsTArray FlattenRecords(const nsTArray& aRecords) { nsTArray result; for (const auto& record : aRecords) { nsTArray> alpnList = record.GetAllAlpn(); if (alpnList.IsEmpty()) { result.AppendElement(SVCBWrapper(record)); } else { for (const auto& alpn : alpnList) { SVCBWrapper wrapper(record); wrapper.mAlpn = Some(alpn); result.AppendElement(wrapper); } } } return result; } already_AddRefed DNSHTTPSSVCRecordBase::GetServiceModeRecordInternal( bool aNoHttp2, bool aNoHttp3, const nsTArray& aRecords, bool& aRecordsAllExcluded, bool aCheckHttp3ExcludedList) { RefPtr selectedRecord; RefPtr h3RecordWithEchConfig; uint32_t recordHasNoDefaultAlpnCount = 0; uint32_t recordExcludedCount = 0; aRecordsAllExcluded = false; nsCOMPtr dns = do_GetService(NS_DNSSERVICE_CONTRACTID); uint32_t h3ExcludedCount = 0; nsTArray records = FlattenRecords(aRecords); for (const auto& record : records) { if (record.mRecord.mSvcFieldPriority == 0) { // In ServiceMode, the SvcPriority should never be 0. return nullptr; } if (record.mRecord.NoDefaultAlpn()) { ++recordHasNoDefaultAlpnCount; } if (!CheckRecordIsUsable(record.mRecord, dns, mHost, recordExcludedCount)) { // Skip if this record is not usable. continue; } if (record.mAlpn) { if (!CheckAlpnIsUsable(Get<1>(*(record.mAlpn)), aNoHttp2, aNoHttp3, aCheckHttp3ExcludedList, record.mRecord.mSvcDomainName, h3ExcludedCount)) { continue; } if (IsHttp3(Get<1>(*(record.mAlpn)))) { // If the selected alpn is h3 and ech for h3 is disabled, we want // to find out if there is another non-h3 record that has // echConfig. If yes, we'll use the non-h3 record with echConfig // to connect. If not, we'll use h3 to connect without echConfig. if (record.mRecord.mHasEchConfig && (gHttpHandler->EchConfigEnabled() && !gHttpHandler->EchConfigEnabled(true))) { if (!h3RecordWithEchConfig) { // Save this h3 record for later use. h3RecordWithEchConfig = new SVCBRecord(record.mRecord, record.mAlpn); // Make sure the next record is not h3. aNoHttp3 = true; continue; } } } } if (!selectedRecord) { selectedRecord = new SVCBRecord(record.mRecord, record.mAlpn); } } if (!selectedRecord && !h3RecordWithEchConfig) { // If all records indicate "no-default-alpn", we should not use this RRSet. if (recordHasNoDefaultAlpnCount == records.Length()) { return nullptr; } if (recordExcludedCount == records.Length()) { aRecordsAllExcluded = true; return nullptr; } // If all records are in http3 excluded list, try again without checking the // excluded list. This is better than returning nothing. if (h3ExcludedCount == records.Length() && aCheckHttp3ExcludedList) { return GetServiceModeRecordInternal(aNoHttp2, aNoHttp3, aRecords, aRecordsAllExcluded, false); } } if (h3RecordWithEchConfig) { if (selectedRecord && selectedRecord->mData.mHasEchConfig) { return selectedRecord.forget(); } return h3RecordWithEchConfig.forget(); } return selectedRecord.forget(); } void DNSHTTPSSVCRecordBase::GetAllRecordsWithEchConfigInternal( bool aNoHttp2, bool aNoHttp3, const nsTArray& aRecords, bool* aAllRecordsHaveEchConfig, bool* aAllRecordsInH3ExcludedList, nsTArray>& aResult, bool aCheckHttp3ExcludedList) { if (aRecords.IsEmpty()) { return; } *aAllRecordsHaveEchConfig = aRecords[0].mHasEchConfig; *aAllRecordsInH3ExcludedList = false; // The first record should have echConfig. if (!(*aAllRecordsHaveEchConfig)) { return; } uint32_t h3ExcludedCount = 0; nsTArray records = FlattenRecords(aRecords); for (const auto& record : records) { if (record.mRecord.mSvcFieldPriority == 0) { // This should not happen, since GetAllRecordsWithEchConfigInternal() // should be called only if GetServiceModeRecordInternal() returns a // non-null record. MOZ_ASSERT(false); return; } // Records with echConfig are in front of records without echConfig, so we // don't have to continue. *aAllRecordsHaveEchConfig &= record.mRecord.mHasEchConfig; if (!(*aAllRecordsHaveEchConfig)) { aResult.Clear(); return; } Maybe port = record.mRecord.GetPort(); if (port && *port == 0) { // Found an unsafe port, skip this record. continue; } if (record.mAlpn) { if (!CheckAlpnIsUsable(Get<1>(*(record.mAlpn)), aNoHttp2, aNoHttp3, aCheckHttp3ExcludedList, record.mRecord.mSvcDomainName, h3ExcludedCount)) { continue; } } RefPtr svcbRecord = new SVCBRecord(record.mRecord, record.mAlpn); aResult.AppendElement(svcbRecord); } // If all records are in http3 excluded list, try again without checking the // excluded list. This is better than returning nothing. if (h3ExcludedCount == records.Length() && aCheckHttp3ExcludedList) { GetAllRecordsWithEchConfigInternal( aNoHttp2, aNoHttp3, aRecords, aAllRecordsHaveEchConfig, aAllRecordsInH3ExcludedList, aResult, false); *aAllRecordsInH3ExcludedList = true; } } bool DNSHTTPSSVCRecordBase::HasIPAddressesInternal( const nsTArray& aRecords) { for (const SVCB& record : aRecords) { if (record.mSvcFieldPriority != 0) { for (const auto& value : record.mSvcFieldValue) { if (value.mValue.is()) { return true; } if (value.mValue.is()) { return true; } } } } return false; } } // namespace net } // namespace mozilla