~ubuntu-branches/ubuntu/intrepid/schroot/intrepid

« back to all changes in this revision

Viewing changes to sbuild/sbuild-auth.cc

  • Committer: Bazaar Package Importer
  • Author(s): Reinhard Tartler
  • Date: 2006-07-08 18:33:28 UTC
  • mfrom: (1.1.4 upstream)
  • Revision ID: james.westby@ubuntu.com-20060708183328-rlo4mpldmyoda55q
Tags: 0.99.2-2ubuntu1
* remerge ubuntu changes:
  + debian/control: libpam-dev (>> 0.79-3ubuntu6)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/* Copyright © 2005-2006  Roger Leigh <rleigh@debian.org>
 
2
 *
 
3
 * schroot is free software; you can redistribute it and/or modify it
 
4
 * under the terms of the GNU General Public License as published by
 
5
 * the Free Software Foundation; either version 2 of the License, or
 
6
 * (at your option) any later version.
 
7
 *
 
8
 * schroot is distributed in the hope that it will be useful, but
 
9
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 
11
 * General Public License for more details.
 
12
 *
 
13
 * You should have received a copy of the GNU General Public License
 
14
 * along with this program; if not, write to the Free Software
 
15
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston,
 
16
 * MA  02111-1307  USA
 
17
 *
 
18
 *********************************************************************/
 
19
 
 
20
#include <config.h>
 
21
 
 
22
#include "sbuild-auth.h"
 
23
#include "sbuild-auth-conv.h"
 
24
#include "sbuild-auth-conv-tty.h"
 
25
 
 
26
#include <cassert>
 
27
#include <cerrno>
 
28
#include <cstdlib>
 
29
#include <cstring>
 
30
#include <iostream>
 
31
#include <sstream>
 
32
 
 
33
#include <syslog.h>
 
34
 
 
35
#include <boost/format.hpp>
 
36
 
 
37
using std::cerr;
 
38
using std::endl;
 
39
using boost::format;
 
40
using namespace sbuild;
 
41
 
 
42
namespace
 
43
{
 
44
 
 
45
  typedef std::pair<sbuild::auth::error_code,const char *> emap;
 
46
 
 
47
  /**
 
48
   * This is a list of the supported error codes.  It's used to
 
49
   * construct the real error codes map.
 
50
   */
 
51
  emap init_errors[] =
 
52
    {
 
53
      emap(auth::HOSTNAME,        N_("Failed to get hostname")),
 
54
      emap(auth::USER,            N_("User not found")),
 
55
      emap(auth::AUTHENTICATION,  N_("Authentication failed")),
 
56
      emap(auth::AUTHORISATION,   N_("Access not authorised")),
 
57
      emap(auth::PAM_DOUBLE_INIT, N_("PAM is already initialised")),
 
58
      emap(auth::PAM,             N_("PAM error"))
 
59
    };
 
60
 
 
61
}
 
62
 
 
63
template<>
 
64
custom_error<auth::error_code>::map_type
 
65
custom_error<auth::error_code>::error_strings
 
66
(init_errors,
 
67
 init_errors + (sizeof(init_errors) / sizeof(init_errors[0])));
 
68
 
 
69
namespace
 
70
{
 
71
 
 
72
  /* This is the glue to link PAM user interaction with auth_conv. */
 
73
  int
 
74
  auth_conv_hook (int                        num_msg,
 
75
                  const struct pam_message **msgm,
 
76
                  struct pam_response      **response,
 
77
                  void                      *appdata_ptr)
 
78
  {
 
79
    if (appdata_ptr == 0)
 
80
      return PAM_CONV_ERR;
 
81
 
 
82
    auth_conv *conv = static_cast<auth_conv *>(appdata_ptr);
 
83
    assert (conv != 0);
 
84
 
 
85
    /* Construct a message vector */
 
86
    auth_conv::message_list messages;
 
87
    for (int i = 0; i < num_msg; ++i)
 
88
      {
 
89
        const struct pam_message *source = msgm[i];
 
90
 
 
91
        auth_message
 
92
          message(static_cast<auth_message::message_type>(source->msg_style),
 
93
                  source->msg);
 
94
        messages.push_back(message);
 
95
      }
 
96
 
 
97
    /* Do the conversation */
 
98
    bool status = conv->conversation(messages);
 
99
 
 
100
    if (status == true)
 
101
      {
 
102
        /* Copy response into **reponse */
 
103
        struct pam_response *reply =
 
104
          static_cast<struct pam_response *>
 
105
          (malloc(sizeof(struct pam_response) * num_msg));
 
106
 
 
107
        for (int i = 0; i < num_msg; ++i)
 
108
          {
 
109
            reply[i].resp_retcode = 0;
 
110
            reply[i].resp = strdup(messages[i].response.c_str());
 
111
          }
 
112
 
 
113
        *response = reply;
 
114
        reply = 0;
 
115
 
 
116
        return PAM_SUCCESS;
 
117
      }
 
118
    else
 
119
      return PAM_CONV_ERR;
 
120
  }
 
121
 
 
122
}
 
123
 
 
124
 
 
125
auth::auth (std::string const& service_name):
 
126
  pam(),
 
127
  service(service_name),
 
128
  uid(0),
 
129
  gid(0),
 
130
  user(),
 
131
  command(),
 
132
  home(),
 
133
  shell(),
 
134
  user_environment(),
 
135
  ruid(),
 
136
  ruser(),
 
137
  conv(dynamic_cast<auth_conv *>(new auth_conv_tty)),
 
138
  message_verbosity(VERBOSITY_NORMAL)
 
139
{
 
140
  this->ruid = getuid();
 
141
  struct passwd *pwent = getpwuid(this->ruid);
 
142
  if (pwent == 0)
 
143
    {
 
144
      // TODO: Convert to using a lexical cast.
 
145
      std::ostringstream str;
 
146
      str << this->ruid;
 
147
      throw error(str.str(), USER, errno);
 
148
    }
 
149
  this->ruser = pwent->pw_name;
 
150
 
 
151
  /* By default, the auth user is the same as the remote user. */
 
152
  set_user(this->ruser);
 
153
}
 
154
 
 
155
auth::~auth ()
 
156
{
 
157
  // Shutdown PAM.
 
158
  try
 
159
    {
 
160
      stop();
 
161
    }
 
162
  catch (...)
 
163
    {
 
164
    }
 
165
}
 
166
 
 
167
std::string const&
 
168
auth::get_service () const
 
169
{
 
170
  return this->service;
 
171
}
 
172
 
 
173
uid_t
 
174
auth::get_uid () const
 
175
{
 
176
  return this->uid;
 
177
}
 
178
 
 
179
gid_t
 
180
auth::get_gid () const
 
181
{
 
182
  return this->gid;
 
183
}
 
184
 
 
185
std::string const&
 
186
auth::get_user () const
 
187
{
 
188
  return this->user;
 
189
}
 
190
 
 
191
void
 
192
auth::set_user (std::string const& user)
 
193
{
 
194
  this->uid = 0;
 
195
  this->gid = 0;
 
196
  this->home = "/";
 
197
  this->shell = "/bin/false";
 
198
 
 
199
  this->user = user;
 
200
 
 
201
  struct passwd *pwent = getpwnam(this->user.c_str());
 
202
  if (pwent == 0)
 
203
    {
 
204
      throw error(user, USER, errno);
 
205
    }
 
206
  this->uid = pwent->pw_uid;
 
207
  this->gid = pwent->pw_gid;
 
208
  this->home = pwent->pw_dir;
 
209
  this->shell = pwent->pw_shell;
 
210
  log_debug(DEBUG_INFO)
 
211
    << format("auth uid = %1%, gid = %2%") % this->uid % this->gid
 
212
    << endl;
 
213
}
 
214
 
 
215
string_list const&
 
216
auth::get_command () const
 
217
{
 
218
  return this->command;
 
219
}
 
220
 
 
221
void
 
222
auth::set_command (string_list const& command)
 
223
{
 
224
  this->command = command;
 
225
}
 
226
 
 
227
std::string const&
 
228
auth::get_home () const
 
229
{
 
230
  return this->home;
 
231
}
 
232
 
 
233
std::string const&
 
234
auth::get_shell () const
 
235
{
 
236
  return this->shell;
 
237
}
 
238
 
 
239
environment const&
 
240
auth::get_environment () const
 
241
{
 
242
  return this->user_environment;
 
243
}
 
244
 
 
245
void
 
246
auth::set_environment (char **environment)
 
247
{
 
248
  set_environment(sbuild::environment(environment));
 
249
}
 
250
 
 
251
void
 
252
auth::set_environment (environment const& environment)
 
253
{
 
254
  this->user_environment = environment;
 
255
}
 
256
 
 
257
environment
 
258
auth::get_pam_environment () const
 
259
{
 
260
  return environment(pam_getenvlist(this->pam));
 
261
}
 
262
 
 
263
uid_t
 
264
auth::get_ruid () const
 
265
{
 
266
  return this->ruid;
 
267
}
 
268
 
 
269
std::string const&
 
270
auth::get_ruser () const
 
271
{
 
272
  return this->ruser;
 
273
}
 
274
 
 
275
auth::verbosity
 
276
auth::get_verbosity () const
 
277
{
 
278
  return this->message_verbosity;
 
279
}
 
280
 
 
281
void
 
282
auth::set_verbosity (auth::verbosity verbosity)
 
283
{
 
284
  this->message_verbosity = verbosity;
 
285
}
 
286
 
 
287
auth::conv_ptr&
 
288
auth::get_conv ()
 
289
{
 
290
  return this->conv;
 
291
}
 
292
 
 
293
void
 
294
auth::set_conv (conv_ptr& conv)
 
295
{
 
296
  this->conv = conv;
 
297
}
 
298
 
 
299
void
 
300
auth::run ()
 
301
{
 
302
  try
 
303
    {
 
304
      start();
 
305
      authenticate();
 
306
      setupenv();
 
307
      account();
 
308
      try
 
309
        {
 
310
          cred_establish();
 
311
 
 
312
          const char *authuser = 0;
 
313
          const void *tmpcast = static_cast<const void *>(authuser);
 
314
          pam_get_item(this->pam, PAM_USER, &tmpcast);
 
315
          log_debug(DEBUG_INFO)
 
316
            << format("PAM authentication succeeded for user %1%") % authuser
 
317
            << endl;
 
318
 
 
319
          run_impl();
 
320
 
 
321
          /* The session is now finished, either
 
322
             successfully or not.  All PAM operations are
 
323
             now for cleanup and shutdown, and we must
 
324
             clean up whether or not errors were raised at
 
325
             any previous point.  This means only the
 
326
             first error is reported back to the user. */
 
327
 
 
328
          /* Don't cope with failure, since we are now
 
329
             already bailing out, and an error may already
 
330
             have been raised */
 
331
        }
 
332
      catch (error const& e)
 
333
        {
 
334
          try
 
335
            {
 
336
              cred_delete();
 
337
            }
 
338
          catch (error const& discard)
 
339
            {
 
340
            }
 
341
          throw;
 
342
        }
 
343
    }
 
344
  catch (error const& e)
 
345
    {
 
346
      try
 
347
        {
 
348
          /* Don't cope with failure, since we are now already bailing out,
 
349
             and an error may already have been raised */
 
350
          stop();
 
351
        }
 
352
      catch (error const& discard)
 
353
        {
 
354
        }
 
355
      throw;
 
356
    }
 
357
}
 
358
 
 
359
void
 
360
auth::start ()
 
361
{
 
362
  assert(!this->user.empty());
 
363
 
 
364
  if (this->pam != 0)
 
365
    {
 
366
      log_debug(DEBUG_CRITICAL)
 
367
        << "pam_start FAIL (already initialised)" << endl;
 
368
      throw error("Init PAM", PAM_DOUBLE_INIT);
 
369
    }
 
370
 
 
371
  struct pam_conv conv_hook =
 
372
    {
 
373
      auth_conv_hook,
 
374
      static_cast<void *>(this->conv.get())
 
375
    };
 
376
 
 
377
  int pam_status;
 
378
 
 
379
  if ((pam_status =
 
380
       pam_start(this->service.c_str(), this->user.c_str(),
 
381
                 &conv_hook, &this->pam)) != PAM_SUCCESS)
 
382
    {
 
383
      log_debug(DEBUG_WARNING) << "pam_start FAIL" << endl;
 
384
      throw error(PAM, pam_strerror(pam_status));
 
385
    }
 
386
 
 
387
  log_debug(DEBUG_NOTICE) << "pam_start OK" << endl;
 
388
}
 
389
 
 
390
void
 
391
auth::stop ()
 
392
{
 
393
  if (this->pam); // PAM must be initialised
 
394
  {
 
395
    int pam_status;
 
396
 
 
397
    if ((pam_status =
 
398
         pam_end(this->pam, PAM_SUCCESS)) != PAM_SUCCESS)
 
399
      {
 
400
        log_debug(DEBUG_WARNING) << "pam_end FAIL" << endl;
 
401
        throw error(PAM, pam_strerror(pam_status));
 
402
      }
 
403
 
 
404
    this->pam = 0;
 
405
    log_debug(DEBUG_NOTICE) << "pam_end OK" << endl;
 
406
  }
 
407
}
 
408
 
 
409
void
 
410
auth::authenticate ()
 
411
{
 
412
  assert(!this->user.empty());
 
413
  assert(this->pam != 0); // PAM must be initialised
 
414
 
 
415
  int pam_status;
 
416
 
 
417
  if ((pam_status =
 
418
       pam_set_item(this->pam, PAM_RUSER, this->ruser.c_str())) != PAM_SUCCESS)
 
419
    {
 
420
      log_debug(DEBUG_WARNING) << "pam_set_item (PAM_RUSER) FAIL" << endl;
 
421
      throw error(_("Set RUSER"), PAM, pam_strerror(pam_status));
 
422
    }
 
423
 
 
424
  long hl = 256; /* sysconf(_SC_HOST_NAME_MAX); BROKEN with Debian libc6 2.3.2.ds1-22 */
 
425
 
 
426
  char *hostname = new char[hl];
 
427
  if (gethostname(hostname, hl) != 0)
 
428
    {
 
429
      log_debug(DEBUG_CRITICAL) << "gethostname FAIL" << endl;
 
430
      throw error(HOSTNAME, errno);
 
431
    }
 
432
 
 
433
  if ((pam_status =
 
434
       pam_set_item(this->pam, PAM_RHOST, hostname)) != PAM_SUCCESS)
 
435
    {
 
436
      log_debug(DEBUG_WARNING) << "pam_set_item (PAM_RHOST) FAIL" << endl;
 
437
      throw error(_("Set RHOST"), PAM, pam_strerror(pam_status));
 
438
    }
 
439
 
 
440
  delete[] hostname;
 
441
  hostname = 0;
 
442
 
 
443
  const char *tty = ttyname(STDIN_FILENO);
 
444
  if (tty)
 
445
    {
 
446
      if ((pam_status =
 
447
           pam_set_item(this->pam, PAM_TTY, tty)) != PAM_SUCCESS)
 
448
        {
 
449
          log_debug(DEBUG_WARNING) << "pam_set_item (PAM_TTY) FAIL" << endl;
 
450
          throw error(_("Set TTY"), PAM, pam_strerror(pam_status));
 
451
        }
 
452
    }
 
453
 
 
454
  /* Authenticate as required. */
 
455
  switch (get_auth_status())
 
456
    {
 
457
    case STATUS_NONE:
 
458
      if ((pam_status = pam_set_item(this->pam, PAM_USER, this->user.c_str()))
 
459
          != PAM_SUCCESS)
 
460
        {
 
461
          log_debug(DEBUG_WARNING) << "pam_set_item (PAM_USER) FAIL" << endl;
 
462
          throw error(_("Set USER"), PAM, pam_strerror(pam_status));
 
463
        }
 
464
      break;
 
465
 
 
466
    case STATUS_USER:
 
467
      if ((pam_status = pam_authenticate(this->pam, 0)) != PAM_SUCCESS)
 
468
        {
 
469
          log_debug(DEBUG_INFO) << "pam_authenticate FAIL" << endl;
 
470
          syslog(LOG_AUTH|LOG_WARNING, "%s->%s Authentication failure",
 
471
                 this->ruser.c_str(), this->user.c_str());
 
472
          throw error(AUTHENTICATION, pam_strerror(pam_status));
 
473
        }
 
474
      log_debug(DEBUG_NOTICE) << "pam_authenticate OK" << endl;
 
475
      break;
 
476
 
 
477
    case STATUS_FAIL:
 
478
        {
 
479
          log_debug(DEBUG_INFO) << "PAM auth premature FAIL" << endl;
 
480
          cerr << format(_("You do not have permission to access the %1% service."))
 
481
            % this->service
 
482
               << '\n'
 
483
               << _("This failure will be reported.")
 
484
               << endl;
 
485
          syslog(LOG_AUTH|LOG_WARNING,
 
486
                 "%s->%s Unauthorised",
 
487
                 this->ruser.c_str(), this->user.c_str());
 
488
          throw error(AUTHORISATION);
 
489
        }
 
490
    default:
 
491
      break;
 
492
    }
 
493
}
 
494
 
 
495
void
 
496
auth::setupenv ()
 
497
{
 
498
  assert(this->pam != 0); // PAM must be initialised
 
499
 
 
500
  int pam_status;
 
501
 
 
502
  environment environment;
 
503
  if (!this->user_environment.empty())
 
504
    environment = this->user_environment;
 
505
 
 
506
  // For security, PATH is always set to a sane state for root, but
 
507
  // only set in other cases if not preserving the environment.
 
508
  if (this->uid == 0)
 
509
    environment.add(std::make_pair("PATH", "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/bin/X11"));
 
510
  else if (this->user_environment.empty())
 
511
    environment.add(std::make_pair("PATH", "/usr/local/bin:/usr/bin:/bin:/usr/bin/X11:/usr/games"));
 
512
 
 
513
  if (this->user_environment.empty())
 
514
    {
 
515
      if (!this->home.empty() )
 
516
        environment.add(std::make_pair("HOME", this->home));
 
517
      else
 
518
        environment.add(std::make_pair("HOME", "/"));
 
519
      if (!this->user.empty())
 
520
        {
 
521
          environment.add(std::make_pair("LOGNAME", this->user));
 
522
          environment.add(std::make_pair("USER", this->user));
 
523
        }
 
524
      {
 
525
        const char *term = getenv("TERM");
 
526
        if (term)
 
527
          environment.add(std::make_pair("TERM", term));
 
528
      }
 
529
      if (!this->shell.empty())
 
530
        environment.add(std::make_pair("SHELL", this->shell));
 
531
    }
 
532
 
 
533
  // Sanitise environment.
 
534
  environment.remove("BASH_ENV");
 
535
  environment.remove("CDPATH");
 
536
  environment.remove("ENV");
 
537
  environment.remove("HOSTALIASES");
 
538
  environment.remove("IFS");
 
539
  environment.remove("KRB5_CONFIG");
 
540
  environment.remove("KRBCONFDIR");
 
541
  environment.remove("KRBTKFILE");
 
542
  environment.remove("KRB_CONF");
 
543
  environment.remove("LOCALDOMAIN");
 
544
  environment.remove("NLSPATH");
 
545
  environment.remove("PATH_LOCALE");
 
546
  environment.remove("RES_OPTIONS");
 
547
  environment.remove("TERMINFO");
 
548
  environment.remove("TERMINFO_DIRS");
 
549
  environment.remove("TERMPATH");
 
550
 
 
551
  // Find and remove LD_.*,
 
552
  string_list ldvars;
 
553
  for (environment::const_iterator cur = environment.begin();
 
554
       cur != environment.end();)
 
555
    {
 
556
      environment::const_iterator next = cur;
 
557
      next++;
 
558
 
 
559
      if (cur->first.substr(0,3) == "LD_")
 
560
        environment.remove(cur->first);
 
561
 
 
562
      cur = next;
 
563
    }
 
564
 
 
565
  // Move into PAM environment.
 
566
  for (environment::const_iterator cur = environment.begin();
 
567
       cur != environment.end();
 
568
       ++cur)
 
569
    {
 
570
      std::string env_string = cur->first + "=" + cur->second;
 
571
      if ((pam_status =
 
572
           pam_putenv(this->pam, env_string.c_str())) != PAM_SUCCESS)
 
573
        {
 
574
          log_debug(DEBUG_WARNING) << "pam_putenv FAIL" << endl;
 
575
          throw error(PAM, pam_strerror(pam_status));
 
576
        }
 
577
      log_debug(DEBUG_INFO)
 
578
        << format("pam_putenv: set %1%=%2%") % cur->first % cur->second
 
579
        << endl;
 
580
    }
 
581
 
 
582
  log_debug(DEBUG_NOTICE) << "pam_putenv OK" << endl;
 
583
}
 
584
 
 
585
void
 
586
auth::account ()
 
587
{
 
588
  assert(this->pam != 0); // PAM must be initialised
 
589
 
 
590
  int pam_status;
 
591
 
 
592
  if ((pam_status =
 
593
       pam_acct_mgmt(this->pam, 0)) != PAM_SUCCESS)
 
594
    {
 
595
      /* We don't handle changing expired passwords here, since we are
 
596
         not login or ssh. */
 
597
      log_debug(DEBUG_WARNING) << "pam_acct_mgmt FAIL" << endl;
 
598
      throw error(PAM, pam_strerror(pam_status));
 
599
    }
 
600
 
 
601
  log_debug(DEBUG_NOTICE) << "pam_acct_mgmt OK" << endl;
 
602
}
 
603
 
 
604
void
 
605
auth::cred_establish ()
 
606
{
 
607
  assert(this->pam != 0); // PAM must be initialised
 
608
 
 
609
  int pam_status;
 
610
 
 
611
  if ((pam_status =
 
612
       pam_setcred(this->pam, PAM_ESTABLISH_CRED)) != PAM_SUCCESS)
 
613
    {
 
614
      log_debug(DEBUG_WARNING) << "pam_setcred FAIL" << endl;
 
615
      throw error(PAM, pam_strerror(pam_status));
 
616
    }
 
617
 
 
618
  log_debug(DEBUG_NOTICE) << "pam_setcred OK" << endl;
 
619
}
 
620
 
 
621
void
 
622
auth::cred_delete ()
 
623
{
 
624
  assert(this->pam != 0); // PAM must be initialised
 
625
 
 
626
  int pam_status;
 
627
 
 
628
  if ((pam_status =
 
629
       pam_setcred(this->pam, PAM_DELETE_CRED)) != PAM_SUCCESS)
 
630
    {
 
631
      log_debug(DEBUG_WARNING) << "pam_setcred (delete) FAIL" << endl;
 
632
      throw error(PAM, pam_strerror(pam_status));
 
633
    }
 
634
 
 
635
  log_debug(DEBUG_NOTICE) << "pam_setcred (delete) OK" << endl;
 
636
}
 
637
 
 
638
void
 
639
auth::open_session ()
 
640
{
 
641
  assert(this->pam != 0); // PAM must be initialised
 
642
 
 
643
  int pam_status;
 
644
 
 
645
  if ((pam_status =
 
646
       pam_open_session(this->pam, 0)) != PAM_SUCCESS)
 
647
    {
 
648
      log_debug(DEBUG_WARNING) << "pam_open_session FAIL" << endl;
 
649
      throw error(PAM, pam_strerror(pam_status));
 
650
    }
 
651
 
 
652
  log_debug(DEBUG_NOTICE) << "pam_open_session OK" << endl;
 
653
}
 
654
 
 
655
void
 
656
auth::close_session ()
 
657
{
 
658
  assert(this->pam != 0); // PAM must be initialised
 
659
 
 
660
  int pam_status;
 
661
 
 
662
  if ((pam_status =
 
663
       pam_close_session(this->pam, 0)) != PAM_SUCCESS)
 
664
    {
 
665
      log_debug(DEBUG_WARNING) << "pam_close_session FAIL" << endl;
 
666
      throw error(PAM, pam_strerror(pam_status));
 
667
    }
 
668
 
 
669
  log_debug(DEBUG_NOTICE) << "pam_close_session OK" << endl;
 
670
}
 
671
 
 
672
auth::status
 
673
auth::get_auth_status () const
 
674
{
 
675
  status authtype = STATUS_NONE;
 
676
 
 
677
  authtype = change_auth(authtype, STATUS_USER);
 
678
 
 
679
  return authtype;
 
680
}
 
681
 
 
682
const char *
 
683
auth::pam_strerror (int pam_error)
 
684
{
 
685
  return ::pam_strerror (this->pam, pam_error);
 
686
}
 
687
 
 
688
/*
 
689
 * Local Variables:
 
690
 * mode:C++
 
691
 * End:
 
692
 */