| /* |
| * |
| * Copyright (c) 2021 Project CHIP Authors |
| * All rights reserved. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| /** |
| * @file |
| * Implementation of SetUp Code Pairer, a class that parses a given |
| * setup code and uses the extracted informations to discover and |
| * filter commissionables nodes, before initiating the pairing process. |
| * |
| */ |
| |
| #include <controller/SetUpCodePairer.h> |
| |
| #include <controller/CHIPDeviceController.h> |
| #include <lib/dnssd/Resolver.h> |
| #include <lib/support/CodeUtils.h> |
| #include <system/SystemClock.h> |
| |
| constexpr uint32_t kDeviceDiscoveredTimeout = CHIP_CONFIG_SETUP_CODE_PAIRER_DISCOVERY_TIMEOUT_SECS * chip::kMillisecondsPerSecond; |
| |
| namespace chip { |
| namespace Controller { |
| |
| CHIP_ERROR SetUpCodePairer::PairDevice(NodeId remoteId, const char * setUpCode, SetupCodePairerBehaviour commission, |
| DiscoveryType discoveryType) |
| { |
| VerifyOrReturnError(mSystemLayer != nullptr, CHIP_ERROR_INCORRECT_STATE); |
| |
| SetupPayload payload; |
| mConnectionType = commission; |
| mDiscoveryType = discoveryType; |
| |
| bool isQRCode = strncmp(setUpCode, kQRCodePrefix, strlen(kQRCodePrefix)) == 0; |
| if (isQRCode) |
| { |
| ReturnErrorOnFailure(QRCodeSetupPayloadParser(setUpCode).populatePayload(payload)); |
| VerifyOrReturnError(payload.isValidQRCodePayload(), CHIP_ERROR_INVALID_ARGUMENT); |
| } |
| else |
| { |
| ReturnErrorOnFailure(ManualSetupPayloadParser(setUpCode).populatePayload(payload)); |
| VerifyOrReturnError(payload.isValidManualCode(), CHIP_ERROR_INVALID_ARGUMENT); |
| } |
| |
| mRemoteId = remoteId; |
| mSetUpPINCode = payload.setUpPINCode; |
| |
| ResetDiscoveryState(); |
| |
| ReturnErrorOnFailure(Connect(payload)); |
| |
| return mSystemLayer->StartTimer(System::Clock::Milliseconds32(kDeviceDiscoveredTimeout), OnDeviceDiscoveredTimeoutCallback, |
| this); |
| } |
| |
| CHIP_ERROR SetUpCodePairer::Connect(SetupPayload & payload) |
| { |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| bool isRunning = false; |
| |
| bool searchOverAll = !payload.rendezvousInformation.HasValue(); |
| |
| if (mDiscoveryType == DiscoveryType::kAll) |
| { |
| if (searchOverAll || payload.rendezvousInformation.Value().Has(RendezvousInformationFlag::kBLE)) |
| { |
| if (CHIP_NO_ERROR == (err = StartDiscoverOverBle(payload))) |
| { |
| isRunning = true; |
| } |
| VerifyOrReturnError(searchOverAll || CHIP_NO_ERROR == err || CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE == err, err); |
| } |
| |
| if (searchOverAll || payload.rendezvousInformation.Value().Has(RendezvousInformationFlag::kSoftAP)) |
| { |
| if (CHIP_NO_ERROR == (err = StartDiscoverOverSoftAP(payload))) |
| { |
| isRunning = true; |
| } |
| VerifyOrReturnError(searchOverAll || CHIP_NO_ERROR == err || CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE == err, err); |
| } |
| } |
| |
| // We always want to search on network because any node that has already been commissioned will use on-network regardless of the |
| // QR code flag. |
| if (CHIP_NO_ERROR == (err = StartDiscoverOverIP(payload))) |
| { |
| isRunning = true; |
| } |
| VerifyOrReturnError(searchOverAll || CHIP_NO_ERROR == err, err); |
| |
| return isRunning ? CHIP_NO_ERROR : CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE; |
| } |
| |
| CHIP_ERROR SetUpCodePairer::StartDiscoverOverBle(SetupPayload & payload) |
| { |
| #if CONFIG_NETWORK_LAYER_BLE |
| #if CHIP_DEVICE_CONFIG_ENABLE_BOTH_COMMISSIONER_AND_COMMISSIONEE |
| VerifyOrReturnError(mCommissioner != nullptr, CHIP_ERROR_INCORRECT_STATE); |
| mCommissioner->ConnectBleTransportToSelf(); |
| #endif // CHIP_DEVICE_CONFIG_ENABLE_BOTH_COMMISSIONER_AND_COMMISSIONEE |
| VerifyOrReturnError(mBleLayer != nullptr, CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE); |
| |
| ChipLogProgress(Controller, "Starting commissioning discovery over BLE"); |
| |
| // Handle possibly-sync callbacks. |
| mWaitingForDiscovery[kBLETransport] = true; |
| CHIP_ERROR err = mBleLayer->NewBleConnectionByDiscriminator(payload.discriminator, this, OnDiscoveredDeviceOverBleSuccess, |
| OnDiscoveredDeviceOverBleError); |
| if (err != CHIP_NO_ERROR) |
| { |
| mWaitingForDiscovery[kBLETransport] = false; |
| } |
| return err; |
| #else |
| return CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE; |
| #endif // CONFIG_NETWORK_LAYER_BLE |
| } |
| |
| CHIP_ERROR SetUpCodePairer::StopConnectOverBle() |
| { |
| // Make sure to not call CancelBleIncompleteConnection unless we are in fact |
| // waiting on BLE discovery. It will cancel connections that are in fact |
| // completed. In particular, if we just established PASE over BLE calling |
| // CancelBleIncompleteConnection here unconditionally would cancel the BLE |
| // connection underlying the PASE session. So make sure to only call |
| // CancelBleIncompleteConnection if we're still waiting to hear back on the |
| // BLE discovery bits. |
| if (!mWaitingForDiscovery[kBLETransport]) |
| { |
| return CHIP_NO_ERROR; |
| } |
| |
| mWaitingForDiscovery[kBLETransport] = false; |
| #if CONFIG_NETWORK_LAYER_BLE |
| VerifyOrReturnError(mBleLayer != nullptr, CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE); |
| ChipLogDetail(Controller, "Stopping commissioning discovery over BLE"); |
| return mBleLayer->CancelBleIncompleteConnection(); |
| #else |
| return CHIP_NO_ERROR; |
| #endif // CONFIG_NETWORK_LAYER_BLE |
| } |
| |
| CHIP_ERROR SetUpCodePairer::StartDiscoverOverIP(SetupPayload & payload) |
| { |
| ChipLogProgress(Controller, "Starting commissioning discovery over DNS-SD"); |
| |
| auto & discriminator = payload.discriminator; |
| if (discriminator.IsShortDiscriminator()) |
| { |
| mCurrentFilter.type = Dnssd::DiscoveryFilterType::kShortDiscriminator; |
| mCurrentFilter.code = discriminator.GetShortValue(); |
| } |
| else |
| { |
| mCurrentFilter.type = Dnssd::DiscoveryFilterType::kLongDiscriminator; |
| mCurrentFilter.code = discriminator.GetLongValue(); |
| } |
| mPayloadVendorID = payload.vendorID; |
| mPayloadProductID = payload.productID; |
| |
| // Handle possibly-sync callbacks. |
| mWaitingForDiscovery[kIPTransport] = true; |
| CHIP_ERROR err = mCommissioner->DiscoverCommissionableNodes(mCurrentFilter); |
| if (err != CHIP_NO_ERROR) |
| { |
| mWaitingForDiscovery[kIPTransport] = false; |
| } |
| return err; |
| } |
| |
| CHIP_ERROR SetUpCodePairer::StopConnectOverIP() |
| { |
| ChipLogDetail(Controller, "Stopping commissioning discovery over DNS-SD"); |
| |
| mWaitingForDiscovery[kIPTransport] = false; |
| mCurrentFilter.type = Dnssd::DiscoveryFilterType::kNone; |
| mPayloadVendorID = kNotAvailable; |
| mPayloadProductID = kNotAvailable; |
| |
| mCommissioner->StopCommissionableDiscovery(); |
| return CHIP_NO_ERROR; |
| } |
| |
| CHIP_ERROR SetUpCodePairer::StartDiscoverOverSoftAP(SetupPayload & payload) |
| { |
| return CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE; |
| } |
| |
| CHIP_ERROR SetUpCodePairer::StopConnectOverSoftAP() |
| { |
| mWaitingForDiscovery[kSoftAPTransport] = false; |
| return CHIP_NO_ERROR; |
| } |
| |
| bool SetUpCodePairer::ConnectToDiscoveredDevice() |
| { |
| if (mWaitingForPASE) |
| { |
| // Nothing to do. Just wait until we either succeed or fail at that |
| // PASE session establishment. |
| return false; |
| } |
| |
| if (!mDiscoveredParameters.empty()) |
| { |
| // Grab the first element from the queue and try connecting to it. |
| // Remove it from the queue before we try to connect, in case the |
| // connection attempt fails and calls right back into us to try the next |
| // thing. |
| SetUpCodePairerParameters params(mDiscoveredParameters.front()); |
| mDiscoveredParameters.pop(); |
| |
| params.SetSetupPINCode(mSetUpPINCode); |
| |
| #if CHIP_PROGRESS_LOGGING |
| char buf[Transport::PeerAddress::kMaxToStringSize]; |
| params.GetPeerAddress().ToString(buf); |
| ChipLogProgress(Controller, "Attempting PASE connection to %s", buf); |
| #endif // CHIP_PROGRESS_LOGGING |
| |
| // Handle possibly-sync call backs from attempts to establish PASE. |
| ExpectPASEEstablishment(); |
| |
| if (params.GetPeerAddress().GetTransportType() == Transport::Type::kUdp) |
| { |
| mCurrentPASEParameters.SetValue(params); |
| } |
| |
| CHIP_ERROR err; |
| if (mConnectionType == SetupCodePairerBehaviour::kCommission) |
| { |
| err = mCommissioner->PairDevice(mRemoteId, params); |
| } |
| else |
| { |
| err = mCommissioner->EstablishPASEConnection(mRemoteId, params); |
| } |
| |
| LogErrorOnFailure(err); |
| if (err == CHIP_NO_ERROR) |
| { |
| return true; |
| } |
| |
| // Failed to start establishing PASE. |
| PASEEstablishmentComplete(); |
| } |
| |
| return false; |
| } |
| |
| #if CONFIG_NETWORK_LAYER_BLE |
| void SetUpCodePairer::OnDiscoveredDeviceOverBle(BLE_CONNECTION_OBJECT connObj) |
| { |
| ChipLogProgress(Controller, "Discovered device to be commissioned over BLE"); |
| |
| mWaitingForDiscovery[kBLETransport] = false; |
| |
| mDiscoveredParameters.emplace(connObj); |
| ConnectToDiscoveredDevice(); |
| } |
| |
| void SetUpCodePairer::OnDiscoveredDeviceOverBleSuccess(void * appState, BLE_CONNECTION_OBJECT connObj) |
| { |
| (static_cast<SetUpCodePairer *>(appState))->OnDiscoveredDeviceOverBle(connObj); |
| } |
| |
| void SetUpCodePairer::OnDiscoveredDeviceOverBleError(void * appState, CHIP_ERROR err) |
| { |
| static_cast<SetUpCodePairer *>(appState)->OnBLEDiscoveryError(err); |
| } |
| |
| void SetUpCodePairer::OnBLEDiscoveryError(CHIP_ERROR err) |
| { |
| ChipLogError(Controller, "Commissioning discovery over BLE failed: %" CHIP_ERROR_FORMAT, err.Format()); |
| mWaitingForDiscovery[kBLETransport] = false; |
| LogErrorOnFailure(err); |
| } |
| #endif // CONFIG_NETWORK_LAYER_BLE |
| |
| bool SetUpCodePairer::IdIsPresent(uint16_t vendorOrProductID) |
| { |
| return vendorOrProductID != kNotAvailable; |
| } |
| |
| bool SetUpCodePairer::NodeMatchesCurrentFilter(const Dnssd::DiscoveredNodeData & nodeData) const |
| { |
| if (nodeData.commissionData.commissioningMode == 0) |
| { |
| return false; |
| } |
| |
| // The advertisement may not include a vendor id. |
| if (IdIsPresent(mPayloadVendorID) && IdIsPresent(nodeData.commissionData.vendorId) && |
| mPayloadVendorID != nodeData.commissionData.vendorId) |
| { |
| return false; |
| } |
| |
| // The advertisement may not include a product id. |
| if (IdIsPresent(mPayloadProductID) && IdIsPresent(nodeData.commissionData.productId) && |
| mPayloadProductID != nodeData.commissionData.productId) |
| { |
| return false; |
| } |
| |
| switch (mCurrentFilter.type) |
| { |
| case Dnssd::DiscoveryFilterType::kShortDiscriminator: |
| return ((nodeData.commissionData.longDiscriminator >> 8) & 0x0F) == mCurrentFilter.code; |
| case Dnssd::DiscoveryFilterType::kLongDiscriminator: |
| return nodeData.commissionData.longDiscriminator == mCurrentFilter.code; |
| default: |
| return false; |
| } |
| return false; |
| } |
| |
| void SetUpCodePairer::NotifyCommissionableDeviceDiscovered(const Dnssd::DiscoveredNodeData & nodeData) |
| { |
| if (!NodeMatchesCurrentFilter(nodeData)) |
| { |
| return; |
| } |
| |
| ChipLogProgress(Controller, "Discovered device to be commissioned over DNS-SD"); |
| |
| mDiscoveredParameters.emplace(nodeData.resolutionData); |
| ConnectToDiscoveredDevice(); |
| } |
| |
| void SetUpCodePairer::CommissionerShuttingDown() |
| { |
| ResetDiscoveryState(); |
| } |
| |
| bool SetUpCodePairer::TryNextRendezvousParameters() |
| { |
| if (ConnectToDiscoveredDevice()) |
| { |
| ChipLogProgress(Controller, "Trying connection to commissionee over different transport"); |
| return true; |
| } |
| |
| if (DiscoveryInProgress()) |
| { |
| ChipLogProgress(Controller, "Waiting to discover commissionees that match our filters"); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool SetUpCodePairer::DiscoveryInProgress() const |
| { |
| for (const auto & waiting : mWaitingForDiscovery) |
| { |
| if (waiting) |
| { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| void SetUpCodePairer::ResetDiscoveryState() |
| { |
| StopConnectOverBle(); |
| StopConnectOverIP(); |
| StopConnectOverSoftAP(); |
| |
| // Just in case any of those failed to reset the waiting state properly. |
| for (auto & waiting : mWaitingForDiscovery) |
| { |
| waiting = false; |
| } |
| |
| while (!mDiscoveredParameters.empty()) |
| { |
| mDiscoveredParameters.pop(); |
| } |
| |
| mCurrentPASEParameters.ClearValue(); |
| mLastPASEError = CHIP_NO_ERROR; |
| } |
| |
| void SetUpCodePairer::ExpectPASEEstablishment() |
| { |
| mWaitingForPASE = true; |
| auto * delegate = mCommissioner->GetPairingDelegate(); |
| if (this == delegate) |
| { |
| // This should really not happen, but if it does, do nothing, to avoid |
| // delegate loops. |
| return; |
| } |
| |
| mPairingDelegate = delegate; |
| mCommissioner->RegisterPairingDelegate(this); |
| } |
| |
| void SetUpCodePairer::PASEEstablishmentComplete() |
| { |
| mWaitingForPASE = false; |
| mCommissioner->RegisterPairingDelegate(mPairingDelegate); |
| mPairingDelegate = nullptr; |
| } |
| |
| void SetUpCodePairer::OnStatusUpdate(DevicePairingDelegate::Status status) |
| { |
| if (status == DevicePairingDelegate::Status::SecurePairingFailed) |
| { |
| // If we're still waiting on discovery, don't propagate this failure |
| // (which is due to PASE failure with something we discovered, but the |
| // "something" may not have been the right thing) for now. Wait until |
| // discovery completes. Then we will either succeed and notify |
| // accordingly or time out and land in OnStatusUpdate again, but at that |
| // point we will not be waiting on discovery anymore. |
| if (!mDiscoveredParameters.empty()) |
| { |
| ChipLogProgress(Controller, "Ignoring SecurePairingFailed status for now; we have more discovered devices to try"); |
| status = DevicePairingDelegate::Status::SecurePairingDiscoveringMoreDevices; |
| } |
| |
| if (DiscoveryInProgress()) |
| { |
| ChipLogProgress(Controller, |
| "Ignoring SecurePairingFailed status for now; we are waiting to see if we discover more devices"); |
| status = DevicePairingDelegate::Status::SecurePairingDiscoveringMoreDevices; |
| } |
| } |
| |
| if (mPairingDelegate) |
| { |
| mPairingDelegate->OnStatusUpdate(status); |
| } |
| } |
| |
| void SetUpCodePairer::OnPairingComplete(CHIP_ERROR error) |
| { |
| // Save the pairing delegate so we can notify it. We want to notify it |
| // _after_ we restore the state on the commissioner, in case the delegate |
| // ends up immediately calling back into the commissioner again when |
| // notified. |
| auto * pairingDelegate = mPairingDelegate; |
| PASEEstablishmentComplete(); |
| |
| if (CHIP_NO_ERROR == error) |
| { |
| mSystemLayer->CancelTimer(OnDeviceDiscoveredTimeoutCallback, this); |
| |
| ResetDiscoveryState(); |
| if (pairingDelegate != nullptr) |
| { |
| pairingDelegate->OnPairingComplete(error); |
| } |
| return; |
| } |
| |
| // It may happen that there is a stale DNS entry. If so, ReconfirmRecord will flush |
| // the record from the daemon cache once it determines that it is invalid. |
| // It may not help for this particular resolve, but may help subsequent resolves. |
| if (CHIP_ERROR_TIMEOUT == error && mCurrentPASEParameters.HasValue()) |
| { |
| const auto & params = mCurrentPASEParameters.Value(); |
| auto & ip = params.GetPeerAddress().GetIPAddress(); |
| auto err = Dnssd::Resolver::Instance().ReconfirmRecord(params.mHostName, ip, params.mInterfaceId); |
| if (CHIP_NO_ERROR != err && CHIP_ERROR_NOT_IMPLEMENTED != err) |
| { |
| ChipLogError(Controller, "Error when verifying the validity of an address: %" CHIP_ERROR_FORMAT, err.Format()); |
| } |
| } |
| mCurrentPASEParameters.ClearValue(); |
| |
| // We failed to establish PASE. Try the next thing we have discovered, if |
| // any. |
| if (TryNextRendezvousParameters()) |
| { |
| // Keep waiting until that finishes. Don't call OnPairingComplete yet. |
| mLastPASEError = error; |
| return; |
| } |
| |
| if (pairingDelegate != nullptr) |
| { |
| pairingDelegate->OnPairingComplete(error); |
| } |
| } |
| |
| void SetUpCodePairer::OnPairingDeleted(CHIP_ERROR error) |
| { |
| if (mPairingDelegate) |
| { |
| mPairingDelegate->OnPairingDeleted(error); |
| } |
| } |
| |
| void SetUpCodePairer::OnCommissioningComplete(NodeId deviceId, CHIP_ERROR error) |
| { |
| // Not really expecting this, but handle it anyway. |
| if (mPairingDelegate) |
| { |
| mPairingDelegate->OnCommissioningComplete(deviceId, error); |
| } |
| } |
| |
| void SetUpCodePairer::OnDeviceDiscoveredTimeoutCallback(System::Layer * layer, void * context) |
| { |
| ChipLogError(Controller, "Discovery timed out"); |
| auto * pairer = static_cast<SetUpCodePairer *>(context); |
| LogErrorOnFailure(pairer->StopConnectOverBle()); |
| LogErrorOnFailure(pairer->StopConnectOverIP()); |
| LogErrorOnFailure(pairer->StopConnectOverSoftAP()); |
| if (!pairer->mWaitingForPASE && pairer->mDiscoveredParameters.empty()) |
| { |
| // We're not waiting on any more PASE attempts, and we're not going to |
| // discover anything at this point, so we should just notify our |
| // listener. |
| CHIP_ERROR err = pairer->mLastPASEError; |
| if (err == CHIP_NO_ERROR) |
| { |
| err = CHIP_ERROR_TIMEOUT; |
| } |
| pairer->mCommissioner->OnSessionEstablishmentError(err); |
| } |
| } |
| |
| SetUpCodePairerParameters::SetUpCodePairerParameters(const Dnssd::CommonResolutionData & data) |
| { |
| mInterfaceId = data.interfaceId; |
| Platform::CopyString(mHostName, data.hostName); |
| |
| auto & ip = data.ipAddress[0]; |
| SetPeerAddress(Transport::PeerAddress::UDP(ip, data.port, ip.IsIPv6LinkLocal() ? data.interfaceId : Inet::InterfaceId::Null())); |
| |
| if (data.mrpRetryIntervalIdle.HasValue()) |
| { |
| SetIdleInterval(data.mrpRetryIntervalIdle.Value()); |
| } |
| |
| if (data.mrpRetryIntervalActive.HasValue()) |
| { |
| SetActiveInterval(data.mrpRetryIntervalActive.Value()); |
| } |
| } |
| |
| #if CONFIG_NETWORK_LAYER_BLE |
| SetUpCodePairerParameters::SetUpCodePairerParameters(BLE_CONNECTION_OBJECT connObj) |
| { |
| Transport::PeerAddress peerAddress = Transport::PeerAddress::BLE(); |
| SetPeerAddress(peerAddress).SetConnectionObject(connObj); |
| } |
| #endif // CONFIG_NETWORK_LAYER_BLE |
| |
| } // namespace Controller |
| } // namespace chip |