Upstream version 9.38.198.0
[platform/framework/web/crosswalk.git] / src / net / ssl / channel_id_service.cc
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "net/ssl/channel_id_service.h"
6
7 #include <algorithm>
8 #include <limits>
9
10 #include "base/bind.h"
11 #include "base/bind_helpers.h"
12 #include "base/callback_helpers.h"
13 #include "base/compiler_specific.h"
14 #include "base/location.h"
15 #include "base/logging.h"
16 #include "base/memory/ref_counted.h"
17 #include "base/memory/scoped_ptr.h"
18 #include "base/message_loop/message_loop_proxy.h"
19 #include "base/metrics/histogram.h"
20 #include "base/rand_util.h"
21 #include "base/stl_util.h"
22 #include "base/task_runner.h"
23 #include "crypto/ec_private_key.h"
24 #include "net/base/net_errors.h"
25 #include "net/base/registry_controlled_domains/registry_controlled_domain.h"
26 #include "net/cert/x509_certificate.h"
27 #include "net/cert/x509_util.h"
28 #include "url/gurl.h"
29
30 #if defined(USE_NSS)
31 #include <private/pprthred.h>  // PR_DetachThread
32 #endif
33
34 namespace net {
35
36 namespace {
37
38 const int kValidityPeriodInDays = 365;
39 // When we check the system time, we add this many days to the end of the check
40 // so the result will still hold even after chrome has been running for a
41 // while.
42 const int kSystemTimeValidityBufferInDays = 90;
43
44 // Used by the GetDomainBoundCertResult histogram to record the final
45 // outcome of each GetChannelID or GetOrCreateChannelID call.
46 // Do not re-use values.
47 enum GetChannelIDResult {
48   // Synchronously found and returned an existing domain bound cert.
49   SYNC_SUCCESS = 0,
50   // Retrieved or generated and returned a domain bound cert asynchronously.
51   ASYNC_SUCCESS = 1,
52   // Retrieval/generation request was cancelled before the cert generation
53   // completed.
54   ASYNC_CANCELLED = 2,
55   // Cert generation failed.
56   ASYNC_FAILURE_KEYGEN = 3,
57   ASYNC_FAILURE_CREATE_CERT = 4,
58   ASYNC_FAILURE_EXPORT_KEY = 5,
59   ASYNC_FAILURE_UNKNOWN = 6,
60   // GetChannelID or GetOrCreateChannelID was called with
61   // invalid arguments.
62   INVALID_ARGUMENT = 7,
63   // We don't support any of the cert types the server requested.
64   UNSUPPORTED_TYPE = 8,
65   // Server asked for a different type of certs while we were generating one.
66   TYPE_MISMATCH = 9,
67   // Couldn't start a worker to generate a cert.
68   WORKER_FAILURE = 10,
69   GET_CHANNEL_ID_RESULT_MAX
70 };
71
72 void RecordGetChannelIDResult(GetChannelIDResult result) {
73   UMA_HISTOGRAM_ENUMERATION("DomainBoundCerts.GetDomainBoundCertResult", result,
74                             GET_CHANNEL_ID_RESULT_MAX);
75 }
76
77 void RecordGetChannelIDTime(base::TimeDelta request_time) {
78   UMA_HISTOGRAM_CUSTOM_TIMES("DomainBoundCerts.GetCertTime",
79                              request_time,
80                              base::TimeDelta::FromMilliseconds(1),
81                              base::TimeDelta::FromMinutes(5),
82                              50);
83 }
84
85 // On success, returns a ChannelID object and sets |*error| to OK.
86 // Otherwise, returns NULL, and |*error| will be set to a net error code.
87 // |serial_number| is passed in because base::RandInt cannot be called from an
88 // unjoined thread, due to relying on a non-leaked LazyInstance
89 scoped_ptr<ChannelIDStore::ChannelID> GenerateChannelID(
90     const std::string& server_identifier,
91     uint32 serial_number,
92     int* error) {
93   scoped_ptr<ChannelIDStore::ChannelID> result;
94
95   base::TimeTicks start = base::TimeTicks::Now();
96   base::Time not_valid_before = base::Time::Now();
97   base::Time not_valid_after =
98       not_valid_before + base::TimeDelta::FromDays(kValidityPeriodInDays);
99   std::string der_cert;
100   std::vector<uint8> private_key_info;
101   scoped_ptr<crypto::ECPrivateKey> key;
102   if (!x509_util::CreateKeyAndChannelIDEC(server_identifier,
103                                           serial_number,
104                                           not_valid_before,
105                                           not_valid_after,
106                                           &key,
107                                           &der_cert)) {
108     DLOG(ERROR) << "Unable to create x509 cert for client";
109     *error = ERR_ORIGIN_BOUND_CERT_GENERATION_FAILED;
110     return result.Pass();
111   }
112
113   if (!key->ExportEncryptedPrivateKey(ChannelIDService::kEPKIPassword,
114                                       1, &private_key_info)) {
115     DLOG(ERROR) << "Unable to export private key";
116     *error = ERR_PRIVATE_KEY_EXPORT_FAILED;
117     return result.Pass();
118   }
119
120   // TODO(rkn): Perhaps ExportPrivateKey should be changed to output a
121   // std::string* to prevent this copying.
122   std::string key_out(private_key_info.begin(), private_key_info.end());
123
124   result.reset(new ChannelIDStore::ChannelID(
125       server_identifier,
126       not_valid_before,
127       not_valid_after,
128       key_out,
129       der_cert));
130   UMA_HISTOGRAM_CUSTOM_TIMES("DomainBoundCerts.GenerateCertTime",
131                              base::TimeTicks::Now() - start,
132                              base::TimeDelta::FromMilliseconds(1),
133                              base::TimeDelta::FromMinutes(5),
134                              50);
135   *error = OK;
136   return result.Pass();
137 }
138
139 }  // namespace
140
141 // Represents the output and result callback of a request.
142 class ChannelIDServiceRequest {
143  public:
144   ChannelIDServiceRequest(base::TimeTicks request_start,
145                           const CompletionCallback& callback,
146                           std::string* private_key,
147                           std::string* cert)
148       : request_start_(request_start),
149         callback_(callback),
150         private_key_(private_key),
151         cert_(cert) {
152   }
153
154   // Ensures that the result callback will never be made.
155   void Cancel() {
156     RecordGetChannelIDResult(ASYNC_CANCELLED);
157     callback_.Reset();
158     private_key_ = NULL;
159     cert_ = NULL;
160   }
161
162   // Copies the contents of |private_key| and |cert| to the caller's output
163   // arguments and calls the callback.
164   void Post(int error,
165             const std::string& private_key,
166             const std::string& cert) {
167     switch (error) {
168       case OK: {
169         base::TimeDelta request_time = base::TimeTicks::Now() - request_start_;
170         UMA_HISTOGRAM_CUSTOM_TIMES("DomainBoundCerts.GetCertTimeAsync",
171                                    request_time,
172                                    base::TimeDelta::FromMilliseconds(1),
173                                    base::TimeDelta::FromMinutes(5),
174                                    50);
175         RecordGetChannelIDTime(request_time);
176         RecordGetChannelIDResult(ASYNC_SUCCESS);
177         break;
178       }
179       case ERR_KEY_GENERATION_FAILED:
180         RecordGetChannelIDResult(ASYNC_FAILURE_KEYGEN);
181         break;
182       case ERR_ORIGIN_BOUND_CERT_GENERATION_FAILED:
183         RecordGetChannelIDResult(ASYNC_FAILURE_CREATE_CERT);
184         break;
185       case ERR_PRIVATE_KEY_EXPORT_FAILED:
186         RecordGetChannelIDResult(ASYNC_FAILURE_EXPORT_KEY);
187         break;
188       case ERR_INSUFFICIENT_RESOURCES:
189         RecordGetChannelIDResult(WORKER_FAILURE);
190         break;
191       default:
192         RecordGetChannelIDResult(ASYNC_FAILURE_UNKNOWN);
193         break;
194     }
195     if (!callback_.is_null()) {
196       *private_key_ = private_key;
197       *cert_ = cert;
198       callback_.Run(error);
199     }
200     delete this;
201   }
202
203   bool canceled() const { return callback_.is_null(); }
204
205  private:
206   base::TimeTicks request_start_;
207   CompletionCallback callback_;
208   std::string* private_key_;
209   std::string* cert_;
210 };
211
212 // ChannelIDServiceWorker runs on a worker thread and takes care of the
213 // blocking process of performing key generation. Will take care of deleting
214 // itself once Start() is called.
215 class ChannelIDServiceWorker {
216  public:
217   typedef base::Callback<void(
218       const std::string&,
219       int,
220       scoped_ptr<ChannelIDStore::ChannelID>)> WorkerDoneCallback;
221
222   ChannelIDServiceWorker(
223       const std::string& server_identifier,
224       const WorkerDoneCallback& callback)
225       : server_identifier_(server_identifier),
226         serial_number_(base::RandInt(0, std::numeric_limits<int>::max())),
227         origin_loop_(base::MessageLoopProxy::current()),
228         callback_(callback) {
229   }
230
231   // Starts the worker on |task_runner|. If the worker fails to start, such as
232   // if the task runner is shutting down, then it will take care of deleting
233   // itself.
234   bool Start(const scoped_refptr<base::TaskRunner>& task_runner) {
235     DCHECK(origin_loop_->RunsTasksOnCurrentThread());
236
237     return task_runner->PostTask(
238         FROM_HERE,
239         base::Bind(&ChannelIDServiceWorker::Run, base::Owned(this)));
240   }
241
242  private:
243   void Run() {
244     // Runs on a worker thread.
245     int error = ERR_FAILED;
246     scoped_ptr<ChannelIDStore::ChannelID> cert =
247         GenerateChannelID(server_identifier_, serial_number_, &error);
248     DVLOG(1) << "GenerateCert " << server_identifier_ << " returned " << error;
249 #if defined(USE_NSS)
250     // Detach the thread from NSPR.
251     // Calling NSS functions attaches the thread to NSPR, which stores
252     // the NSPR thread ID in thread-specific data.
253     // The threads in our thread pool terminate after we have called
254     // PR_Cleanup. Unless we detach them from NSPR, net_unittests gets
255     // segfaults on shutdown when the threads' thread-specific data
256     // destructors run.
257     PR_DetachThread();
258 #endif
259     origin_loop_->PostTask(FROM_HERE,
260                            base::Bind(callback_, server_identifier_, error,
261                                       base::Passed(&cert)));
262   }
263
264   const std::string server_identifier_;
265   // Note that serial_number_ must be initialized on a non-worker thread
266   // (see documentation for GenerateCert).
267   uint32 serial_number_;
268   scoped_refptr<base::SequencedTaskRunner> origin_loop_;
269   WorkerDoneCallback callback_;
270
271   DISALLOW_COPY_AND_ASSIGN(ChannelIDServiceWorker);
272 };
273
274 // A ChannelIDServiceJob is a one-to-one counterpart of an
275 // ChannelIDServiceWorker. It lives only on the ChannelIDService's
276 // origin message loop.
277 class ChannelIDServiceJob {
278  public:
279   ChannelIDServiceJob(bool create_if_missing)
280       : create_if_missing_(create_if_missing) {
281   }
282
283   ~ChannelIDServiceJob() {
284     if (!requests_.empty())
285       DeleteAllCanceled();
286   }
287
288   void AddRequest(ChannelIDServiceRequest* request,
289                   bool create_if_missing = false) {
290     create_if_missing_ |= create_if_missing;
291     requests_.push_back(request);
292   }
293
294   void HandleResult(int error,
295                     const std::string& private_key,
296                     const std::string& cert) {
297     PostAll(error, private_key, cert);
298   }
299
300   bool CreateIfMissing() const { return create_if_missing_; }
301
302  private:
303   void PostAll(int error,
304                const std::string& private_key,
305                const std::string& cert) {
306     std::vector<ChannelIDServiceRequest*> requests;
307     requests_.swap(requests);
308
309     for (std::vector<ChannelIDServiceRequest*>::iterator
310          i = requests.begin(); i != requests.end(); i++) {
311       (*i)->Post(error, private_key, cert);
312       // Post() causes the ChannelIDServiceRequest to delete itself.
313     }
314   }
315
316   void DeleteAllCanceled() {
317     for (std::vector<ChannelIDServiceRequest*>::iterator
318          i = requests_.begin(); i != requests_.end(); i++) {
319       if ((*i)->canceled()) {
320         delete *i;
321       } else {
322         LOG(DFATAL) << "ChannelIDServiceRequest leaked!";
323       }
324     }
325   }
326
327   std::vector<ChannelIDServiceRequest*> requests_;
328   bool create_if_missing_;
329 };
330
331 // static
332 const char ChannelIDService::kEPKIPassword[] = "";
333
334 ChannelIDService::RequestHandle::RequestHandle()
335     : service_(NULL),
336       request_(NULL) {}
337
338 ChannelIDService::RequestHandle::~RequestHandle() {
339   Cancel();
340 }
341
342 void ChannelIDService::RequestHandle::Cancel() {
343   if (request_) {
344     service_->CancelRequest(request_);
345     request_ = NULL;
346     callback_.Reset();
347   }
348 }
349
350 void ChannelIDService::RequestHandle::RequestStarted(
351     ChannelIDService* service,
352     ChannelIDServiceRequest* request,
353     const CompletionCallback& callback) {
354   DCHECK(request_ == NULL);
355   service_ = service;
356   request_ = request;
357   callback_ = callback;
358 }
359
360 void ChannelIDService::RequestHandle::OnRequestComplete(int result) {
361   request_ = NULL;
362   // Running the callback might delete |this|, so we can't touch any of our
363   // members afterwards. Reset callback_ first.
364   base::ResetAndReturn(&callback_).Run(result);
365 }
366
367 ChannelIDService::ChannelIDService(
368     ChannelIDStore* channel_id_store,
369     const scoped_refptr<base::TaskRunner>& task_runner)
370     : channel_id_store_(channel_id_store),
371       task_runner_(task_runner),
372       requests_(0),
373       cert_store_hits_(0),
374       inflight_joins_(0),
375       workers_created_(0),
376       weak_ptr_factory_(this) {
377   base::Time start = base::Time::Now();
378   base::Time end = start + base::TimeDelta::FromDays(
379       kValidityPeriodInDays + kSystemTimeValidityBufferInDays);
380   is_system_time_valid_ = x509_util::IsSupportedValidityRange(start, end);
381 }
382
383 ChannelIDService::~ChannelIDService() {
384   STLDeleteValues(&inflight_);
385 }
386
387 //static
388 std::string ChannelIDService::GetDomainForHost(const std::string& host) {
389   std::string domain =
390       registry_controlled_domains::GetDomainAndRegistry(
391           host, registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
392   if (domain.empty())
393     return host;
394   return domain;
395 }
396
397 int ChannelIDService::GetOrCreateChannelID(
398     const std::string& host,
399     std::string* private_key,
400     std::string* cert,
401     const CompletionCallback& callback,
402     RequestHandle* out_req) {
403   DVLOG(1) << __FUNCTION__ << " " << host;
404   DCHECK(CalledOnValidThread());
405   base::TimeTicks request_start = base::TimeTicks::Now();
406
407   if (callback.is_null() || !private_key || !cert || host.empty()) {
408     RecordGetChannelIDResult(INVALID_ARGUMENT);
409     return ERR_INVALID_ARGUMENT;
410   }
411
412   std::string domain = GetDomainForHost(host);
413   if (domain.empty()) {
414     RecordGetChannelIDResult(INVALID_ARGUMENT);
415     return ERR_INVALID_ARGUMENT;
416   }
417
418   requests_++;
419
420   // See if a request for the same domain is currently in flight.
421   bool create_if_missing = true;
422   if (JoinToInFlightRequest(request_start, domain, private_key, cert,
423                             create_if_missing, callback, out_req)) {
424     return ERR_IO_PENDING;
425   }
426
427   int err = LookupChannelID(request_start, domain, private_key, cert,
428                                   create_if_missing, callback, out_req);
429   if (err == ERR_FILE_NOT_FOUND) {
430     // Sync lookup did not find a valid cert.  Start generating a new one.
431     workers_created_++;
432     ChannelIDServiceWorker* worker = new ChannelIDServiceWorker(
433         domain,
434         base::Bind(&ChannelIDService::GeneratedChannelID,
435                    weak_ptr_factory_.GetWeakPtr()));
436     if (!worker->Start(task_runner_)) {
437       // TODO(rkn): Log to the NetLog.
438       LOG(ERROR) << "ChannelIDServiceWorker couldn't be started.";
439       RecordGetChannelIDResult(WORKER_FAILURE);
440       return ERR_INSUFFICIENT_RESOURCES;
441     }
442     // We are waiting for cert generation.  Create a job & request to track it.
443     ChannelIDServiceJob* job = new ChannelIDServiceJob(create_if_missing);
444     inflight_[domain] = job;
445
446     ChannelIDServiceRequest* request = new ChannelIDServiceRequest(
447         request_start,
448         base::Bind(&RequestHandle::OnRequestComplete,
449                    base::Unretained(out_req)),
450         private_key,
451         cert);
452     job->AddRequest(request);
453     out_req->RequestStarted(this, request, callback);
454     return ERR_IO_PENDING;
455   }
456
457   return err;
458 }
459
460 int ChannelIDService::GetChannelID(
461     const std::string& host,
462     std::string* private_key,
463     std::string* cert,
464     const CompletionCallback& callback,
465     RequestHandle* out_req) {
466   DVLOG(1) << __FUNCTION__ << " " << host;
467   DCHECK(CalledOnValidThread());
468   base::TimeTicks request_start = base::TimeTicks::Now();
469
470   if (callback.is_null() || !private_key || !cert || host.empty()) {
471     RecordGetChannelIDResult(INVALID_ARGUMENT);
472     return ERR_INVALID_ARGUMENT;
473   }
474
475   std::string domain = GetDomainForHost(host);
476   if (domain.empty()) {
477     RecordGetChannelIDResult(INVALID_ARGUMENT);
478     return ERR_INVALID_ARGUMENT;
479   }
480
481   requests_++;
482
483   // See if a request for the same domain currently in flight.
484   bool create_if_missing = false;
485   if (JoinToInFlightRequest(request_start, domain, private_key, cert,
486                             create_if_missing, callback, out_req)) {
487     return ERR_IO_PENDING;
488   }
489
490   int err = LookupChannelID(request_start, domain, private_key, cert,
491                             create_if_missing, callback, out_req);
492   return err;
493 }
494
495 void ChannelIDService::GotChannelID(
496     int err,
497     const std::string& server_identifier,
498     base::Time expiration_time,
499     const std::string& key,
500     const std::string& cert) {
501   DCHECK(CalledOnValidThread());
502
503   std::map<std::string, ChannelIDServiceJob*>::iterator j;
504   j = inflight_.find(server_identifier);
505   if (j == inflight_.end()) {
506     NOTREACHED();
507     return;
508   }
509
510   if (err == OK) {
511     // Async DB lookup found a valid cert.
512     DVLOG(1) << "Cert store had valid cert for " << server_identifier;
513     cert_store_hits_++;
514     // ChannelIDServiceRequest::Post will do the histograms and stuff.
515     HandleResult(OK, server_identifier, key, cert);
516     return;
517   }
518   // Async lookup failed or the certificate was missing. Return the error
519   // directly, unless the certificate was missing and a request asked to create
520   // one.
521   if (err != ERR_FILE_NOT_FOUND || !j->second->CreateIfMissing()) {
522     HandleResult(err, server_identifier, key, cert);
523     return;
524   }
525   // At least one request asked to create a cert => start generating a new one.
526   workers_created_++;
527   ChannelIDServiceWorker* worker = new ChannelIDServiceWorker(
528       server_identifier,
529       base::Bind(&ChannelIDService::GeneratedChannelID,
530                  weak_ptr_factory_.GetWeakPtr()));
531   if (!worker->Start(task_runner_)) {
532     // TODO(rkn): Log to the NetLog.
533     LOG(ERROR) << "ChannelIDServiceWorker couldn't be started.";
534     HandleResult(ERR_INSUFFICIENT_RESOURCES,
535                  server_identifier,
536                  std::string(),
537                  std::string());
538   }
539 }
540
541 ChannelIDStore* ChannelIDService::GetChannelIDStore() {
542   return channel_id_store_.get();
543 }
544
545 void ChannelIDService::CancelRequest(ChannelIDServiceRequest* req) {
546   DCHECK(CalledOnValidThread());
547   req->Cancel();
548 }
549
550 void ChannelIDService::GeneratedChannelID(
551     const std::string& server_identifier,
552     int error,
553     scoped_ptr<ChannelIDStore::ChannelID> cert) {
554   DCHECK(CalledOnValidThread());
555
556   if (error == OK) {
557     // TODO(mattm): we should just Pass() the cert object to
558     // SetChannelID().
559     channel_id_store_->SetChannelID(
560         cert->server_identifier(),
561         cert->creation_time(),
562         cert->expiration_time(),
563         cert->private_key(),
564         cert->cert());
565
566     HandleResult(error, server_identifier, cert->private_key(), cert->cert());
567   } else {
568     HandleResult(error, server_identifier, std::string(), std::string());
569   }
570 }
571
572 void ChannelIDService::HandleResult(
573     int error,
574     const std::string& server_identifier,
575     const std::string& private_key,
576     const std::string& cert) {
577   DCHECK(CalledOnValidThread());
578
579   std::map<std::string, ChannelIDServiceJob*>::iterator j;
580   j = inflight_.find(server_identifier);
581   if (j == inflight_.end()) {
582     NOTREACHED();
583     return;
584   }
585   ChannelIDServiceJob* job = j->second;
586   inflight_.erase(j);
587
588   job->HandleResult(error, private_key, cert);
589   delete job;
590 }
591
592 bool ChannelIDService::JoinToInFlightRequest(
593     const base::TimeTicks& request_start,
594     const std::string& domain,
595     std::string* private_key,
596     std::string* cert,
597     bool create_if_missing,
598     const CompletionCallback& callback,
599     RequestHandle* out_req) {
600   ChannelIDServiceJob* job = NULL;
601   std::map<std::string, ChannelIDServiceJob*>::const_iterator j =
602       inflight_.find(domain);
603   if (j != inflight_.end()) {
604     // A request for the same domain is in flight already. We'll attach our
605     // callback, but we'll also mark it as requiring a cert if one's mising.
606     job = j->second;
607     inflight_joins_++;
608
609     ChannelIDServiceRequest* request = new ChannelIDServiceRequest(
610         request_start,
611         base::Bind(&RequestHandle::OnRequestComplete,
612                    base::Unretained(out_req)),
613         private_key,
614         cert);
615     job->AddRequest(request, create_if_missing);
616     out_req->RequestStarted(this, request, callback);
617     return true;
618   }
619   return false;
620 }
621
622 int ChannelIDService::LookupChannelID(
623     const base::TimeTicks& request_start,
624     const std::string& domain,
625     std::string* private_key,
626     std::string* cert,
627     bool create_if_missing,
628     const CompletionCallback& callback,
629     RequestHandle* out_req) {
630   // Check if a domain bound cert already exists for this domain. Note that
631   // |expiration_time| is ignored, and expired certs are considered valid.
632   base::Time expiration_time;
633   int err = channel_id_store_->GetChannelID(
634       domain,
635       &expiration_time  /* ignored */,
636       private_key,
637       cert,
638       base::Bind(&ChannelIDService::GotChannelID,
639                  weak_ptr_factory_.GetWeakPtr()));
640
641   if (err == OK) {
642     // Sync lookup found a valid cert.
643     DVLOG(1) << "Cert store had valid cert for " << domain;
644     cert_store_hits_++;
645     RecordGetChannelIDResult(SYNC_SUCCESS);
646     base::TimeDelta request_time = base::TimeTicks::Now() - request_start;
647     UMA_HISTOGRAM_TIMES("DomainBoundCerts.GetCertTimeSync", request_time);
648     RecordGetChannelIDTime(request_time);
649     return OK;
650   }
651
652   if (err == ERR_IO_PENDING) {
653     // We are waiting for async DB lookup.  Create a job & request to track it.
654     ChannelIDServiceJob* job = new ChannelIDServiceJob(create_if_missing);
655     inflight_[domain] = job;
656
657     ChannelIDServiceRequest* request = new ChannelIDServiceRequest(
658         request_start,
659         base::Bind(&RequestHandle::OnRequestComplete,
660                    base::Unretained(out_req)),
661         private_key,
662         cert);
663     job->AddRequest(request);
664     out_req->RequestStarted(this, request, callback);
665     return ERR_IO_PENDING;
666   }
667
668   return err;
669 }
670
671 int ChannelIDService::cert_count() {
672   return channel_id_store_->GetChannelIDCount();
673 }
674
675 }  // namespace net