File Coverage

blib/lib/Config/OpenSSH/Authkey/Entry/Options.pm
Criterion Covered Total %
statement 87 87 100.0
branch 26 28 92.8
condition 6 14 42.8
subroutine 12 12 100.0
pod 9 9 100.0
total 140 150 93.3


line stmt bran cond sub pod time code
1             # -*- Perl -*-
2             #
3             # Representation of authorized_keys entry options, either associated
4             # with a particular Config::OpenSSH::Authkey::Entry object, or
5             # standalone.
6              
7             package Config::OpenSSH::Authkey::Entry::Options;
8              
9 3     3   26873 use strict;
  3         7  
  3         105  
10 3     3   18 use warnings;
  3         7  
  3         97  
11              
12 3     3   15 use Carp qw/croak/;
  3         5  
  3         3557  
13              
14             our $VERSION = '1.04';
15              
16             # Delved from sshd(8), auth-options.c of OpenSSH 5.2. Insensitive match
17             # required, as OpenSSH uses strncasecmp(3).
18             my $AUTHKEY_OPTION_NAME_RE = qr/(?i)[a-z0-9_-]+/;
19              
20             ######################################################################
21             #
22             # Class methods
23              
24             sub new {
25 4     4 1 5186 my $class = shift;
26 4         9 my $option_string = shift;
27 4         15 my $self = { _options => [] };
28              
29 4 100       22 if ( defined $option_string ) {
30 2         10 $self->{_options} =
31             Config::OpenSSH::Authkey::Entry::Options->split_options($option_string);
32             }
33              
34 4         18 bless $self, $class;
35 4         16 return $self;
36             }
37              
38             sub split_options {
39 7     7 1 1607 my $class = shift;
40 7         10 my $option_string = shift;
41 7         13 my @options;
42              
43             # Inspected OpenSSH auth-options.c,v 1.44 to derive this lexer:
44             #
45             # In OpenSSH, unparsable options result in a call to bad_options and
46             # the entry being rejected. This module is more permissive, in that
47             # any option name is allowed, regardless of whether OpenSSH supports
48             # such an option or whether the option is the correct type (boolean
49             # vs. string value). This makes the module more future proof, at the
50             # cost of allowing garbage through.
51             #
52             # Options are stored using a list of hashrefs, which allows for
53             # duplicate options, and preserves the order of options. Also, an
54             # index is maintained to speed lookups of the data, and to note if
55             # duplicate options exist. This is due to inconsistent handling by
56             # OpenSSH_5.1p1 of command="" vs. from="" vs. environment="" options
57             # when multiple entries are present. Methods are offered to detect and
58             # cleanup such (hopefully rare) duplicate options.
59              
60             OPTION_LEXER: {
61             # String Argument Options - value is a perhaps empty string enclosed
62             # in double quotes. Internal double quotes are allowed, but only if
63             # these are preceded by a backslash.
64 7 100       9 if (
  24         449  
65             $option_string =~ m/ \G ($AUTHKEY_OPTION_NAME_RE)="( (?: \\"|[^"] )*? )"
66             (?:,|[ \t]+)? /cgx
67             ) {
68 7         17 my $option_name = $1;
69 7   50     38 my $option_value = $2 || q{};
70              
71 7         27 push @options, { name => $option_name, value => $option_value };
72              
73 7         16 redo OPTION_LEXER;
74             }
75              
76             # Boolean options - mere presence enables them in OpenSSH
77 17 100       352 if (
78             $option_string =~ m/ \G ($AUTHKEY_OPTION_NAME_RE) (?:,|[ \t]+)? /cgx ) {
79 10         23 my $option_name = $1;
80              
81 10         29 push @options, { name => $option_name };
82              
83 10         36 redo OPTION_LEXER;
84             }
85             }
86              
87 7 100       56 return wantarray ? @options : \@options;
88             }
89              
90             ######################################################################
91             #
92             # Instance methods
93              
94             sub parse {
95 2     2 1 4 my $self = shift;
96 2         3 my $option_string = shift;
97              
98 2         7 $self->{_options} =
99             Config::OpenSSH::Authkey::Entry::Options->split_options($option_string);
100 2         5 return scalar @{ $self->{_options} };
  2         8  
101             }
102              
103             sub as_string {
104 10     10 1 518 my $self = shift;
105 10         14 my @options;
106 10         14 for my $options_ref ( @{ $self->{_options} } ) {
  10         25  
107 18 100       132 if ( exists $options_ref->{value} ) {
108 9         33 ( my $value = $options_ref->{value} ) =~ s/(?
109 9         35 push @options, $options_ref->{name} . '="' . $value . '"';
110             } else {
111 9         233 push @options, $options_ref->{name};
112             }
113             }
114 10         56 return join( q{,}, @options );
115             }
116              
117             # NOTE - boolean return the name of the option, while string value
118             # options the string. This may change, depending on how I like how this
119             # is handled...
120             sub get_option {
121 8     8 1 14 my $self = shift;
122 8   33     26 my $option_name = shift || croak 'get_option requires an option name';
123              
124 5 100       37 my @values =
125 12         189 map { $_->{value} || $option_name }
126 8         12 grep { $_->{name} eq $option_name } @{ $self->{_options} };
  8         30  
127              
128 8 100       54 return wantarray ? @values : defined $values[0] ? $values[0] : '';
    100          
129             }
130              
131             sub get_options {
132 4     4 1 1103 map { $_->{name} } @{ shift->{_options} };
  6         22  
  4         17  
133             }
134              
135             # Sets an option. To enable a boolean option, only supply the option
136             # name, and pass no value data.
137             sub set_option {
138 6     6 1 12 my $self = shift;
139 6   33     22 my $option_name = shift || croak 'set_option requires an option name';
140 6         10 my $option_value = shift;
141              
142 6         9 my $updated = 0;
143 6         9 my $record_count = @{ $self->{_options} };
  6         13  
144              
145 6         11 for my $options_ref ( @{ $self->{_options} } ) {
  6         17  
146 8 100       30 if ( $options_ref->{name} eq $option_name ) {
147 2 50       7 $options_ref->{value} = $option_value if defined $option_value;
148 2         4 ++$updated;
149             }
150             }
151 6 100       22 if ( $updated == 0 ) {
    50          
152 5 100       6 push @{ $self->{_options} },
  5         33  
153             {
154             name => $option_name,
155             ( defined $option_value ? ( value => $option_value ) : () )
156             };
157             } elsif ( $updated > 1 ) {
158             # KLUGE edge-case where duplicate entries exist for this option. Clear
159             # all duplicates beyond the first entry.
160 1         2 my $seen = 0;
161 1 100 66     5 @{ $self->{_options} } = grep {
  3         23  
162 1         2 $_->{name} ne $option_name
163             or $_->{name} eq $option_name
164             && !$seen++
165 1         2 } @{ $self->{_options} };
166             }
167              
168 6         28 return $record_count - @{ $self->{_options} };
  6         22  
169             }
170              
171             sub unset_option {
172 4     4 1 6 my $self = shift;
173 4   33     12 my $option_name = shift || croak 'unset_option requires an option name';
174              
175 4         6 my $record_count = @{ $self->{_options} };
  4         9  
176 4         15 @{ $self->{_options} } =
  9         22  
177 4         7 grep { $_->{name} ne $option_name } @{ $self->{_options} };
  4         9  
178              
179 4         5 return $record_count - @{ $self->{_options} };
  4         13  
180             }
181              
182             sub unset_options {
183 2     2 1 6 shift->{_options} = [];
184 2         9 return 1;
185             }
186              
187             1;
188              
189             __END__