1 /*
2  *  Copyright (c) 2020, The OpenThread Authors.
3  *  All rights reserved.
4  *
5  *  Redistribution and use in source and binary forms, with or without
6  *  modification, are permitted provided that the following conditions are met:
7  *  1. Redistributions of source code must retain the above copyright
8  *     notice, this list of conditions and the following disclaimer.
9  *  2. Redistributions in binary form must reproduce the above copyright
10  *     notice, this list of conditions and the following disclaimer in the
11  *     documentation and/or other materials provided with the distribution.
12  *  3. Neither the name of the copyright holder nor the
13  *     names of its contributors may be used to endorse or promote products
14  *     derived from this software without specific prior written permission.
15  *
16  *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17  *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18  *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19  *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
20  *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21  *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22  *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23  *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24  *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25  *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26  *  POSSIBILITY OF SUCH DAMAGE.
27  */
28 
29 /**
30  * @file
31  *   This file implements a simple CLI for the SRP Client.
32  */
33 
34 #include "cli_srp_client.hpp"
35 
36 #if OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE
37 
38 #include <string.h>
39 
40 #include "cli/cli.hpp"
41 
42 namespace ot {
43 namespace Cli {
44 
45 constexpr SrpClient::Command SrpClient::sCommands[];
46 
CopyString(char * aDest,uint16_t aDestSize,const char * aSource)47 static otError CopyString(char *aDest, uint16_t aDestSize, const char *aSource)
48 {
49     // Copies a string from `aSource` to `aDestination` (char array),
50     // verifying that the string fits in the destination array.
51 
52     otError error = OT_ERROR_NONE;
53     size_t  len   = strlen(aSource);
54 
55     VerifyOrExit(len + 1 <= aDestSize, error = OT_ERROR_INVALID_ARGS);
56     memcpy(aDest, aSource, len + 1);
57 
58 exit:
59     return error;
60 }
61 
SrpClient(Interpreter & aInterpreter)62 SrpClient::SrpClient(Interpreter &aInterpreter)
63     : mInterpreter(aInterpreter)
64     , mCallbackEnabled(false)
65 {
66     otSrpClientSetCallback(mInterpreter.mInstance, SrpClient::HandleCallback, this);
67 }
68 
Process(Arg aArgs[])69 otError SrpClient::Process(Arg aArgs[])
70 {
71     otError        error = OT_ERROR_INVALID_COMMAND;
72     const Command *command;
73 
74     if (aArgs[0].IsEmpty())
75     {
76         IgnoreError(ProcessHelp(aArgs));
77         ExitNow();
78     }
79 
80     command = Utils::LookupTable::Find(aArgs[0].GetCString(), sCommands);
81     VerifyOrExit(command != nullptr);
82 
83     error = (this->*command->mHandler)(aArgs + 1);
84 
85 exit:
86     return error;
87 }
88 
89 #if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE
90 
ProcessAutoStart(Arg aArgs[])91 otError SrpClient::ProcessAutoStart(Arg aArgs[])
92 {
93     otError error = OT_ERROR_NONE;
94     bool    enable;
95 
96     if (aArgs[0].IsEmpty())
97     {
98         mInterpreter.OutputEnabledDisabledStatus(otSrpClientIsAutoStartModeEnabled(mInterpreter.mInstance));
99         ExitNow();
100     }
101 
102     SuccessOrExit(error = Interpreter::ParseEnableOrDisable(aArgs[0], enable));
103 
104     if (enable)
105     {
106         otSrpClientEnableAutoStartMode(mInterpreter.mInstance, /* aCallback */ nullptr, /* aContext */ nullptr);
107     }
108     else
109     {
110         otSrpClientDisableAutoStartMode(mInterpreter.mInstance);
111     }
112 
113 exit:
114     return error;
115 }
116 
117 #endif // OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE
118 
ProcessCallback(Arg aArgs[])119 otError SrpClient::ProcessCallback(Arg aArgs[])
120 {
121     otError error = OT_ERROR_NONE;
122 
123     if (aArgs[0].IsEmpty())
124     {
125         mInterpreter.OutputEnabledDisabledStatus(mCallbackEnabled);
126         ExitNow();
127     }
128 
129     error = Interpreter::ParseEnableOrDisable(aArgs[0], mCallbackEnabled);
130 
131 exit:
132     return error;
133 }
134 
ProcessHelp(Arg aArgs[])135 otError SrpClient::ProcessHelp(Arg aArgs[])
136 {
137     OT_UNUSED_VARIABLE(aArgs);
138 
139     for (const Command &command : sCommands)
140     {
141         mInterpreter.OutputLine(command.mName);
142     }
143 
144     return OT_ERROR_NONE;
145 }
146 
ProcessHost(Arg aArgs[])147 otError SrpClient::ProcessHost(Arg aArgs[])
148 {
149     otError error = OT_ERROR_NONE;
150 
151     if (aArgs[0].IsEmpty())
152     {
153         OutputHostInfo(0, *otSrpClientGetHostInfo(mInterpreter.mInstance));
154         ExitNow();
155     }
156 
157     if (aArgs[0] == "name")
158     {
159         if (aArgs[1].IsEmpty())
160         {
161             const char *name = otSrpClientGetHostInfo(mInterpreter.mInstance)->mName;
162             mInterpreter.OutputLine("%s", (name != nullptr) ? name : "(null)");
163         }
164         else
165         {
166             uint16_t len;
167             uint16_t size;
168             char *   hostName;
169 
170             VerifyOrExit(aArgs[2].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
171             hostName = otSrpClientBuffersGetHostNameString(mInterpreter.mInstance, &size);
172 
173             len = aArgs[1].GetLength();
174             VerifyOrExit(len + 1 <= size, error = OT_ERROR_INVALID_ARGS);
175 
176             // We first make sure we can set the name, and if so
177             // we copy it to the persisted string buffer and set
178             // the host name again now with the persisted buffer.
179             // This ensures that we do not overwrite a previous
180             // buffer with a host name that cannot be set.
181 
182             SuccessOrExit(error = otSrpClientSetHostName(mInterpreter.mInstance, aArgs[1].GetCString()));
183             memcpy(hostName, aArgs[1].GetCString(), len + 1);
184 
185             IgnoreError(otSrpClientSetHostName(mInterpreter.mInstance, hostName));
186         }
187     }
188     else if (aArgs[0] == "state")
189     {
190         VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
191         mInterpreter.OutputLine("%s",
192                                 otSrpClientItemStateToString(otSrpClientGetHostInfo(mInterpreter.mInstance)->mState));
193     }
194     else if (aArgs[0] == "address")
195     {
196         if (aArgs[1].IsEmpty())
197         {
198             const otSrpClientHostInfo *hostInfo = otSrpClientGetHostInfo(mInterpreter.mInstance);
199 
200             for (uint8_t index = 0; index < hostInfo->mNumAddresses; index++)
201             {
202                 mInterpreter.OutputIp6Address(hostInfo->mAddresses[index]);
203                 mInterpreter.OutputLine("");
204             }
205         }
206         else
207         {
208             uint8_t       numAddresses = 0;
209             otIp6Address  addresses[kMaxHostAddresses];
210             uint8_t       arrayLength;
211             otIp6Address *hostAddressArray;
212 
213             hostAddressArray = otSrpClientBuffersGetHostAddressesArray(mInterpreter.mInstance, &arrayLength);
214 
215             // We first make sure we can set the addresses, and if so
216             // we copy the address list into the persisted address array
217             // and set it again. This ensures that we do not overwrite
218             // a previous list before we know it is safe to set/change
219             // the address list.
220 
221             if (arrayLength > kMaxHostAddresses)
222             {
223                 arrayLength = kMaxHostAddresses;
224             }
225 
226             for (Arg *arg = &aArgs[1]; !arg->IsEmpty(); arg++)
227             {
228                 VerifyOrExit(numAddresses < arrayLength, error = OT_ERROR_NO_BUFS);
229                 SuccessOrExit(error = arg->ParseAsIp6Address(addresses[numAddresses]));
230                 numAddresses++;
231             }
232 
233             SuccessOrExit(error = otSrpClientSetHostAddresses(mInterpreter.mInstance, addresses, numAddresses));
234 
235             memcpy(hostAddressArray, addresses, numAddresses * sizeof(hostAddressArray[0]));
236             IgnoreError(otSrpClientSetHostAddresses(mInterpreter.mInstance, hostAddressArray, numAddresses));
237         }
238     }
239     else if (aArgs[0] == "remove")
240     {
241         bool removeKeyLease    = false;
242         bool sendUnregToServer = false;
243 
244         if (!aArgs[1].IsEmpty())
245         {
246             SuccessOrExit(error = aArgs[1].ParseAsBool(removeKeyLease));
247 
248             if (!aArgs[2].IsEmpty())
249             {
250                 SuccessOrExit(error = aArgs[2].ParseAsBool(sendUnregToServer));
251                 VerifyOrExit(aArgs[3].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
252             }
253         }
254 
255         error = otSrpClientRemoveHostAndServices(mInterpreter.mInstance, removeKeyLease, sendUnregToServer);
256     }
257     else if (aArgs[0] == "clear")
258     {
259         VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
260         otSrpClientClearHostAndServices(mInterpreter.mInstance);
261         otSrpClientBuffersFreeAllServices(mInterpreter.mInstance);
262     }
263     else
264     {
265         error = OT_ERROR_INVALID_COMMAND;
266     }
267 
268 exit:
269     return error;
270 }
271 
ProcessLeaseInterval(Arg aArgs[])272 otError SrpClient::ProcessLeaseInterval(Arg aArgs[])
273 {
274     return mInterpreter.ProcessGetSet(aArgs, otSrpClientGetLeaseInterval, otSrpClientSetLeaseInterval);
275 }
276 
ProcessKeyLeaseInterval(Arg aArgs[])277 otError SrpClient::ProcessKeyLeaseInterval(Arg aArgs[])
278 {
279     return mInterpreter.ProcessGetSet(aArgs, otSrpClientGetKeyLeaseInterval, otSrpClientSetKeyLeaseInterval);
280 }
281 
ProcessServer(Arg aArgs[])282 otError SrpClient::ProcessServer(Arg aArgs[])
283 {
284     otError           error          = OT_ERROR_NONE;
285     const otSockAddr *serverSockAddr = otSrpClientGetServerAddress(mInterpreter.mInstance);
286 
287     if (aArgs[0].IsEmpty())
288     {
289         char string[OT_IP6_SOCK_ADDR_STRING_SIZE];
290 
291         otIp6SockAddrToString(serverSockAddr, string, sizeof(string));
292         mInterpreter.OutputLine(string);
293         ExitNow();
294     }
295 
296     VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
297 
298     if (aArgs[0] == "address")
299     {
300         mInterpreter.OutputIp6Address(serverSockAddr->mAddress);
301         mInterpreter.OutputLine("");
302     }
303     else if (aArgs[0] == "port")
304     {
305         mInterpreter.OutputLine("%u", serverSockAddr->mPort);
306     }
307     else
308     {
309         error = OT_ERROR_INVALID_COMMAND;
310     }
311 
312 exit:
313     return error;
314 }
315 
ProcessService(Arg aArgs[])316 otError SrpClient::ProcessService(Arg aArgs[])
317 {
318     otError error = OT_ERROR_NONE;
319     bool    isRemove;
320 
321     if (aArgs[0].IsEmpty())
322     {
323         OutputServiceList(0, otSrpClientGetServices(mInterpreter.mInstance));
324         ExitNow();
325     }
326 
327     if (aArgs[0] == "add")
328     {
329         error = ProcessServiceAdd(aArgs);
330     }
331     else if ((isRemove = (aArgs[0] == "remove")) || (aArgs[0] == "clear"))
332     {
333         // `remove`|`clear` <instance-name> <service-name>
334 
335         const otSrpClientService *service;
336 
337         VerifyOrExit(!aArgs[2].IsEmpty() && aArgs[3].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
338 
339         for (service = otSrpClientGetServices(mInterpreter.mInstance); service != nullptr; service = service->mNext)
340         {
341             if ((aArgs[1] == service->mInstanceName) && (aArgs[2] == service->mName))
342             {
343                 break;
344             }
345         }
346 
347         VerifyOrExit(service != nullptr, error = OT_ERROR_NOT_FOUND);
348 
349         if (isRemove)
350         {
351             error = otSrpClientRemoveService(mInterpreter.mInstance, const_cast<otSrpClientService *>(service));
352         }
353         else
354         {
355             SuccessOrExit(
356                 error = otSrpClientClearService(mInterpreter.mInstance, const_cast<otSrpClientService *>(service)));
357 
358             otSrpClientBuffersFreeService(mInterpreter.mInstance, reinterpret_cast<otSrpClientBuffersServiceEntry *>(
359                                                                       const_cast<otSrpClientService *>(service)));
360         }
361     }
362 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
363     else if (aArgs[0] == "key")
364     {
365         // `key [enable/disable]`
366 
367         bool enable;
368 
369         if (aArgs[1].IsEmpty())
370         {
371             mInterpreter.OutputEnabledDisabledStatus(otSrpClientIsServiceKeyRecordEnabled(mInterpreter.mInstance));
372             ExitNow();
373         }
374 
375         SuccessOrExit(error = Interpreter::ParseEnableOrDisable(aArgs[1], enable));
376         VerifyOrExit(aArgs[2].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
377         otSrpClientSetServiceKeyRecordEnabled(mInterpreter.mInstance, enable);
378     }
379 #endif // OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
380     else
381     {
382         error = OT_ERROR_INVALID_COMMAND;
383     }
384 
385 exit:
386     return error;
387 }
388 
ProcessServiceAdd(Arg aArgs[])389 otError SrpClient::ProcessServiceAdd(Arg aArgs[])
390 {
391     // `add` <instance-name> <service-name> <port> [priority] [weight] [txt]
392 
393     otSrpClientBuffersServiceEntry *entry = nullptr;
394     uint16_t                        size;
395     char *                          string;
396     otError                         error;
397     char *                          label;
398 
399     entry = otSrpClientBuffersAllocateService(mInterpreter.mInstance);
400 
401     VerifyOrExit(entry != nullptr, error = OT_ERROR_NO_BUFS);
402 
403     SuccessOrExit(error = aArgs[3].ParseAsUint16(entry->mService.mPort));
404 
405     // Successfully parsing aArgs[3] indicates that aArgs[1] and
406     // aArgs[2] are also non-empty.
407 
408     string = otSrpClientBuffersGetServiceEntryInstanceNameString(entry, &size);
409     SuccessOrExit(error = CopyString(string, size, aArgs[1].GetCString()));
410 
411     string = otSrpClientBuffersGetServiceEntryServiceNameString(entry, &size);
412     SuccessOrExit(error = CopyString(string, size, aArgs[2].GetCString()));
413 
414     // Service subtypes are added as part of service name as a comma separated list
415     // e.g., "_service._udp,_sub1,_sub2"
416 
417     label = strchr(string, ',');
418 
419     if (label != nullptr)
420     {
421         uint16_t     arrayLength;
422         const char **subTypeLabels = otSrpClientBuffersGetSubTypeLabelsArray(entry, &arrayLength);
423 
424         // Leave the last array element as `nullptr` to indicate end of array.
425         for (uint16_t index = 0; index + 1 < arrayLength; index++)
426         {
427             *label++             = '\0';
428             subTypeLabels[index] = label;
429 
430             label = strchr(label, ',');
431 
432             if (label == nullptr)
433             {
434                 break;
435             }
436         }
437 
438         VerifyOrExit(label == nullptr, error = OT_ERROR_NO_BUFS);
439     }
440 
441     SuccessOrExit(error = aArgs[3].ParseAsUint16(entry->mService.mPort));
442 
443     if (!aArgs[4].IsEmpty())
444     {
445         SuccessOrExit(error = aArgs[4].ParseAsUint16(entry->mService.mPriority));
446     }
447 
448     if (!aArgs[5].IsEmpty())
449     {
450         SuccessOrExit(error = aArgs[5].ParseAsUint16(entry->mService.mWeight));
451     }
452 
453     if (!aArgs[6].IsEmpty())
454     {
455         uint8_t *txtBuffer;
456 
457         txtBuffer                     = otSrpClientBuffersGetServiceEntryTxtBuffer(entry, &size);
458         entry->mTxtEntry.mValueLength = size;
459 
460         SuccessOrExit(error = aArgs[6].ParseAsHexString(entry->mTxtEntry.mValueLength, txtBuffer));
461         VerifyOrExit(aArgs[7].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
462     }
463     else
464     {
465         entry->mService.mNumTxtEntries = 0;
466     }
467 
468     SuccessOrExit(error = otSrpClientAddService(mInterpreter.mInstance, &entry->mService));
469 
470     entry = nullptr;
471 
472 exit:
473     if (entry != nullptr)
474     {
475         otSrpClientBuffersFreeService(mInterpreter.mInstance, entry);
476     }
477 
478     return error;
479 }
480 
OutputHostInfo(uint8_t aIndentSize,const otSrpClientHostInfo & aHostInfo)481 void SrpClient::OutputHostInfo(uint8_t aIndentSize, const otSrpClientHostInfo &aHostInfo)
482 {
483     mInterpreter.OutputFormat(aIndentSize, "name:");
484 
485     if (aHostInfo.mName != nullptr)
486     {
487         mInterpreter.OutputFormat("\"%s\"", aHostInfo.mName);
488     }
489     else
490     {
491         mInterpreter.OutputFormat("(null)");
492     }
493 
494     mInterpreter.OutputFormat(", state:%s, addrs:[", otSrpClientItemStateToString(aHostInfo.mState));
495 
496     for (uint8_t index = 0; index < aHostInfo.mNumAddresses; index++)
497     {
498         if (index > 0)
499         {
500             mInterpreter.OutputFormat(", ");
501         }
502 
503         mInterpreter.OutputIp6Address(aHostInfo.mAddresses[index]);
504     }
505 
506     mInterpreter.OutputLine("]");
507 }
508 
OutputServiceList(uint8_t aIndentSize,const otSrpClientService * aServices)509 void SrpClient::OutputServiceList(uint8_t aIndentSize, const otSrpClientService *aServices)
510 {
511     while (aServices != nullptr)
512     {
513         OutputService(aIndentSize, *aServices);
514         aServices = aServices->mNext;
515     }
516 }
517 
OutputService(uint8_t aIndentSize,const otSrpClientService & aService)518 void SrpClient::OutputService(uint8_t aIndentSize, const otSrpClientService &aService)
519 {
520     mInterpreter.OutputFormat(aIndentSize, "instance:\"%s\", name:\"%s", aService.mInstanceName, aService.mName);
521 
522     if (aService.mSubTypeLabels != nullptr)
523     {
524         for (uint16_t index = 0; aService.mSubTypeLabels[index] != nullptr; index++)
525         {
526             mInterpreter.OutputFormat(",%s", aService.mSubTypeLabels[index]);
527         }
528     }
529 
530     mInterpreter.OutputLine("\", state:%s, port:%d, priority:%d, weight:%d",
531                             otSrpClientItemStateToString(aService.mState), aService.mPort, aService.mPriority,
532                             aService.mWeight);
533 }
534 
ProcessStart(Arg aArgs[])535 otError SrpClient::ProcessStart(Arg aArgs[])
536 {
537     otError    error = OT_ERROR_NONE;
538     otSockAddr serverSockAddr;
539 
540     SuccessOrExit(error = aArgs[0].ParseAsIp6Address(serverSockAddr.mAddress));
541     SuccessOrExit(error = aArgs[1].ParseAsUint16(serverSockAddr.mPort));
542     VerifyOrExit(aArgs[2].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
543 
544     error = otSrpClientStart(mInterpreter.mInstance, &serverSockAddr);
545 
546 exit:
547     return error;
548 }
549 
ProcessState(Arg aArgs[])550 otError SrpClient::ProcessState(Arg aArgs[])
551 {
552     otError error = OT_ERROR_NONE;
553 
554     VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
555 
556     mInterpreter.OutputEnabledDisabledStatus(otSrpClientIsRunning(mInterpreter.mInstance));
557 
558 exit:
559     return error;
560 }
561 
ProcessStop(Arg aArgs[])562 otError SrpClient::ProcessStop(Arg aArgs[])
563 {
564     otError error = OT_ERROR_NONE;
565 
566     VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
567     otSrpClientStop(mInterpreter.mInstance);
568 
569 exit:
570     return error;
571 }
572 
HandleCallback(otError aError,const otSrpClientHostInfo * aHostInfo,const otSrpClientService * aServices,const otSrpClientService * aRemovedServices,void * aContext)573 void SrpClient::HandleCallback(otError                    aError,
574                                const otSrpClientHostInfo *aHostInfo,
575                                const otSrpClientService * aServices,
576                                const otSrpClientService * aRemovedServices,
577                                void *                     aContext)
578 {
579     static_cast<SrpClient *>(aContext)->HandleCallback(aError, aHostInfo, aServices, aRemovedServices);
580 }
581 
HandleCallback(otError aError,const otSrpClientHostInfo * aHostInfo,const otSrpClientService * aServices,const otSrpClientService * aRemovedServices)582 void SrpClient::HandleCallback(otError                    aError,
583                                const otSrpClientHostInfo *aHostInfo,
584                                const otSrpClientService * aServices,
585                                const otSrpClientService * aRemovedServices)
586 {
587     otSrpClientService *next;
588 
589     if (mCallbackEnabled)
590     {
591         mInterpreter.OutputLine("SRP client callback - error:%s", otThreadErrorToString(aError));
592         mInterpreter.OutputLine("Host info:");
593         OutputHostInfo(kIndentSize, *aHostInfo);
594 
595         mInterpreter.OutputLine("Service list:");
596         OutputServiceList(kIndentSize, aServices);
597 
598         if (aRemovedServices != nullptr)
599         {
600             mInterpreter.OutputLine("Removed service list:");
601             OutputServiceList(kIndentSize, aRemovedServices);
602         }
603     }
604 
605     // Go through removed services and free all removed services
606 
607     for (const otSrpClientService *service = aRemovedServices; service != nullptr; service = next)
608     {
609         next = service->mNext;
610 
611         otSrpClientBuffersFreeService(mInterpreter.mInstance, reinterpret_cast<otSrpClientBuffersServiceEntry *>(
612                                                                   const_cast<otSrpClientService *>(service)));
613     }
614 }
615 
616 } // namespace Cli
617 } // namespace ot
618 
619 #endif // OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE
620