|
| 1 | +# LDAP DN support classes |
| 2 | +# |
| 3 | + |
| 4 | +## |
| 5 | +# Objects of this class represent an LDAP DN. |
| 6 | +# |
| 7 | +# In LDAP-land, a DN ("Distinguished Name") is a unique identifier for an |
| 8 | +# entry within an LDAP directory. It is made up of a number of other |
| 9 | +# attributes strung together, to identify the entry in the tree. |
| 10 | +# |
| 11 | +# Each attribute that makes up a DN needs to have its value escaped so that |
| 12 | +# the DN is valid. This class helps take care of that. |
| 13 | +# |
| 14 | +# A fully escaped DN needs to be unescaped when analysing its contents. This |
| 15 | +# class also helps take care of that. |
| 16 | +class Net::LDAP::DN |
| 17 | + ## |
| 18 | + # Initialize a DN, escaping as required. Pass in attributes in name/value |
| 19 | + # pairs. If there is a left over argument, it will be appended to the dn |
| 20 | + # without escaping (useful for a base string). |
| 21 | + # |
| 22 | + # Most uses of this class will be to escape a DN, rather than to parse it, |
| 23 | + # so storing the dn as an escaped String and parsing parts as required with |
| 24 | + # a state machine seems sensible. |
| 25 | + def initialize(*args) |
| 26 | + buffer = StringIO.new |
| 27 | + |
| 28 | + args.each_index do |index| |
| 29 | + buffer << "=" if index % 2 == 1 |
| 30 | + buffer << "," if index % 2 == 0 && index != 0 |
| 31 | + |
| 32 | + if index < args.length - 1 || index % 2 == 1 |
| 33 | + buffer << Net::LDAP::DN.escape(args[index]) |
| 34 | + else |
| 35 | + buffer << args[index] |
| 36 | + end |
| 37 | + end |
| 38 | + |
| 39 | + @dn = buffer.string |
| 40 | + end |
| 41 | + |
| 42 | + ## |
| 43 | + # Parse a DN into key value pairs using ASN from |
| 44 | + # http://tools.ietf.org/html/rfc2253 section 3. |
| 45 | + # |
| 46 | + def each_pair |
| 47 | + state = :key |
| 48 | + key = StringIO.new |
| 49 | + value = StringIO.new |
| 50 | + hex_buffer = "" |
| 51 | + |
| 52 | + @dn.each_char do |char| |
| 53 | + case state |
| 54 | + |
| 55 | + when :key then case char |
| 56 | + when 'a'..'z','A'..'Z' then |
| 57 | + state = :key_normal |
| 58 | + key << char |
| 59 | + when '0'..'9' then |
| 60 | + state = :key_oid |
| 61 | + key << char |
| 62 | + when ' ' then state = :key |
| 63 | + else raise "DN badly formed" |
| 64 | + end |
| 65 | + when :key_normal then case char |
| 66 | + when '=' then state = :value |
| 67 | + when 'a'..'z','A'..'Z','0'..'9','-',' ' then key << char |
| 68 | + else raise "DN badly formed" |
| 69 | + end |
| 70 | + when :key_oid then case char |
| 71 | + when '=' then state = :value |
| 72 | + when '0'..'9','.',' ' then key << char |
| 73 | + else raise "DN badly formed" |
| 74 | + end |
| 75 | + |
| 76 | + when :value then case char |
| 77 | + when '\\' then state = :value_normal_escape |
| 78 | + when '"' then state = :value_quoted |
| 79 | + when ' ' then state = :value |
| 80 | + when '#' then |
| 81 | + state = :value_hexstring |
| 82 | + value << char |
| 83 | + when ',' then |
| 84 | + state = :key |
| 85 | + yield key.string.strip, value.string.rstrip |
| 86 | + key = StringIO.new |
| 87 | + value = StringIO.new; |
| 88 | + else |
| 89 | + state = :value_normal |
| 90 | + value << char |
| 91 | + end |
| 92 | + |
| 93 | + when :value_normal then case char |
| 94 | + when '\\' then state = :value_normal_escape |
| 95 | + when ',' then |
| 96 | + state = :key |
| 97 | + yield key.string.strip, value.string.rstrip |
| 98 | + key = StringIO.new |
| 99 | + value = StringIO.new; |
| 100 | + else value << char |
| 101 | + end |
| 102 | + when :value_normal_escape then case char |
| 103 | + when '0'..'9', 'a'..'f', 'A'..'F' then |
| 104 | + state = :value_normal_escape_hex |
| 105 | + hex_buffer = char |
| 106 | + else state = :value_normal; value << char |
| 107 | + end |
| 108 | + when :value_normal_escape_hex then case char |
| 109 | + when '0'..'9', 'a'..'f', 'A'..'F' then |
| 110 | + state = :value_normal |
| 111 | + value << "#{hex_buffer}#{char}".to_i(16).chr |
| 112 | + else raise "DN badly formed" |
| 113 | + end |
| 114 | + |
| 115 | + when :value_quoted then case char |
| 116 | + when '\\' then state = :value_quoted_escape |
| 117 | + when '"' then state = :value_end |
| 118 | + else value << char |
| 119 | + end |
| 120 | + when :value_quoted_escape then case char |
| 121 | + when '0'..'9', 'a'..'f', 'A'..'F' then |
| 122 | + state = :value_quoted_escape_hex |
| 123 | + hex_buffer = char |
| 124 | + else state = :value_quoted; value << char |
| 125 | + end |
| 126 | + when :value_quoted_escape_hex then case char |
| 127 | + when '0'..'9', 'a'..'f', 'A'..'F' then |
| 128 | + state = :value_quoted |
| 129 | + value << "#{hex_buffer}#{char}".to_i(16).chr |
| 130 | + else raise "DN badly formed" |
| 131 | + end |
| 132 | + |
| 133 | + when :value_hexstring then case char |
| 134 | + when '0'..'9', 'a'..'f', 'A'..'F' then |
| 135 | + state = :value_hexstring_hex |
| 136 | + value << char |
| 137 | + when ' ' then state = :value_end |
| 138 | + when ',' then |
| 139 | + state = :key |
| 140 | + yield key.string.strip, value.string.rstrip |
| 141 | + key = StringIO.new |
| 142 | + value = StringIO.new; |
| 143 | + else raise "DN badly formed" |
| 144 | + end |
| 145 | + when :value_hexstring_hex then case char |
| 146 | + when '0'..'9', 'a'..'f', 'A'..'F' then |
| 147 | + state = :value_hexstring |
| 148 | + value << char |
| 149 | + else raise "DN badly formed" |
| 150 | + end |
| 151 | + |
| 152 | + when :value_end then case char |
| 153 | + when ' ' then state = :value_end |
| 154 | + when ',' then |
| 155 | + state = :key |
| 156 | + yield key.string.strip, value.string.rstrip |
| 157 | + key = StringIO.new |
| 158 | + value = StringIO.new; |
| 159 | + else raise "DN badly formed" |
| 160 | + end |
| 161 | + |
| 162 | + else raise "Fell out of state machine" |
| 163 | + end |
| 164 | + end |
| 165 | + |
| 166 | + # Last pair |
| 167 | + if [:value, :value_normal, :value_hexstring, :value_end].include? state |
| 168 | + yield key.string.strip, value.string.rstrip |
| 169 | + else |
| 170 | + raise "DN badly formed" |
| 171 | + end |
| 172 | + end |
| 173 | + |
| 174 | + ## |
| 175 | + # Returns the DN as an array in the form expected by the constructor. |
| 176 | + def to_a |
| 177 | + a = [] |
| 178 | + self.each_pair { |key, value| a << key << value } |
| 179 | + a |
| 180 | + end |
| 181 | + |
| 182 | + ## |
| 183 | + # Return the DN as an escaped string. |
| 184 | + def to_s |
| 185 | + @dn |
| 186 | + end |
| 187 | + |
| 188 | + # http://tools.ietf.org/html/rfc2253 section 2.4 lists these exceptions |
| 189 | + # for dn values. All of the following must be escaped in any normal |
| 190 | + # string using a single backslash ('\') as escape. |
| 191 | + # |
| 192 | + ESCAPES = { |
| 193 | + ',' => ',', |
| 194 | + '+' => '+', |
| 195 | + '"' => '"', |
| 196 | + '\\' => '\\', |
| 197 | + '<' => '<', |
| 198 | + '>' => '>', |
| 199 | + ';' => ';', |
| 200 | + } |
| 201 | + # Compiled character class regexp using the keys from the above hash, and |
| 202 | + # checking for a space or # at the start, or space at the end, of the |
| 203 | + # string. |
| 204 | + ESCAPE_RE = Regexp.new( |
| 205 | + "(^ |^#| $|[" + |
| 206 | + ESCAPES.keys.map { |e| Regexp.escape(e) }.join + |
| 207 | + "])") |
| 208 | + |
| 209 | + ## |
| 210 | + # Escape a string for use in a DN value |
| 211 | + def self.escape(string) |
| 212 | + string.gsub(ESCAPE_RE) { |char| "\\" + ESCAPES[char] } |
| 213 | + end |
| 214 | + |
| 215 | + ## |
| 216 | + # Proxy all other requests to the string object, because a DN is mainly |
| 217 | + # used within the library as a string |
| 218 | + def method_missing(method, *args, &block) |
| 219 | + @dn.send(method, *args, &block) |
| 220 | + end |
| 221 | +end |
0 commit comments