Apply Upstream code (2021-03-15)
[platform/upstream/connectedhomeip.git] / src / app / server / Server.cpp
1 /*
2  *
3  *    Copyright (c) 2020 Project CHIP Authors
4  *
5  *    Licensed under the Apache License, Version 2.0 (the "License");
6  *    you may not use this file except in compliance with the License.
7  *    You may obtain a copy of the License at
8  *
9  *        http://www.apache.org/licenses/LICENSE-2.0
10  *
11  *    Unless required by applicable law or agreed to in writing, software
12  *    distributed under the License is distributed on an "AS IS" BASIS,
13  *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  *    See the License for the specific language governing permissions and
15  *    limitations under the License.
16  */
17
18 #include <app/server/Server.h>
19
20 #include <app/InteractionModelEngine.h>
21 #include <app/server/DataModelHandler.h>
22 #include <app/server/EchoHandler.h>
23 #include <app/server/RendezvousServer.h>
24 #include <app/server/SessionManager.h>
25
26 #include <ble/BLEEndPoint.h>
27 #include <core/CHIPPersistentStorageDelegate.h>
28 #include <inet/IPAddress.h>
29 #include <inet/InetError.h>
30 #include <inet/InetLayer.h>
31 #include <messaging/ExchangeMgr.h>
32 #include <platform/CHIPDeviceLayer.h>
33 #include <platform/KeyValueStoreManager.h>
34 #include <setup_payload/SetupPayload.h>
35 #include <support/CodeUtils.h>
36 #include <support/ErrorStr.h>
37 #include <support/ReturnMacros.h>
38 #include <support/logging/CHIPLogging.h>
39 #include <sys/param.h>
40 #include <system/SystemPacketBuffer.h>
41 #include <system/TLVPacketBufferBackingStore.h>
42 #include <transport/SecureSessionMgr.h>
43 #include <transport/StorablePeerConnection.h>
44
45 #include "Mdns.h"
46
47 using namespace ::chip;
48 using namespace ::chip::Inet;
49 using namespace ::chip::Transport;
50 using namespace ::chip::DeviceLayer;
51 using namespace ::chip::Messaging;
52
53 namespace {
54
55 constexpr bool isRendezvousBypassed()
56 {
57 #if defined(CHIP_BYPASS_RENDEZVOUS) && CHIP_BYPASS_RENDEZVOUS
58     return true;
59 #elif defined(CONFIG_RENDEZVOUS_MODE)
60     return static_cast<RendezvousInformationFlags>(CONFIG_RENDEZVOUS_MODE) == RendezvousInformationFlags::kNone;
61 #else
62     return false;
63 #endif
64 }
65
66 constexpr bool useTestPairing()
67 {
68     // Use the test pairing whenever rendezvous is bypassed. Otherwise, there wouldn't be
69     // any way to communicate with the device using CHIP protocol.
70     return isRendezvousBypassed();
71 }
72
73 class ServerStorageDelegate : public PersistentStorageDelegate
74 {
75     void SetDelegate(PersistentStorageResultDelegate * delegate) override
76     {
77         ChipLogError(AppServer, "ServerStorageDelegate does not support async operations");
78         chipDie();
79     }
80
81     void GetKeyValue(const char * key) override
82     {
83         ChipLogError(AppServer, "ServerStorageDelegate does not support async operations");
84         chipDie();
85     }
86
87     void SetKeyValue(const char * key, const char * value) override
88     {
89         ChipLogError(AppServer, "ServerStorageDelegate does not support async operations");
90         chipDie();
91     }
92
93     CHIP_ERROR GetKeyValue(const char * key, void * buffer, uint16_t & size) override
94     {
95         return PersistedStorage::KeyValueStoreMgr().Get(key, buffer, size);
96     }
97
98     CHIP_ERROR SetKeyValue(const char * key, const void * value, uint16_t size) override
99     {
100         return PersistedStorage::KeyValueStoreMgr().Put(key, value, size);
101     }
102
103     void DeleteKeyValue(const char * key) override { PersistedStorage::KeyValueStoreMgr().Delete(key); }
104 };
105
106 ServerStorageDelegate gServerStorage;
107
108 CHIP_ERROR PersistAdminPairingToKVS(AdminPairingInfo * admin, AdminId nextAvailableId)
109 {
110     ReturnErrorCodeIf(admin == nullptr, CHIP_ERROR_INVALID_ARGUMENT);
111     ChipLogProgress(AppServer, "Persisting admin ID %d, next available %d", admin->GetAdminId(), nextAvailableId);
112
113     ReturnErrorOnFailure(admin->StoreIntoKVS(gServerStorage));
114     ReturnErrorOnFailure(PersistedStorage::KeyValueStoreMgr().Put(kAdminTableCountKey, &nextAvailableId, sizeof(nextAvailableId)));
115
116     ChipLogProgress(AppServer, "Persisting admin ID successfully");
117     return CHIP_NO_ERROR;
118 }
119
120 CHIP_ERROR RestoreAllAdminPairingsFromKVS(AdminPairingTable & adminPairings, AdminId & nextAvailableId)
121 {
122     // It's not an error if the key doesn't exist. Just return right away.
123     VerifyOrReturnError(PersistedStorage::KeyValueStoreMgr().Get(kAdminTableCountKey, &nextAvailableId) == CHIP_NO_ERROR,
124                         CHIP_NO_ERROR);
125     ChipLogProgress(AppServer, "Next available admin ID is %d", nextAvailableId);
126
127     // TODO: The admin ID space allocation should be re-evaluated. With the current approach, the space could be
128     //       exhausted while IDs are still available (e.g. if the admin IDs are allocated and freed over a period of time).
129     //       Also, the current approach can make ID lookup slower as more IDs are allocated and freed.
130     for (AdminId id = 0; id < nextAvailableId; id++)
131     {
132         AdminPairingInfo * admin = adminPairings.AssignAdminId(id);
133         // Recreate the binding if one exists in persistent storage. Else skip to the next ID
134         if (admin->FetchFromKVS(gServerStorage) != CHIP_NO_ERROR)
135         {
136             adminPairings.ReleaseAdminId(id);
137         }
138         else
139         {
140             ChipLogProgress(AppServer, "Found admin pairing for %d, node ID %llu", admin->GetAdminId(), admin->GetNodeId());
141         }
142     }
143
144     return CHIP_NO_ERROR;
145 }
146
147 void EraseAllAdminPairingsUpTo(AdminId nextAvailableId)
148 {
149     PersistedStorage::KeyValueStoreMgr().Delete(kAdminTableCountKey);
150
151     for (AdminId id = 0; id < nextAvailableId; id++)
152     {
153         AdminPairingInfo::DeleteFromKVS(gServerStorage, id);
154     }
155 }
156
157 static CHIP_ERROR RestoreAllSessionsFromKVS(SecureSessionMgr & sessionMgr, RendezvousServer & server)
158 {
159     uint16_t nextSessionKeyId = 0;
160     // It's not an error if the key doesn't exist. Just return right away.
161     VerifyOrReturnError(PersistedStorage::KeyValueStoreMgr().Get(kStorablePeerConnectionCountKey, &nextSessionKeyId) ==
162                             CHIP_NO_ERROR,
163                         CHIP_NO_ERROR);
164     ChipLogProgress(AppServer, "Found %d stored connections", nextSessionKeyId);
165
166     PASESession * session = chip::Platform::New<PASESession>();
167     VerifyOrReturnError(session != nullptr, CHIP_ERROR_NO_MEMORY);
168
169     for (uint16_t keyId = 0; keyId < nextSessionKeyId; keyId++)
170     {
171         StorablePeerConnection connection;
172         if (CHIP_NO_ERROR == connection.FetchFromKVS(gServerStorage, keyId))
173         {
174             connection.GetPASESession(session);
175
176             ChipLogProgress(AppServer, "Fetched the session information: from %llu", session->PeerConnection().GetPeerNodeId());
177             sessionMgr.NewPairing(Optional<Transport::PeerAddress>::Value(session->PeerConnection().GetPeerAddress()),
178                                   session->PeerConnection().GetPeerNodeId(), session,
179                                   SecureSessionMgr::PairingDirection::kResponder, connection.GetAdminId(), nullptr);
180             session->Clear();
181         }
182     }
183
184     chip::Platform::Delete(session);
185
186     server.GetRendezvousSession()->SetNextKeyId(nextSessionKeyId);
187     return CHIP_NO_ERROR;
188 }
189
190 void EraseAllSessionsUpTo(uint16_t nextSessionKeyId)
191 {
192     PersistedStorage::KeyValueStoreMgr().Delete(kStorablePeerConnectionCountKey);
193
194     for (uint16_t keyId = 0; keyId < nextSessionKeyId; keyId++)
195     {
196         StorablePeerConnection::DeleteFromKVS(gServerStorage, keyId);
197     }
198 }
199
200 // TODO: The following class is setting the discriminator in Persistent Storage. This is
201 //       is needed since BLE reads the discriminator using ConfigurationMgr APIs. The
202 //       better solution will be to pass the discriminator to BLE without changing it
203 //       in the persistent storage.
204 //       https://github.com/project-chip/connectedhomeip/issues/4767
205 class DeviceDiscriminatorCache
206 {
207 public:
208     CHIP_ERROR UpdateDiscriminator(uint16_t discriminator)
209     {
210         if (!mOriginalDiscriminatorCached)
211         {
212             // Cache the original discriminator
213             ReturnErrorOnFailure(DeviceLayer::ConfigurationMgr().GetSetupDiscriminator(mOriginalDiscriminator));
214             mOriginalDiscriminatorCached = true;
215         }
216
217         return DeviceLayer::ConfigurationMgr().StoreSetupDiscriminator(discriminator);
218     }
219
220     CHIP_ERROR RestoreDiscriminator()
221     {
222         if (mOriginalDiscriminatorCached)
223         {
224             // Restore the original discriminator
225             ReturnErrorOnFailure(DeviceLayer::ConfigurationMgr().StoreSetupDiscriminator(mOriginalDiscriminator));
226             mOriginalDiscriminatorCached = false;
227         }
228
229         return CHIP_NO_ERROR;
230     }
231
232 private:
233     bool mOriginalDiscriminatorCached = false;
234     uint16_t mOriginalDiscriminator   = 0;
235 };
236
237 DeviceDiscriminatorCache gDeviceDiscriminatorCache;
238 AdminPairingTable gAdminPairings;
239 AdminId gNextAvailableAdminId = 0;
240
241 class ServerRendezvousAdvertisementDelegate : public RendezvousAdvertisementDelegate
242 {
243 public:
244     CHIP_ERROR StartAdvertisement() const override
245     {
246         ReturnErrorOnFailure(chip::DeviceLayer::ConnectivityMgr().SetBLEAdvertisingEnabled(true));
247         if (mDelegate != nullptr)
248         {
249             mDelegate->OnPairingWindowOpened();
250         }
251         return CHIP_NO_ERROR;
252     }
253     CHIP_ERROR StopAdvertisement() const override
254     {
255         gDeviceDiscriminatorCache.RestoreDiscriminator();
256
257         ReturnErrorOnFailure(chip::DeviceLayer::ConnectivityMgr().SetBLEAdvertisingEnabled(false));
258         {
259             if (mDelegate != nullptr)
260                 mDelegate->OnPairingWindowClosed();
261         }
262
263         AdminPairingInfo * admin = gAdminPairings.FindAdmin(mAdmin);
264         if (admin != nullptr)
265         {
266             ReturnErrorOnFailure(PersistAdminPairingToKVS(admin, gNextAvailableAdminId));
267         }
268
269         return CHIP_NO_ERROR;
270     }
271
272     void RendezvousComplete() const override
273     {
274         // Once rendezvous completed, assume we are operational
275         if (app::Mdns::AdvertiseOperational() != CHIP_NO_ERROR)
276         {
277             ChipLogError(Discovery, "Failed to start advertising operational state at rendezvous completion time.");
278         }
279     }
280
281     void SetDelegate(AppDelegate * delegate) { mDelegate = delegate; }
282
283     void SetAdminId(AdminId id) { mAdmin = id; }
284
285 private:
286     AppDelegate * mDelegate = nullptr;
287     AdminId mAdmin;
288 };
289
290 DemoTransportMgr gTransports;
291 SecureSessionMgr gSessions;
292 RendezvousServer gRendezvousServer;
293
294 ServerRendezvousAdvertisementDelegate gAdvDelegate;
295
296 static CHIP_ERROR OpenPairingWindowUsingVerifier(uint16_t discriminator, PASEVerifier & verifier)
297 {
298     RendezvousParameters params;
299
300     ReturnErrorOnFailure(gDeviceDiscriminatorCache.UpdateDiscriminator(discriminator));
301
302 #if CONFIG_NETWORK_LAYER_BLE
303     params.SetPASEVerifier(verifier)
304         .SetBleLayer(DeviceLayer::ConnectivityMgr().GetBleLayer())
305         .SetPeerAddress(Transport::PeerAddress::BLE())
306         .SetAdvertisementDelegate(&gAdvDelegate);
307 #else
308     params.SetPASEVerifier(verifier);
309 #endif // CONFIG_NETWORK_LAYER_BLE
310
311     AdminId admin                = gNextAvailableAdminId;
312     AdminPairingInfo * adminInfo = gAdminPairings.AssignAdminId(admin);
313     VerifyOrReturnError(adminInfo != nullptr, CHIP_ERROR_NO_MEMORY);
314     gNextAvailableAdminId++;
315
316     return gRendezvousServer.WaitForPairing(std::move(params), &gTransports, &gSessions, adminInfo);
317 }
318
319 class ServerCallback : public SecureSessionMgrDelegate
320 {
321 public:
322     void OnMessageReceived(const PacketHeader & header, const PayloadHeader & payloadHeader, SecureSessionHandle session,
323                            System::PacketBufferHandle buffer, SecureSessionMgr * mgr) override
324     {
325         auto state            = mgr->GetPeerConnectionState(session);
326         const size_t data_len = buffer->DataLength();
327         char src_addr[PeerAddress::kMaxToStringSize];
328
329         // as soon as a client connects, assume it is connected
330         VerifyOrExit(!buffer.IsNull(), ChipLogProgress(AppServer, "Received data but couldn't process it..."));
331         VerifyOrExit(header.GetSourceNodeId().HasValue(), ChipLogProgress(AppServer, "Unknown source for received message"));
332
333         VerifyOrExit(state->GetPeerNodeId() != kUndefinedNodeId, ChipLogProgress(AppServer, "Unknown source for received message"));
334
335         state->GetPeerAddress().ToString(src_addr);
336
337         ChipLogProgress(AppServer, "Packet received from %s: %zu bytes", src_addr, static_cast<size_t>(data_len));
338
339         // TODO: This code is temporary, and must be updated to use the Cluster API.
340         // Issue: https://github.com/project-chip/connectedhomeip/issues/4725
341         if (payloadHeader.GetProtocolID() == chip::Protocols::kProtocol_ServiceProvisioning)
342         {
343             CHIP_ERROR err = CHIP_NO_ERROR;
344             uint32_t timeout;
345             uint16_t discriminator;
346             PASEVerifier verifier;
347
348             ChipLogProgress(AppServer, "Received service provisioning message. Treating it as OpenPairingWindow request");
349             chip::System::PacketBufferTLVReader reader;
350             reader.Init(std::move(buffer));
351             reader.ImplicitProfileId = chip::Protocols::kProtocol_ServiceProvisioning;
352
353             SuccessOrExit(reader.Next(kTLVType_UnsignedInteger, TLV::ProfileTag(reader.ImplicitProfileId, 1)));
354             SuccessOrExit(reader.Get(timeout));
355
356             err = reader.Next(kTLVType_UnsignedInteger, TLV::ProfileTag(reader.ImplicitProfileId, 2));
357             if (err == CHIP_NO_ERROR)
358             {
359                 SuccessOrExit(reader.Get(discriminator));
360
361                 err = reader.Next(kTLVType_ByteString, TLV::ProfileTag(reader.ImplicitProfileId, 3));
362                 if (err == CHIP_NO_ERROR)
363                 {
364                     SuccessOrExit(reader.GetBytes(reinterpret_cast<uint8_t *>(verifier), sizeof(verifier)));
365                 }
366             }
367
368             ChipLogProgress(AppServer, "Pairing Window timeout %d seconds", timeout);
369
370             if (err != CHIP_NO_ERROR)
371             {
372                 SuccessOrExit(err = OpenDefaultPairingWindow(ResetAdmins::kNo));
373             }
374             else
375             {
376                 ChipLogProgress(AppServer, "Pairing Window discriminator %d", discriminator);
377                 err = OpenPairingWindowUsingVerifier(discriminator, verifier);
378                 SuccessOrExit(err);
379             }
380             ChipLogProgress(AppServer, "Opened the pairing window");
381         }
382         else
383         {
384             HandleDataModelMessage(header.GetSourceNodeId().Value(), std::move(buffer));
385         }
386
387     exit:;
388     }
389
390     void OnReceiveError(CHIP_ERROR error, const Transport::PeerAddress & source, SecureSessionMgr * mgr) override
391     {
392         ChipLogProgress(AppServer, "Packet received error: %s", ErrorStr(error));
393         if (mDelegate != nullptr)
394         {
395             mDelegate->OnReceiveError();
396         }
397     }
398
399     void OnNewConnection(SecureSessionHandle session, SecureSessionMgr * mgr) override
400     {
401         ChipLogProgress(AppServer, "Received a new connection.");
402     }
403
404     void SetDelegate(AppDelegate * delegate) { mDelegate = delegate; }
405
406 private:
407     AppDelegate * mDelegate = nullptr;
408 };
409
410 #if defined(CHIP_APP_USE_INTERACTION_MODEL) || defined(CHIP_APP_USE_ECHO)
411 Messaging::ExchangeManager gExchangeMgr;
412 #endif
413 ServerCallback gCallbacks;
414 SecurePairingUsingTestSecret gTestPairing;
415
416 } // namespace
417
418 SecureSessionMgr & chip::SessionManager()
419 {
420     return gSessions;
421 }
422
423 CHIP_ERROR OpenDefaultPairingWindow(ResetAdmins resetAdmins)
424 {
425     gDeviceDiscriminatorCache.RestoreDiscriminator();
426
427     uint32_t pinCode;
428     ReturnErrorOnFailure(DeviceLayer::ConfigurationMgr().GetSetupPinCode(pinCode));
429
430     RendezvousParameters params;
431
432 #if CONFIG_NETWORK_LAYER_BLE
433     params.SetSetupPINCode(pinCode)
434         .SetBleLayer(DeviceLayer::ConnectivityMgr().GetBleLayer())
435         .SetPeerAddress(Transport::PeerAddress::BLE())
436         .SetAdvertisementDelegate(&gAdvDelegate);
437 #else
438     params.SetSetupPINCode(pinCode);
439 #endif // CONFIG_NETWORK_LAYER_BLE
440
441     if (resetAdmins == ResetAdmins::kYes)
442     {
443         uint16_t nextKeyId = gRendezvousServer.GetRendezvousSession()->GetNextKeyId();
444         EraseAllAdminPairingsUpTo(gNextAvailableAdminId);
445         EraseAllSessionsUpTo(nextKeyId);
446         gNextAvailableAdminId = 0;
447         gAdminPairings.Reset();
448     }
449
450     AdminId admin                = gNextAvailableAdminId;
451     AdminPairingInfo * adminInfo = gAdminPairings.AssignAdminId(admin);
452     VerifyOrReturnError(adminInfo != nullptr, CHIP_ERROR_NO_MEMORY);
453     gNextAvailableAdminId++;
454
455     return gRendezvousServer.WaitForPairing(std::move(params), &gTransports, &gSessions, adminInfo);
456 }
457
458 // The function will initialize datamodel handler and then start the server
459 // The server assumes the platform's networking has been setup already
460 void InitServer(AppDelegate * delegate)
461 {
462     CHIP_ERROR err = CHIP_NO_ERROR;
463
464     chip::Platform::MemoryInit();
465
466     InitDataModelHandler();
467     gCallbacks.SetDelegate(delegate);
468
469     err = gRendezvousServer.Init(delegate, &gServerStorage);
470     SuccessOrExit(err);
471
472     gAdvDelegate.SetDelegate(delegate);
473
474     // Init transport before operations with secure session mgr.
475 #if INET_CONFIG_ENABLE_IPV4
476     err = gTransports.Init(UdpListenParameters(&DeviceLayer::InetLayer).SetAddressType(kIPAddressType_IPv6),
477                            UdpListenParameters(&DeviceLayer::InetLayer).SetAddressType(kIPAddressType_IPv4));
478 #else
479     err = gTransports.Init(UdpListenParameters(&DeviceLayer::InetLayer).SetAddressType(kIPAddressType_IPv6));
480 #endif
481     SuccessOrExit(err);
482
483     err = gSessions.Init(chip::kTestDeviceNodeId, &DeviceLayer::SystemLayer, &gTransports, &gAdminPairings);
484     SuccessOrExit(err);
485
486 #if defined(CHIP_APP_USE_INTERACTION_MODEL) || defined(CHIP_APP_USE_ECHO)
487     err = gExchangeMgr.Init(&gSessions);
488     SuccessOrExit(err);
489 #else
490     gSessions.SetDelegate(&gCallbacks);
491 #endif
492
493 #if defined(CHIP_APP_USE_INTERACTION_MODEL)
494     err = chip::app::InteractionModelEngine::GetInstance()->Init(&gExchangeMgr);
495     SuccessOrExit(err);
496 #endif
497
498 #if defined(CHIP_APP_USE_ECHO)
499     err = InitEchoHandler(&gExchangeMgr);
500     SuccessOrExit(err);
501 #endif
502
503     if (useTestPairing())
504     {
505         SuccessOrExit(err = AddTestPairing());
506     }
507
508     // This flag is used to bypass BLE in the cirque test
509     // Only in the cirque test this is enabled with --args='bypass_rendezvous=true'
510     if (isRendezvousBypassed())
511     {
512         ChipLogProgress(AppServer, "Rendezvous and secure pairing skipped");
513     }
514     else if (DeviceLayer::ConnectivityMgr().IsWiFiStationProvisioned() || DeviceLayer::ConnectivityMgr().IsThreadProvisioned())
515     {
516         // If the network is already provisioned, proactively disable BLE advertisement.
517         ChipLogProgress(AppServer, "Network already provisioned. Disabling BLE advertisement");
518         chip::DeviceLayer::ConnectivityMgr().SetBLEAdvertisingEnabled(false);
519
520         // Restore any previous admin pairings
521         VerifyOrExit(CHIP_NO_ERROR == RestoreAllAdminPairingsFromKVS(gAdminPairings, gNextAvailableAdminId),
522                      ChipLogError(AppServer, "Could not restore admin table"));
523
524         VerifyOrExit(CHIP_NO_ERROR == RestoreAllSessionsFromKVS(gSessions, gRendezvousServer),
525                      ChipLogError(AppServer, "Could not restore previous sessions"));
526     }
527     else
528     {
529 #if CHIP_DEVICE_CONFIG_ENABLE_PAIRING_AUTOSTART
530         SuccessOrExit(err = OpenDefaultPairingWindow(ResetAdmins::kYes));
531 #endif
532     }
533
534 // Starting mDNS server only for Thread devices due to problem reported in issue #5076.
535 #if CHIP_DEVICE_CONFIG_ENABLE_THREAD
536     app::Mdns::StartServer();
537 #endif
538
539 exit:
540     if (err != CHIP_NO_ERROR)
541     {
542         ChipLogError(AppServer, "ERROR setting up transport: %s", ErrorStr(err));
543     }
544     else
545     {
546         ChipLogProgress(AppServer, "Server Listening...");
547     }
548 }
549
550 CHIP_ERROR AddTestPairing()
551 {
552     CHIP_ERROR err               = CHIP_NO_ERROR;
553     AdminPairingInfo * adminInfo = nullptr;
554
555     for (const AdminPairingInfo & admin : gAdminPairings)
556         if (admin.IsInitialized() && admin.GetNodeId() == chip::kTestDeviceNodeId)
557             ExitNow();
558
559     adminInfo = gAdminPairings.AssignAdminId(gNextAvailableAdminId);
560     VerifyOrExit(adminInfo != nullptr, err = CHIP_ERROR_NO_MEMORY);
561
562     adminInfo->SetNodeId(chip::kTestDeviceNodeId);
563     SuccessOrExit(err = gSessions.NewPairing(Optional<PeerAddress>{ PeerAddress::Uninitialized() }, chip::kTestControllerNodeId,
564                                              &gTestPairing, SecureSessionMgr::PairingDirection::kResponder, gNextAvailableAdminId));
565     ++gNextAvailableAdminId;
566
567 exit:
568     if (err != CHIP_NO_ERROR && adminInfo != nullptr)
569         gAdminPairings.ReleaseAdminId(gNextAvailableAdminId);
570
571     return err;
572 }
573
574 AdminPairingTable & GetGlobalAdminPairingTable()
575 {
576     return gAdminPairings;
577 }