File Coverage

blib/lib/Config/OpenSSH/Authkey/Entry/Options.pm
Criterion Covered Total %
statement 89 89 100.0
branch 26 28 92.8
condition 6 14 42.8
subroutine 13 13 100.0
pod 9 9 100.0
total 143 153 93.4


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   68646 use 5.006000;
  3         19  
10 3     3   16 use strict;
  3         6  
  3         79  
11 3     3   16 use warnings;
  3         6  
  3         82  
12              
13 3     3   14 use Carp qw(croak);
  3         6  
  3         2993  
14              
15             our $VERSION = '1.06';
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 2203 my $class = shift;
27 4         26 my $option_string = shift;
28 4         30 my $self = { _options => [] };
29              
30 4 100       20 if ( defined $option_string ) {
31             $self->{_options} =
32 2         9 Config::OpenSSH::Authkey::Entry::Options->split_options($option_string);
33             }
34              
35 4         10 bless $self, $class;
36 4         13 return $self;
37             }
38              
39             sub split_options {
40 7     7 1 1341 my $class = shift;
41 7         11 my $option_string = shift;
42 7         16 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       13 if ($option_string =~ m/ \G ($AUTHKEY_OPTION_NAME_RE)="( (?: \\"|[^"] )*? )"
  24         332  
66             (?:,|[ \t]+)? /cgx
67             ) {
68 7         32 my $option_name = $1;
69 7   50     26 my $option_value = $2 || q{};
70              
71 7         39 push @options, { name => $option_name, value => $option_value };
72              
73 7         18 redo OPTION_LEXER;
74             }
75              
76             # Boolean options - mere presence enables them in OpenSSH
77 17 100       137 if ( $option_string =~ m/ \G ($AUTHKEY_OPTION_NAME_RE) (?:,|[ \t]+)? /cgx ) {
78 10         29 my $option_name = $1;
79              
80 10         28 push @options, { name => $option_name };
81              
82 10         26 redo OPTION_LEXER;
83             }
84             }
85              
86 7 100       40 return wantarray ? @options : \@options;
87             }
88              
89             ######################################################################
90             #
91             # Instance methods
92              
93             sub parse {
94 2     2 1 5 my $self = shift;
95 2         4 my $option_string = shift;
96              
97             $self->{_options} =
98 2         7 Config::OpenSSH::Authkey::Entry::Options->split_options($option_string);
99 2         4 return scalar @{ $self->{_options} };
  2         7  
100             }
101              
102             sub as_string {
103 10     10 1 556 my $self = shift;
104 10         17 my @options;
105 10         18 for my $options_ref ( @{ $self->{_options} } ) {
  10         25  
106 18 100       42 if ( exists $options_ref->{value} ) {
107 9         32 ( my $value = $options_ref->{value} ) =~ s/(?
108 9         31 push @options, $options_ref->{name} . '="' . $value . '"';
109             } else {
110 9         23 push @options, $options_ref->{name};
111             }
112             }
113 10         53 return join( q{,}, @options );
114             }
115              
116             # NOTE - boolean return the name of the option, while string value
117             # options the string. This may change, depending on how I like how this
118             # is handled...
119             sub get_option {
120 8     8 1 18 my $self = shift;
121 8   33     23 my $option_name = shift || croak 'get_option requires an option name';
122              
123             my @values =
124 5 100       30 map { $_->{value} || $option_name }
125 8         14 grep { $_->{name} eq $option_name } @{ $self->{_options} };
  12         33  
  8         32  
126              
127 8 100       45 return wantarray ? @values : defined $values[0] ? $values[0] : '';
    100          
128             }
129              
130             sub get_options {
131 4     4 1 621 map { $_->{name} } @{ shift->{_options} };
  6         25  
  4         38  
132             }
133              
134             # Sets an option. To enable a boolean option, only supply the option
135             # name, and pass no value data.
136             sub set_option {
137 6     6 1 11 my $self = shift;
138 6   33     21 my $option_name = shift || croak 'set_option requires an option name';
139 6         12 my $option_value = shift;
140              
141 6         10 my $updated = 0;
142 6         11 my $record_count = @{ $self->{_options} };
  6         14  
143              
144 6         9 for my $options_ref ( @{ $self->{_options} } ) {
  6         21  
145 8 100       24 if ( $options_ref->{name} eq $option_name ) {
146 2 50       7 $options_ref->{value} = $option_value if defined $option_value;
147 2         4 ++$updated;
148             }
149             }
150 6 100       21 if ( $updated == 0 ) {
    50          
151 5 100       10 push @{ $self->{_options} },
  5         21  
152             { name => $option_name,
153             ( defined $option_value ? ( value => $option_value ) : () )
154             };
155             } elsif ( $updated > 1 ) {
156             # KLUGE edge-case where duplicate entries exist for this option. Clear
157             # all duplicates beyond the first entry.
158 1         3 my $seen = 0;
159 1         4 @{ $self->{_options} } =
160 3 100 66     17 grep { $_->{name} ne $option_name or $_->{name} eq $option_name && !$seen++ }
161 1         2 @{ $self->{_options} };
  1         4  
162             }
163              
164 6         13 return $record_count - @{ $self->{_options} };
  6         16  
165             }
166              
167             sub unset_option {
168 4     4 1 9 my $self = shift;
169 4   33     12 my $option_name = shift || croak 'unset_option requires an option name';
170              
171 4         8 my $record_count = @{ $self->{_options} };
  4         10  
172 4         13 @{ $self->{_options} } =
173 4         10 grep { $_->{name} ne $option_name } @{ $self->{_options} };
  9         24  
  4         8  
174              
175 4         7 return $record_count - @{ $self->{_options} };
  4         13  
176             }
177              
178             sub unset_options {
179 2     2 1 8 shift->{_options} = [];
180 2         5 return 1;
181             }
182              
183             1;
184              
185             __END__