1#+##############################################################################
2#                                                                              #
3# File: No/Worries/DN.pm                                                       #
4#                                                                              #
5# Description: Distinguished Names handling without worries                    #
6#                                                                              #
7#-##############################################################################
8
9#
10# module definition
11#
12
13package No::Worries::DN;
14use strict;
15use warnings;
16our $VERSION  = "1.6";
17our $REVISION = sprintf("%d.%02d", q$Revision: 1.6 $ =~ /(\d+)\.(\d+)/);
18
19#
20# used modules
21#
22
23use No::Worries::Die qw(dief);
24use No::Worries::Export qw(export_control);
25use Params::Validate qw(validate_pos :types);
26
27#
28# constants
29#
30
31use constant FORMAT_RFC2253 => "rfc2253";
32use constant FORMAT_JAVA    => "java";
33use constant FORMAT_OPENSSL => "openssl";
34
35#
36# global variables
37#
38
39our(
40    %_Map,        # map of known attribute types
41    $_TypeRE,     # regexp for a valid attribute type
42    $_ValueRE,    # regexp for a valid attribute value
43);
44
45foreach my $type (qw(Email emailAddress EMAILADDRESS)) {
46    $_Map{$type} = $type;
47}
48
49foreach my $type (qw(C CN DC L O OU ST)) {
50    $_Map{$type} = $_Map{lc($type)} = $type;
51}
52
53$_TypeRE = join("|", keys(%_Map));
54
55$_ValueRE = "[" .
56    "0-9a-zA-Z" .   # alphanumerical
57    "\\x20" .       # space
58    "\\x27" .       # quote
59    "\\x28" .       # left parenthesis
60    "\\x29" .       # right parenthesis
61    "\\x2d" .       # dash
62    "\\x2e" .       # dot
63    "\\x2f" .       # slash
64    "\\x3a" .       # colon
65    "\\x40" .       # at sign
66    "\\x5f" .       # underscore
67    "\\xa0-\\xff" . # some high-bit characters that may come from ISO-8859-1
68"]+";
69
70#
71# parse a string containing a DN and return an array reference
72#
73
74sub dn_parse ($) {
75    my($string) = @_;
76    my($sep, @list, @dn);
77
78    validate_pos(@_, { type => SCALAR });
79    if ($string =~ m/^(\/[a-z]+=[^=]*){3,}$/i) {
80        $sep = "/";
81    } elsif ($string =~ m/^[a-z]+=[^=]*(,[a-z]+=[^=]*){2,}$/i) {
82        $sep = ",";
83    } elsif ($string =~ m/^[a-z]+=[^=]*(, [a-z]+=[^=]*){2,}$/i) {
84        $sep = ", ";
85    } else {
86        dief("unexpected DN: %s", $string);
87    }
88    @list = split(/$sep/, $string);
89    shift(@list) if $sep eq "/";
90    @dn = ();
91    foreach my $attr (@list) {
92        if ($attr =~ /^($_TypeRE)=($_ValueRE)$/) {
93            # type=value
94            push(@dn, "$_Map{$1}=$2");
95        } elsif (@dn and $attr =~ /^($_ValueRE)$/) {
96            # value only, assumed to come from previous attribute
97            $dn[-1] .= $sep . $attr;
98        } else {
99            dief("invalid DN: %s", $string);
100        }
101    }
102    @dn = reverse(@dn) if $sep eq "/";
103    return(\@dn);
104}
105
106#
107# convert the given parsed DN into a string
108#
109
110sub dn_string ($$) {
111    my($dn, $format) = @_;
112
113    validate_pos(@_, { type => ARRAYREF }, { type => SCALAR });
114    return(join(",", @{ $dn })) if $format eq FORMAT_RFC2253;
115    return(join(", ", @{ $dn })) if $format eq FORMAT_JAVA;
116    return(join("/", "", reverse(@{ $dn }))) if $format eq FORMAT_OPENSSL;
117    dief("unsupported DN format: %s", $format);
118}
119
120#
121# export control
122#
123
124sub import : method {
125    my($pkg, %exported);
126
127    $pkg = shift(@_);
128    grep($exported{$_}++, map("dn_$_", qw(parse string)));
129    export_control(scalar(caller()), $pkg, \%exported, @_);
130}
131
1321;
133
134__DATA__
135
136=head1 NAME
137
138No::Worries::DN - Distinguished Names handling without worries
139
140=head1 SYNOPSIS
141
142  use No::Worries::DN qw(dn_parse dn_string);
143
144  $dn = dn_parse("/C=US/O=Acme Corporation/CN=John Doe");
145  $string = dn_string($dn, No::Worries::DN::FORMAT_JAVA);
146
147=head1 DESCRIPTION
148
149This module eases Distinguished Names (DNs) handling by providing
150convenient functions to parse and convert DNs from and to different
151formats. All the functions die() on error.
152
153=head1 FUNCTIONS
154
155This module provides the following functions (none of them being
156exported by default):
157
158=over
159
160=item dn_parse(STRING)
161
162parse a string containing DN information and return an array reference
163
164=item dn_string(DN, FORMAT)
165
166convert the given parsed DN (an array reference) into a string of the
167given format, this is somehow the opposite of dn_parse()
168
169=back
170
171=head1 FORMATS
172
173Here are the supported formats:
174
175=over
176
177=item No::Worries::DN::FORMAT_RFC2253
178
179this is the format defined by RFC 2253, for instance:
180C<CN=John Doe,O=Acme Corporation,C=US>
181
182=item No::Worries::DN::FORMAT_JAVA
183
184this is a variant of RFC 2253, with extra spaces, for instance:
185C<CN=John Doe, O=Acme Corporation, C=US>
186
187=item No::Worries::DN::FORMAT_OPENSSL
188
189this is the default format used by OpenSSL, for instance:
190C</C=US/O=Acme Corporation/CN=John Doe>
191
192=back
193
194=head1 SEE ALSO
195
196L<No::Worries>.
197
198=head1 AUTHOR
199
200Lionel Cons L<http://cern.ch/lionel.cons>
201
202Copyright (C) CERN 2012-2019
203