X500::DN::Marpa - Parse X.500 DNs
X500::DN::Marpa
#!/usr/bin/env perl use strict; use warnings; use X500::DN::Marpa ':constants'; # ----------- my(%count) = (fail => 0, success => 0, total => 0); my($parser) = X500::DN::Marpa -> new ( options => long_descriptors, ); my(@text) = ( q||, q|1.4.9=2001|, q|cn=Nemo,c=US|, q|cn=Nemo, c=US|, q|cn = Nemo, c = US|, q|cn=John Doe, o=Acme, c=US|, q|cn=John Doe, o=Acme\\, Inc., c=US|, q|x= |, q|x=\\ |, q|x = \\ |, q|x=\\ \\ |, q|x=\\#\"\\41|, q|x=#616263|, q|SN=Lu\C4\8Di\C4\87|, # 'Lučić'. q|foo=FOO + bar=BAR + frob=FROB, baz=BAZ|, q|UID=jsmith,DC=example,DC=net|, q|OU=Sales+CN=J. Smith,DC=example,DC=net|, q|CN=James \"Jim\" Smith\, III,DC=example,DC=net|, q|CN=Before\0dAfter,DC=example,DC=net|, q|1.3.6.1.4.1.1466.0=#04024869|, q|UID=nobody@example.com,DC=example,DC=com|, q|CN=John Smith,OU=Sales,O=ACME Limited,L=Moab,ST=Utah,C=US|, ); my($result); for my $text (@text) { $count{total}++; print "# $count{total}. Parsing |$text|. \n"; $result = $parser -> parse($text); print "Parse result: $result (0 is success)\n"; if ($result == 0) { $count{success}++; for my $item ($parser -> stack -> print) { print "$$item{type} = $$item{value}. count = $$item{count}. \n"; } print 'DN: ', $parser -> dn, ". \n"; print 'OpenSSL DN: ', $parser -> openssl_dn, ". \n"; } print '-' x 50, "\n"; } $count{fail} = $count{total} - $count{success}; print "\n"; print 'Statistics: ', join(', ', map{"$_ => $count{$_}"} sort keys %count), ". \n";
See scripts/synopsis.pl.
This is part of the printout of synopsis.pl:
# 3. Parsing |cn=Nemo,c=US|. Parse result: 0 (0 is success) commonName = Nemo. count = 1. countryName = US. count = 1. DN: countryName=US,commonName=Nemo. OpenSSL DN: commonName=Nemo+countryName=US. -------------------------------------------------- ... -------------------------------------------------- # 13. Parsing |x=#616263|. Parse result: 0 (0 is success) x = #616263. count = 1. DN: x=#616263. OpenSSL DN: x=#616263. -------------------------------------------------- ... -------------------------------------------------- # 15. Parsing |foo=FOO + bar=BAR + frob=FROB, baz=BAZ|. Parse result: 0 (0 is success) foo = FOO+bar=BAR+frob=FROB. count = 3. baz = BAZ. count = 1. DN: baz=BAZ,foo=FOO+bar=BAR+frob=FROB. OpenSSL DN: foo=FOO+bar=BAR+frob=FROB+baz=BAZ.
If you set the option return_hex_as_chars, as discussed in the "FAQ", then case 13 will print:
return_hex_as_chars
# 13. Parsing |x=#616263|. Parse result: 0 (0 is success) x = abc. count = 1. DN: x=abc. OpenSSL DN: x=abc.
X500::DN::Marpa provides a Marpa::R2-based parser for parsing X.500 Distinguished Names.
It is based on RFC4514: Lightweight Directory Access Protocol (LDAP): String Representation of Distinguished Names.
This module is available as a Unix-style distro (*.tgz).
See http://savage.net.au/Perl-modules/html/installing-a-module.html for help on unpacking and installing distros.
Install X500::DN::Marpa as you would any Perl module:
Perl
Run:
cpanm X500::DN::Marpa
or run:
sudo cpan X500::DN::Marpa
or unpack the distro, and then either:
perl Build.PL ./Build ./Build test sudo ./Build install
or:
perl Makefile.PL make (or dmake or nmake) make test make install
new() is called as my($parser) = X500::DN::Marpa -> new(k1 => v1, k2 => v2, ...).
new()
my($parser) = X500::DN::Marpa -> new(k1 => v1, k2 => v2, ...)
It returns a new object of type X500::DN::Marpa.
Key-value pairs accepted in the parameter list (see corresponding methods for details [e.g. "options([$bit_string])"]):
This allows you to turn on various options.
Default: 0 (nothing is fatal).
See the "FAQ" for details.
Default: ''.
Returns a string containing the grammar used by this module.
Returns the RDNs, separated by commas, as a single string in the reverse order compared with the order of the RNDs in the input text.
The order reversal is discussed in section 2.1 of RFC4514.
Hence 'cn=Nemo, c=US' is returned as 'countryName=US,commonName=Nemo' (when the long_descriptors option is used), and as 'c=US,cn=Nemo' by default.
long_descriptors
See also "openssl_dn()".
Returns the last error or warning message set.
Error messages always start with 'Error: '. Messages never end with "\n".
Parsing error strings is not a good idea, ever though this module's format for them is fixed.
See "error_number()".
Returns the last error or warning number set.
Warnings have values < 0, and errors have values > 0.
If the value is > 0, the message has the prefix 'Error: ', and if the value is < 0, it has the prefix 'Warning: '. If this is not the case, it's a reportable bug.
Possible values for error_number() and error_message():
This is the default value.
If "error_number()" returns 1, it's an error, and if it returns -1 it's a warning.
You can set the option exhaustion_is_fatal to make it fatal.
exhaustion_is_fatal
This message is only produced when the parse is ambiguous.
If "error_number()" returns 2, it's an error, and if it returns -2 it's a warning.
You can set the option ambiguity_is_fatal to make it fatal.
ambiguity_is_fatal
See "error_message()".
See "Constructor and Initialization" for details on the parameters accepted by "new()".
Returns the RDNs, separated by pluses, as a single string in the same order compared with the order of the RNDs in the input text.
Hence 'cn=Nemo, c=US' is returned as 'commonName=Nemo+countryName=US' (when the long_descriptors option is used), and as 'cn=Nemo+c=US' by default.
See also "dn()".
Here, the [] indicate an optional parameter.
Get or set the option flags.
For typical usage, see scripts/synopsis.pl.
'options' is a parameter to "new()". See "Constructor and Initialization" for details.
This is the only method the user needs to call. All data can be supplied when calling "new()".
You can of course call other methods (e.g. "text([$string])" ) after calling "new()" but before calling parse().
parse()
Note: If a string is passed to parse(), it takes precedence over any string passed to new(text => $string), and over any string passed to "text([$string])". Further, the string passed to parse() is passed to "text([$string)", meaning any subsequent call to text() returns the string passed to parse().
new(text => $string)
text()
Returns 0 for success and 1 for failure.
If the value is 1, you should call "error_number()" to find out what happened.
Returns a string containing the $n-th RDN, or returns '' if $n is out of range.
$n counts from 1.
If the input is 'UID=nobody@example.com,DC=example,DC=com', rdn(1) returns 'uid=nobody@example.com'. Note the lower-case 'uid'.
rdn(1)
See t/dn.t.
Returns a string containing the $n-th RDN's count (multivalue indicator), or returns 0 if $n is out of range.
If the input is 'UID=nobody@example.com,DC=example,DC=com', rdn_count(1) returns 1.
rdn_count(1)
If the input is 'foo=FOO+bar=BAR+frob=FROB, baz=BAZ', rdn_count(1) returns 3.
Not to be confused with "rdn_number()".
Returns the number of RDNs, which may be 0.
If the input is 'UID=nobody@example.com,DC=example,DC=com', rdn_number() returns 3.
rdn_number()
Not to be confused with "rdn_count($n)".
Returns a string containing the $n-th RDN's attribute type, or returns '' if $n is out of range.
If the input is 'UID=nobody@example.com,DC=example,DC=com', rdn_type(1) returns 'uid'.
rdn_type(1)
Returns an array containing all the types of all the RDNs for the given RDN, or returns () if $n is out of range.
If the DN is 'foo=FOO+bar=BAR+frob=FROB, baz=BAZ', rdn_types(1) returns ('foo', 'bar', frob').
rdn_types(1)
Returns a string containing the $n-th RDN's attribute value, or returns '' if $n is out of range.
If the input is 'UID=nobody@example.com,DC=example,DC=com', rdn_type(1) returns 'nobody@example.com'.
Returns an array containing the RDN attribute values for the attribute type $type, or ().
If the input is 'UID=nobody@example.com,DC=example,DC=com', rdn_values('DC') returns ('example', 'com').
rdn_values('DC')
Returns an object of type Set::Array, which holds the parsed data.
Obviously, it only makes sense to call stack() after calling "parse([$string])".
stack()
The structure of elements in this stack is documented in the "FAQ".
See scripts/tiny.pl for sample code.
Get or set a string to be parsed.
'text' is a parameter to "new()". See "Constructor and Initialization" for details.
See "error_message()" and "error_number()".
See also "What are the possible values for the 'options' parameter to new()?" below.
The module outputs a stack, which is an object of type Set::Array. See "stack()".
Elements in this stack are in the same order as the RDNs are in the input string.
The "dn()" method returns the RDNs, separated by commas, as a single string in the reverse order, whereas "openssl_dn()" separates them by pluses and uses the original order.
Each element of this stack is a hashref, with these (key => value) pairs:
The number of attribute types and values in a (possibly multivalued) RDN.
$number counts from 1.
The attribute type.
The attribute value.
Sample DNs:
Note: These examples assume the default case of the option long_descriptors (discussed below) not being used.
If the input is 'UID=nobody@example.com,DC=example,DC=com', the stack will contain:
If the input is 'foo=FOO+bar=BAR+frob=FROB, baz=BAZ', the stack will contain:
Sample Code:
A typical script uses code like this (copied from scripts/tiny.pl):
$result = $parser -> parse($text); print "Parse result: $result (0 is success)\n"; if ($result == 0) { for my $item ($parser -> stack -> print) { print "$$item{type} = $$item{value}. count = $$item{count}. \n"; } }
If the option long_descriptors is not used in the call to "new()", then $$item{type} defaults to lower-case. RFC4512 says 'Short names are case insensitive....'. I've chosen to use lower-case as the canonical form output by my code.
If that option is used, then some types are output in mixed case. The list of such types is given in section 3 (at the top of page 6) in RFC4514. This document is one of those listed in "References", below.
For a discussion of the mixed-case descriptors, see "What are the possible values for the 'options' parameter to new()?" below.
An extended list of such long descriptors is given in section 4 (page 25) in RFC4519. Note that 'streetAddress' is missing from this list.
Firstly, to make these constants available, you must say:
use X500::DN::Marpa ':constants';
Secondly, more detail on errors and warnings can be found at "error_number()".
Thirdly, for usage of these option flags, see scripts/synopsis.pl and scripts/tiny.pl.
Now the flags themselves:
This is the default.
nothing_is_fatal has the value of 0.
nothing_is_fatal
Print error messages if this flag is set.
print_errors has the value of 1.
print_errors
Print various warnings if this flag is set:
Ambiguity is not, in and of itself, an error. But see the ambiguity_is_fatal option, below.
It's tempting to call this option warnings, but Perl already has use warnings, so I didn't.
warnings
use warnings
print_warnings has the value of 2.
print_warnings
Print extra stuff if this flag is set.
print_debugs has the value of 4.
print_debugs
This makes "error_number()" return 2 rather than -2.
ambiguity_is_fatal has the value of 8.
This makes "error_number()" return 1 rather than -1.
exhaustion_is_fatal has the value of 16.
This makes the type key in the output stack's elements contain long descriptor names rather than abbreviations.
type
For example, if the input was 'cn=Nemo,c=US', the output stack would contain, by default, i.e. without setting this option:
However, if this option is set, the output will contain:
long_descriptors has the value of 32.
This triggers extra processing of attribute values which start with '#':
So, if this option is not used, 'x=#616263' is parsed as {type => 'x', value => '#616263'}, but if the option is used, you get {type => 'x', value => 'abc'}.
return_hex_as_chars has the value of 64.
Handling of UTF8 is discussed in one of the RFCs listed in "References", below.
http://savage.net.au/Marpa.html.
That page has a long list of links.
This runs both standard and author tests:
shell> perl Build.PL; ./Build; ./Build authortest
I found RFCs 4514 and 4512 to be the most directly relevant ones.
RFC Index: The Index. Just search for 'LDAP'.
RFC4514: Lightweight Directory Access Protocol (LDAP): String Representation of Distinguished Names.
RFC4512: Lightweight Directory Access Protocol (LDAP): Directory Information Models.
RFC4517: Lightweight Directory Access Protocol (LDAP): Syntaxes and Matching Rules.
RFC5234: Augmented BNF for Syntax Specifications: ABNF.
RFC3629: UTF-8, a transformation format of ISO 10646.
RFC4514 also discusses UTF8. Search it using the string 'UTF-8'.
X500::DN. Note: This module is based on the obsolete RFC2253.
The file Changes was converted into Changelog.ini by Module::Metadata::Changes.
Version numbers < 1.00 represent development versions. From 1.00 up, they are production versions.
https://github.com/ronsavage/X500-DN-Marpa
Email the author, or log a bug on RT:
https://rt.cpan.org/Public/Dist/Display.html?Name=X500::DN::Marpa.
X500::DN::Marpa was written by Ron Savage <ron@savage.net.au> in 2015.
Marpa's homepage: http://savage.net.au/Marpa.html.
My homepage: http://savage.net.au/.
Australian copyright (c) 2015, Ron Savage.
All Programs of mine are 'OSI Certified Open Source Software'; you can redistribute them and/or modify them under the terms of The Artistic License 2.0, a copy of which is available at: http://opensource.org/licenses/alphabetical.
To install X500::DN::Marpa, copy and paste the appropriate command in to your terminal.
cpanm
CPAN shell
perl -MCPAN -e shell install X500::DN::Marpa
For more information on module installation, please visit the detailed CPAN module installation guide.