krb5-auth 0.6 → 0.7

Sign up to get free protection for your applications and to get access to all the features.
data/README CHANGED
@@ -4,18 +4,18 @@ This is an implementation of Ruby bindings for the Kerberos library.
4
4
 
5
5
  To build a gem, you'll want to:
6
6
 
7
- $ gem build krb5-auth.gemspec
8
- # gem install krb5-auth-{VERSION}.gem
7
+ $ rake gem
8
+ $ sudo gem install pkg/krb5-auth-{VERSION}.gem
9
9
 
10
10
  To build an RPM, you'll want to:
11
11
 
12
- $ gem build krb5-auth.gemspec
13
- $ gem2rpm -s -t rubygem-krb5-auth.spec krb5-auth-{VERSION}.gem
14
- $ rpmbuild --rebuild rubygem-krb5-auth-{VERSION}-{RELEASE}.fc8.src.rpm
12
+ $ rake rpm
13
+ $ sudo rpm -Uvh pkg/{i386,x86_64}/rubygem-krb5-auth-{VERSION}.{i386,x86_64}.rpm
15
14
 
16
15
  and then install the resulting RPM.
17
16
 
18
17
  To build it just for development, you'll want to:
19
- $ cd ext
20
- $ ruby extconf.rb
21
- $ make
18
+ $ rake build
19
+
20
+ To build the documentation, make sure you have rdoc installed, and then:
21
+ $ rake rdoc
@@ -0,0 +1,103 @@
1
+ # -*- ruby -*-
2
+ # Rakefile: build ruby kerberos bindings
3
+ #
4
+ # Copyright (C) 2008 Red Hat, Inc.
5
+ #
6
+ # Distributed under the GNU Lesser General Public License v2.1 or later.
7
+ # See COPYING for details
8
+ #
9
+ # Chris Lalancette <clalance@redhat.com>
10
+
11
+ # Rakefile for ruby-rpm -*- ruby -*-
12
+ require 'rake/clean'
13
+ require 'rake/rdoctask'
14
+ require 'rake/testtask'
15
+ require 'rake/gempackagetask'
16
+
17
+ PKG_NAME='krb5-auth'
18
+ PKG_VERSION='0.7'
19
+
20
+ EXT_CONF='ext/extconf.rb'
21
+ MAKEFILE='ext/Makefile'
22
+ KRB5AUTH_MODULE='ext/krb5_auth.so'
23
+ SPEC_FILE='rubygem-krb5-auth.spec'
24
+ KRB5AUTH_SRC='ext/ruby_krb5_auth.c'
25
+
26
+ CLEAN.include [ "ext/*.o", KRB5AUTH_MODULE ]
27
+ CLOBBER.include [ "ext/mkmf.log", MAKEFILE ]
28
+
29
+ #
30
+ # Build locally
31
+ #
32
+ file MAKEFILE => EXT_CONF do |t|
33
+ Dir::chdir(File::dirname(EXT_CONF)) do
34
+ unless sh "ruby #{File::basename(EXT_CONF)}"
35
+ $stderr.puts "Failed to run extconf"
36
+ break
37
+ end
38
+ end
39
+ end
40
+ file KRB5AUTH_MODULE => [ MAKEFILE, KRB5AUTH_SRC ] do |t|
41
+ Dir::chdir(File::dirname(EXT_CONF)) do
42
+ unless sh "make"
43
+ $stderr.puts "make failed"
44
+ break
45
+ end
46
+ end
47
+ end
48
+ desc "Build the native library"
49
+ task :build => KRB5AUTH_MODULE
50
+
51
+ Rake::RDocTask.new(:rdoc) do |rd|
52
+ rd.rdoc_dir = "doc"
53
+ rd.rdoc_files.include("ext/*.c")
54
+ end
55
+
56
+ #
57
+ # Package tasks
58
+ #
59
+
60
+ PKG_FILES = FileList[
61
+ "README","bin/example.rb","ext/extconf.rb",
62
+ "ext/ruby_krb5_auth.c","COPYING","TODO","Rakefile"
63
+ ]
64
+
65
+ SPEC = Gem::Specification.new do |s|
66
+ s.name = PKG_NAME
67
+ s.version = PKG_VERSION
68
+ s.email = "clalance@redhat.com"
69
+ s.homepage = "http://rubyforge.org/projects/krb5-auth/"
70
+ s.summary = "Kerberos binding for Ruby"
71
+ s.files = PKG_FILES
72
+ s.autorequire = "Krb5Auth"
73
+ s.require_paths = [ "ext" ]
74
+ s.extensions = "ext/extconf.rb"
75
+ s.author = "Chris Lalancette"
76
+ s.platform = Gem::Platform::RUBY
77
+ s.has_rdoc = true
78
+ end
79
+
80
+ Rake::GemPackageTask.new(SPEC) do |pkg|
81
+ pkg.need_tar = true
82
+ pkg.need_zip = true
83
+ end
84
+
85
+ desc "Build (S)RPM for #{PKG_NAME}"
86
+ task :rpm => [ :package ] do |t|
87
+ system("sed -e 's/@VERSION@/#{PKG_VERSION}/' #{SPEC_FILE} > pkg/#{SPEC_FILE}")
88
+ Dir::chdir("pkg") do |dir|
89
+ dir = File::expand_path(".")
90
+ system("rpmbuild --define '_topdir #{dir}' --define '_sourcedir #{dir}' --define '_srcrpmdir #{dir}' --define '_rpmdir #{dir}' --define '_builddir #{dir}' -ba #{SPEC_FILE} > rpmbuild.log 2>&1")
91
+ if $? != 0
92
+ raise "rpmbuild failed"
93
+ end
94
+ end
95
+ end
96
+
97
+ #
98
+ # Default
99
+ #
100
+
101
+ desc "Default task: build all"
102
+ task :default => [ :build, :rdoc, :rpm ] do |t|
103
+ end
data/TODO CHANGED
@@ -1,4 +1,3 @@
1
1
  TODO items:
2
- 1. Implement cached principal listing (similar to klist)
3
- 2. Finer-grained error reporting
4
- 3. Documentation on how to use the API
2
+ 1. Finer-grained error reporting
3
+ 2. Automated tests
@@ -23,6 +23,13 @@ krb5.cache
23
23
 
24
24
  puts "Principal: " + krb5.get_default_principal
25
25
 
26
+ # List all of the credentials in the cache, and expiration times, etc.
27
+ krb5.list_cache.each do |cred|
28
+ starttime = DateTime.strptime(cred.starttime.to_s, "%s")
29
+ endtime = DateTime.strptime(cred.endtime.to_s, "%s")
30
+ puts "Client: " + cred.client + " Server: " + cred.server + " starttime: " + starttime.strftime("%D %T") + " endtime: " + endtime.strftime("%D %T")
31
+ end
32
+
26
33
  # destroy those same credentials from the default cache location
27
34
  krb5.destroy
28
35
 
@@ -20,13 +20,14 @@
20
20
  * Author: Chris Lalancette <clalance@redhat.com>
21
21
  */
22
22
 
23
- #include "ruby.h"
24
- #include "krb5.h"
23
+ #include <ruby.h>
24
+ #include <krb5.h>
25
25
  #include <stdio.h>
26
26
  #include <strings.h>
27
27
 
28
28
  static VALUE mKerberos;
29
29
  static VALUE cKrb5;
30
+ static VALUE cCred;
30
31
  static VALUE cKrb5_Exception;
31
32
 
32
33
  struct ruby_krb5 {
@@ -64,6 +65,12 @@ static void kerb_free(void *p)
64
65
  free(kerb);
65
66
  }
66
67
 
68
+ /*
69
+ * call-seq:
70
+ * new
71
+ *
72
+ * Create a new Krb5Auth::Krb5 object. This must be called before any other methods are called. Returns true on success, raises Krb5Auth::Krb5::Exception on failure.
73
+ */
67
74
  static VALUE Krb5_new(VALUE self)
68
75
  {
69
76
  struct ruby_krb5 *kerb;
@@ -86,6 +93,12 @@ static VALUE Krb5_new(VALUE self)
86
93
  return Data_Wrap_Struct(cKrb5, NULL, kerb_free, kerb);
87
94
  }
88
95
 
96
+ /*
97
+ * call-seq:
98
+ * get_default_realm -> string
99
+ *
100
+ * Call krb5_get_default_realm() to get the default realm. Returns the default realm on success, raises Krb5Auth::Krb5::Exception on failure.
101
+ */
89
102
  static VALUE Krb5_get_default_realm(VALUE self)
90
103
  {
91
104
  struct ruby_krb5 *kerb;
@@ -112,6 +125,12 @@ static VALUE Krb5_get_default_realm(VALUE self)
112
125
  return result;
113
126
  }
114
127
 
128
+ /*
129
+ * call-seq:
130
+ * get_default_principal -> string
131
+ *
132
+ * Call krb5_cc_get_principal() to get the principal from the default cachefile. Returns the default principal on success, raises Krb5Auth::Krb5::Exception on failure.
133
+ */
115
134
  static VALUE Krb5_get_default_principal(VALUE self)
116
135
  {
117
136
  struct ruby_krb5 *kerb;
@@ -154,6 +173,12 @@ static VALUE Krb5_get_default_principal(VALUE self)
154
173
  return result;
155
174
  }
156
175
 
176
+ /*
177
+ * call-seq:
178
+ * get_init_creds_password(username, password)
179
+ *
180
+ * Call krb5_get_init_creds_password() to get credentials based on a username and password. Returns true on success, raises Krb5Auth::Krb5::Exception on failure.
181
+ */
157
182
  static VALUE Krb5_get_init_creds_password(VALUE self, VALUE _user, VALUE _pass)
158
183
  {
159
184
  Check_Type(_user,T_STRING);
@@ -191,6 +216,12 @@ static VALUE Krb5_get_init_creds_password(VALUE self, VALUE _user, VALUE _pass)
191
216
  return Qfalse;
192
217
  }
193
218
 
219
+ /*
220
+ * call-seq:
221
+ * get_init_creds_keytab([principal][,keytab])
222
+ *
223
+ * Call krb5_get_init_creds_keytab() to get credentials based on a keytab. With no parameters, gets the default principal (probably the username@DEFAULT_REALM) from the default keytab (as configured in /etc/krb5.conf). With one parameter, get the named principal from the default keytab (as configured in /etc/krb5.conf). With two parameters, get the named principal from the named keytab. Returns true on success, raises Krb5Auth::Krb5::Exception on failure.
224
+ */
194
225
  static VALUE Krb5_get_init_creds_keytab(int argc, VALUE *argv, VALUE self)
195
226
  {
196
227
  char *princ;
@@ -270,6 +301,12 @@ static VALUE Krb5_get_init_creds_keytab(int argc, VALUE *argv, VALUE self)
270
301
  return Qfalse;
271
302
  }
272
303
 
304
+ /*
305
+ * call-seq:
306
+ * set_password(new_password)
307
+ *
308
+ * Call krb5_set_password to set the password for this credential to new_password. This requires that the credentials have already been fetched via Krb5.get_init_creds_password or Krb5.get_init_creds_keytab. Returns true on success, raises Krb5Auth::Krb5::Exception on failure.
309
+ */
273
310
  static VALUE Krb5_change_password(VALUE self, VALUE _newpass)
274
311
  {
275
312
  Check_Type(_newpass,T_STRING);
@@ -296,6 +333,12 @@ static VALUE Krb5_change_password(VALUE self, VALUE _newpass)
296
333
  return Qtrue;
297
334
  }
298
335
 
336
+ /*
337
+ * call-seq:
338
+ * cache([cache_name])
339
+ *
340
+ * Call krb5_cc_store_cred to store credentials in a cachefile. With no parameters, it stores the credentials in the default cachefile. With one parameter, it stores the credentials in the named cachefile. This requires that the credentials have already been fetched via Krb5.get_init_creds_password or Krb5.get_init_creds_keytab. Returns true on success, raises Krb5Auth::Krb5::Exception on failure.
341
+ */
299
342
  static VALUE Krb5_cache_creds(int argc, VALUE *argv, VALUE self)
300
343
  {
301
344
  struct ruby_krb5 *kerb;
@@ -363,6 +406,124 @@ static VALUE Krb5_cache_creds(int argc, VALUE *argv, VALUE self)
363
406
  return Qfalse;
364
407
  }
365
408
 
409
+ /*
410
+ * call-seq:
411
+ * list_cache([cache_name]) -> array
412
+ *
413
+ * Call krb5_cc_next_cred to fetch credentials from a cachefile. With no parameters, it fetches the credentials in the default cachefile. With one parameter, it fetches the credentials in the named cachefile. Returns a list of Krb5Auth::Krb5::Cred objects on success, raises Krb5Auth::Krb5::Exception on failure.
414
+ */
415
+ static VALUE Krb5_list_cache_creds(int argc, VALUE *argv, VALUE self)
416
+ {
417
+ struct ruby_krb5 *kerb;
418
+ krb5_error_code krbret;
419
+ char *cache_name;
420
+ krb5_ccache cc;
421
+ krb5_cc_cursor cur;
422
+ krb5_creds creds;
423
+ char *name;
424
+ char *sname;
425
+ krb5_ticket *tkt;
426
+ VALUE result;
427
+ VALUE line;
428
+
429
+ if (argc == 0) {
430
+ cache_name = NULL;
431
+ }
432
+ else if (argc == 1) {
433
+ Check_Type(argv[0], T_STRING);
434
+ cache_name = STR2CSTR(argv[0]);
435
+ }
436
+ else {
437
+ rb_raise(rb_eRuntimeError, "Invalid arguments");
438
+ }
439
+
440
+ Data_Get_Struct(self, struct ruby_krb5, kerb);
441
+ if (!kerb) {
442
+ NOSTRUCT_EXCEPT();
443
+ return Qfalse;
444
+ }
445
+
446
+ if (cache_name == NULL) {
447
+ krbret = krb5_cc_default(kerb->ctx, &cc);
448
+ }
449
+ else {
450
+ krbret = krb5_cc_resolve(kerb->ctx, cache_name, &cc);
451
+ }
452
+
453
+ if (krbret) {
454
+ goto cache_fail_raise;
455
+ }
456
+
457
+ krbret = krb5_cc_start_seq_get(kerb->ctx, cc, &cur);
458
+ if (krbret) {
459
+ goto cache_fail_close;
460
+ }
461
+
462
+ result = rb_ary_new();
463
+ while (!(krbret = krb5_cc_next_cred(kerb->ctx, cc, &cur, &creds))) {
464
+ krbret = krb5_unparse_name(kerb->ctx, creds.client, &name);
465
+ if (krbret) {
466
+ krb5_free_cred_contents(kerb->ctx, &creds);
467
+ break;
468
+ }
469
+ krbret = krb5_unparse_name(kerb->ctx, creds.server, &sname);
470
+ if (krbret) {
471
+ free(name);
472
+ krb5_free_cred_contents(kerb->ctx, &creds);
473
+ break;
474
+ }
475
+ krbret = krb5_decode_ticket(&creds.ticket, &tkt);
476
+ if (krbret) {
477
+ free(sname);
478
+ free(name);
479
+ krb5_free_cred_contents(kerb->ctx, &creds);
480
+ break;
481
+ }
482
+ line = rb_class_new_instance(0, NULL, cCred);
483
+ rb_iv_set(line, "@client", rb_str_new2(name));
484
+ rb_iv_set(line, "@server", rb_str_new2(sname));
485
+ rb_iv_set(line, "@starttime", INT2NUM(creds.times.starttime));
486
+ rb_iv_set(line, "@authtime", INT2NUM(creds.times.authtime));
487
+ rb_iv_set(line, "@endtime", INT2NUM(creds.times.endtime));
488
+ rb_iv_set(line, "@ticket_flags", INT2NUM(creds.ticket_flags));
489
+ rb_iv_set(line, "@cred_enctype", INT2NUM(creds.keyblock.enctype));
490
+ rb_iv_set(line, "@ticket_enctype", INT2NUM(tkt->enc_part.enctype));
491
+ rb_ary_push(result, line);
492
+ krb5_free_ticket(kerb->ctx, tkt);
493
+ free(sname);
494
+ free(name);
495
+ krb5_free_cred_contents(kerb->ctx, &creds);
496
+ }
497
+
498
+ if (krbret != KRB5_CC_END) {
499
+ // FIXME: do we need to free up "result" here? There will be no
500
+ // references to it, so I think the garbage collector will pick it up,
501
+ // but I'm not sure.
502
+
503
+ goto cache_fail_close;
504
+ }
505
+
506
+ krbret = krb5_cc_end_seq_get(kerb->ctx, cc, &cur);
507
+
508
+ krb5_cc_close(kerb->ctx, cc);
509
+
510
+ return result;
511
+
512
+ cache_fail_close:
513
+ krb5_cc_close(kerb->ctx, cc);
514
+
515
+ cache_fail_raise:
516
+ Krb5_register_error(krbret);
517
+
518
+ return Qfalse;
519
+ }
520
+
521
+ /*
522
+ * call-seq:
523
+ * destroy([cache_name])
524
+ *
525
+ * Call krb5_cc_destroy to destroy all credentials in a cachefile. With no parameters, it destroys the credentials in the default cachefile. With one parameter, it destroys the credentials in the named cachefile. Returns true on success, raises Krb5Auth::Krb5::Exception on failure.
526
+ */
366
527
  static VALUE Krb5_destroy_creds(int argc, VALUE *argv, VALUE self)
367
528
  {
368
529
  struct ruby_krb5 *kerb;
@@ -411,6 +572,12 @@ static VALUE Krb5_destroy_creds(int argc, VALUE *argv, VALUE self)
411
572
  return Qtrue;
412
573
  }
413
574
 
575
+ /*
576
+ * call-seq:
577
+ * close
578
+ *
579
+ * Free up all memory associated with this object. After this is called, no more methods may be called against this object.
580
+ */
414
581
  static VALUE Krb5_close(VALUE self)
415
582
  {
416
583
  struct ruby_krb5 *kerb;
@@ -424,6 +591,11 @@ static VALUE Krb5_close(VALUE self)
424
591
  return Qnil;
425
592
  }
426
593
 
594
+ /*
595
+ * = Ruby bindings for kerberos
596
+ *
597
+ * The module Krb5Auth provides bindings to kerberos version 5 libraries
598
+ */
427
599
  void Init_krb5_auth()
428
600
  {
429
601
  mKerberos = rb_define_module("Krb5Auth");
@@ -432,6 +604,55 @@ void Init_krb5_auth()
432
604
 
433
605
  cKrb5_Exception = rb_define_class_under(cKrb5, "Exception", rb_eStandardError);
434
606
 
607
+ cCred = rb_define_class_under(cKrb5, "Cred", rb_cObject);
608
+ rb_define_attr(cCred, "client", 1, 0);
609
+ rb_define_attr(cCred, "server", 1, 0);
610
+ rb_define_attr(cCred, "starttime", 1, 0);
611
+ rb_define_attr(cCred, "authtime", 1, 0);
612
+ rb_define_attr(cCred, "endtime", 1, 0);
613
+ rb_define_attr(cCred, "ticket_flags", 1, 0);
614
+ rb_define_attr(cCred, "cred_enctype", 1, 0);
615
+ rb_define_attr(cCred, "ticket_enctype", 1, 0);
616
+
617
+ #define DEF_FLAG_CONST(name) \
618
+ rb_define_const(cCred, #name, INT2NUM(name))
619
+
620
+ DEF_FLAG_CONST(TKT_FLG_FORWARDABLE);
621
+ DEF_FLAG_CONST(TKT_FLG_FORWARDED);
622
+ DEF_FLAG_CONST(TKT_FLG_PROXIABLE);
623
+ DEF_FLAG_CONST(TKT_FLG_PROXY);
624
+ DEF_FLAG_CONST(TKT_FLG_MAY_POSTDATE);
625
+ DEF_FLAG_CONST(TKT_FLG_POSTDATED);
626
+ DEF_FLAG_CONST(TKT_FLG_INVALID);
627
+ DEF_FLAG_CONST(TKT_FLG_RENEWABLE);
628
+ DEF_FLAG_CONST(TKT_FLG_INITIAL);
629
+ DEF_FLAG_CONST(TKT_FLG_HW_AUTH);
630
+ DEF_FLAG_CONST(TKT_FLG_PRE_AUTH);
631
+ DEF_FLAG_CONST(TKT_FLG_TRANSIT_POLICY_CHECKED);
632
+ DEF_FLAG_CONST(TKT_FLG_OK_AS_DELEGATE);
633
+ DEF_FLAG_CONST(TKT_FLG_ANONYMOUS);
634
+
635
+ #undef DEF_FLAG_CONST
636
+
637
+ #define DEF_ENC_CONST(name) \
638
+ rb_define_const(cCred, #name, INT2NUM(name))
639
+
640
+ DEF_ENC_CONST(ENCTYPE_NULL);
641
+ DEF_ENC_CONST(ENCTYPE_DES_CBC_CRC);
642
+ DEF_ENC_CONST(ENCTYPE_DES_CBC_MD4);
643
+ DEF_ENC_CONST(ENCTYPE_DES_CBC_MD5);
644
+ DEF_ENC_CONST(ENCTYPE_DES_CBC_RAW);
645
+ DEF_ENC_CONST(ENCTYPE_DES3_CBC_SHA);
646
+ DEF_ENC_CONST(ENCTYPE_DES3_CBC_RAW);
647
+ DEF_ENC_CONST(ENCTYPE_DES_HMAC_SHA1);
648
+ DEF_ENC_CONST(ENCTYPE_DES3_CBC_SHA1);
649
+ DEF_ENC_CONST(ENCTYPE_AES128_CTS_HMAC_SHA1_96);
650
+ DEF_ENC_CONST(ENCTYPE_AES256_CTS_HMAC_SHA1_96);
651
+ DEF_ENC_CONST(ENCTYPE_ARCFOUR_HMAC);
652
+ DEF_ENC_CONST(ENCTYPE_ARCFOUR_HMAC_EXP);
653
+ DEF_ENC_CONST(ENCTYPE_UNKNOWN);
654
+ #undef DEF_ENC_CONST
655
+
435
656
  rb_define_singleton_method(cKrb5, "new", Krb5_new, 0);
436
657
  rb_define_method(cKrb5, "get_init_creds_password", Krb5_get_init_creds_password, 2);
437
658
  rb_define_method(cKrb5, "get_init_creds_keytab", Krb5_get_init_creds_keytab, -1);
@@ -439,6 +660,7 @@ void Init_krb5_auth()
439
660
  rb_define_method(cKrb5, "get_default_principal", Krb5_get_default_principal, 0);
440
661
  rb_define_method(cKrb5, "change_password", Krb5_change_password, 1);
441
662
  rb_define_method(cKrb5, "cache", Krb5_cache_creds, -1);
663
+ rb_define_method(cKrb5, "list_cache", Krb5_list_cache_creds, -1);
442
664
  rb_define_method(cKrb5, "destroy", Krb5_destroy_creds, -1);
443
665
  rb_define_method(cKrb5, "close", Krb5_close, 0);
444
666
  }
metadata CHANGED
@@ -3,13 +3,13 @@ rubygems_version: 0.9.4
3
3
  specification_version: 1
4
4
  name: krb5-auth
5
5
  version: !ruby/object:Gem::Version
6
- version: "0.6"
7
- date: 2008-05-21 00:00:00 +02:00
6
+ version: "0.7"
7
+ date: 2008-06-20 00:00:00 +02:00
8
8
  summary: Kerberos binding for Ruby
9
9
  require_paths:
10
- - lib
10
+ - ext
11
11
  email: clalance@redhat.com
12
- homepage:
12
+ homepage: http://rubyforge.org/projects/krb5-auth/
13
13
  rubyforge_project:
14
14
  description:
15
15
  autorequire: Krb5Auth
@@ -30,12 +30,12 @@ authors:
30
30
  - Chris Lalancette
31
31
  files:
32
32
  - README
33
- - lib/krb5-auth.rb
34
33
  - bin/example.rb
35
34
  - ext/extconf.rb
36
35
  - ext/ruby_krb5_auth.c
37
36
  - COPYING
38
37
  - TODO
38
+ - Rakefile
39
39
  test_files: []
40
40
 
41
41
  rdoc_options: []
@@ -1,23 +0,0 @@
1
- #
2
- # krb5-auth.rb: main module for the ruby-krb5-auth bindings
3
- #
4
- # Copyright (C) 2008 Red Hat, Inc.
5
- #
6
- # Distributed under the GNU Lesser General Public License v2.1 or later.
7
- # See COPYING for details
8
- #
9
- # Chris Lalancette <clalance@redhat.com>
10
-
11
- require 'krb5_auth'
12
-
13
- # Ruby C extension for basic Kerberos functions. Tested on Linux with
14
- # Kerberos 5-1.6.1
15
- module Krb5Auth
16
-
17
- # Krb5 contains the kerberos end user functionality, such as user
18
- # authentication and password changes.
19
- class Krb5
20
-
21
- end
22
-
23
- end