File Coverage

blib/lib/Config/OpenSSH/Authkey/Entry/Options.pm
Criterion Covered Total %
statement 90 90 100.0
branch 26 28 92.8
condition 6 14 42.8
subroutine 13 13 100.0
pod 9 9 100.0
total 144 154 93.5


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