|
| 1 | +# $Id$ |
| 2 | +# |
| 3 | +# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved. |
| 4 | +# Gmail account: garbagecat10. |
| 5 | +# |
| 6 | +# This is an LDAP server intended for unit testing of Net::LDAP. |
| 7 | +# It implements as much of the protocol as we have the stomach |
| 8 | +# to implement but serves static data. Use ldapsearch to test |
| 9 | +# this server! |
| 10 | +# |
| 11 | +# To make this easier to write, we use the Ruby/EventMachine |
| 12 | +# reactor library. |
| 13 | +# |
| 14 | + |
| 15 | +#------------------------------------------------ |
| 16 | + |
| 17 | +module LdapServer |
| 18 | + LdapServerAsnSyntaxTemplate = { |
| 19 | + :application => { |
| 20 | + :constructed => { |
| 21 | + 0 => :array, # LDAP BindRequest |
| 22 | + 3 => :array # LDAP SearchRequest |
| 23 | + }, |
| 24 | + :primitive => { |
| 25 | + 2 => :string, # ldapsearch sends this to unbind |
| 26 | + }, |
| 27 | + }, |
| 28 | + :context_specific => { |
| 29 | + :primitive => { |
| 30 | + 0 => :string, # simple auth (password) |
| 31 | + 7 => :string # present filter |
| 32 | + }, |
| 33 | + :constructed => { |
| 34 | + 3 => :array # equality filter |
| 35 | + }, |
| 36 | + }, |
| 37 | + } |
| 38 | + |
| 39 | + def post_init |
| 40 | + #$logger.info "Accepted LDAP connection" |
| 41 | + @authenticated = false |
| 42 | + end |
| 43 | + |
| 44 | + def receive_data data |
| 45 | + @data ||= ""; @data << data |
| 46 | + while pdu = @data.read_ber!(LdapServerAsnSyntax) |
| 47 | + begin |
| 48 | + handle_ldap_pdu pdu |
| 49 | + rescue |
| 50 | + #$logger.error "closing connection due to error #{$!}" |
| 51 | + close_connection |
| 52 | + end |
| 53 | + end |
| 54 | + end |
| 55 | + |
| 56 | + def handle_ldap_pdu pdu |
| 57 | + tag_id = pdu[1].ber_identifier |
| 58 | + case tag_id |
| 59 | + when 0x60 |
| 60 | + handle_bind_request pdu |
| 61 | + when 0x63 |
| 62 | + handle_search_request pdu |
| 63 | + when 0x42 |
| 64 | + # bizarre thing, it's a null object (primitive application-2) |
| 65 | + # sent by ldapsearch to request an unbind (or a kiss-off, not sure which) |
| 66 | + close_connection_after_writing |
| 67 | + else |
| 68 | + #$logger.error "received unknown packet-type #{tag_id}" |
| 69 | + close_connection_after_writing |
| 70 | + end |
| 71 | + end |
| 72 | + |
| 73 | + def handle_bind_request pdu |
| 74 | + # TODO, return a proper LDAP error instead of blowing up on version error |
| 75 | + if pdu[1][0] != 3 || pdu[1][1] == "bad_version" |
| 76 | + send_ldap_response 1, pdu[0].to_i, 2, "", "We only support version 3" |
| 77 | + elsif pdu[1][1] != "user" |
| 78 | + send_ldap_response 1, pdu[0].to_i, 48, "", "Who are you?" |
| 79 | + elsif pdu[1][2].ber_identifier != 0x80 |
| 80 | + send_ldap_response 1, pdu[0].to_i, 7, "", "Keep it simple, man" |
| 81 | + elsif pdu[1][2] != "password" |
| 82 | + send_ldap_response 1, pdu[0].to_i, 49, "", "Make my day" |
| 83 | + else |
| 84 | + @authenticated = true |
| 85 | + send_ldap_response 1, pdu[0].to_i, 0, pdu[1][1], "I'll take it" |
| 86 | + end |
| 87 | + end |
| 88 | + |
| 89 | + # -- |
| 90 | + # Search Response ::= |
| 91 | + # CHOICE { |
| 92 | + # entry [APPLICATION 4] SEQUENCE { |
| 93 | + # objectName LDAPDN, |
| 94 | + # attributes SEQUENCE OF SEQUENCE { |
| 95 | + # AttributeType, |
| 96 | + # SET OF AttributeValue |
| 97 | + # } |
| 98 | + # }, |
| 99 | + # resultCode [APPLICATION 5] LDAPResult |
| 100 | + # } |
| 101 | + def handle_search_request pdu |
| 102 | + unless @authenticated |
| 103 | + # NOTE, early exit. |
| 104 | + send_ldap_response 5, pdu[0].to_i, 50, "", "Who did you say you were?" |
| 105 | + return |
| 106 | + end |
| 107 | + |
| 108 | + treebase = pdu[1][0] |
| 109 | + if treebase != "dc=bayshorenetworks,dc=com" |
| 110 | + send_ldap_response 5, pdu[0].to_i, 32, "", "unknown treebase" |
| 111 | + return |
| 112 | + end |
| 113 | + |
| 114 | + msgid = pdu[0].to_i.to_ber |
| 115 | + |
| 116 | + # pdu[1][7] is the list of requested attributes. |
| 117 | + # If it's an empty array, that means that *all* attributes were requested. |
| 118 | + requested_attrs = if pdu[1][7].length > 0 |
| 119 | + pdu[1][7].map(&:downcase) |
| 120 | + else |
| 121 | + :all |
| 122 | + end |
| 123 | + |
| 124 | + filters = pdu[1][6] |
| 125 | + if filters.length == 0 |
| 126 | + # NOTE, early exit. |
| 127 | + send_ldap_response 5, pdu[0].to_i, 53, "", "No filter specified" |
| 128 | + end |
| 129 | + |
| 130 | + # TODO, what if this returns nil? |
| 131 | + filter = Net::LDAP::Filter.parse_ldap_filter(filters) |
| 132 | + |
| 133 | + $ldif.each do |dn, entry| |
| 134 | + if filter.match(entry) |
| 135 | + attrs = [] |
| 136 | + entry.each do |k, v| |
| 137 | + if requested_attrs == :all || requested_attrs.include?(k.downcase) |
| 138 | + attrvals = v.map(&:to_ber).to_ber_set |
| 139 | + attrs << [k.to_ber, attrvals].to_ber_sequence |
| 140 | + end |
| 141 | + end |
| 142 | + |
| 143 | + appseq = [dn.to_ber, attrs.to_ber_sequence].to_ber_appsequence(4) |
| 144 | + pkt = [msgid.to_ber, appseq].to_ber_sequence |
| 145 | + send_data pkt |
| 146 | + end |
| 147 | + end |
| 148 | + |
| 149 | + send_ldap_response 5, pdu[0].to_i, 0, "", "Was that what you wanted?" |
| 150 | + end |
| 151 | + |
| 152 | + def send_ldap_response pkt_tag, msgid, code, dn, text |
| 153 | + send_data([msgid.to_ber, [code.to_ber, dn.to_ber, text.to_ber].to_ber_appsequence(pkt_tag)].to_ber) |
| 154 | + end |
| 155 | +end |
| 156 | + |
| 157 | +#------------------------------------------------ |
| 158 | + |
| 159 | +# Rather bogus, a global method, which reads a HARDCODED filename |
| 160 | +# parses out LDIF data. It will be used to serve LDAP queries out of this server. |
| 161 | +# |
| 162 | +def load_test_data |
| 163 | + ary = (<<END).split("\n") |
| 164 | +dn: dc=bayshorenetworks,dc=com |
| 165 | +objectClass: dcObject |
| 166 | +objectClass: organization |
| 167 | +o: Bayshore Networks LLC |
| 168 | +dc: bayshorenetworks |
| 169 | +
|
| 170 | +dn: cn=Manager,dc=bayshorenetworks,dc=com |
| 171 | +objectClass: organizationalrole |
| 172 | +cn: Manager |
| 173 | +
|
| 174 | +dn: ou=people,dc=bayshorenetworks,dc=com |
| 175 | +objectClass: organizationalunit |
| 176 | +ou: people |
| 177 | +
|
| 178 | +dn: ou=privileges,dc=bayshorenetworks,dc=com |
| 179 | +objectClass: organizationalunit |
| 180 | +ou: privileges |
| 181 | +
|
| 182 | +dn: ou=roles,dc=bayshorenetworks,dc=com |
| 183 | +objectClass: organizationalunit |
| 184 | +ou: roles |
| 185 | +
|
| 186 | +dn: ou=office,dc=bayshorenetworks,dc=com |
| 187 | +objectClass: organizationalunit |
| 188 | +ou: office |
| 189 | +
|
| 190 | +dn: mail=nogoodnik@steamheat.net,ou=people,dc=bayshorenetworks,dc=com |
| 191 | +cn: Bob Fosse |
| 192 | +mail: nogoodnik@steamheat.net |
| 193 | +sn: Fosse |
| 194 | +ou: people |
| 195 | +objectClass: top |
| 196 | +objectClass: inetorgperson |
| 197 | +objectClass: authorizedperson |
| 198 | +hasAccessRole: uniqueIdentifier=engineer,ou=roles |
| 199 | +hasAccessRole: uniqueIdentifier=ldapadmin,ou=roles |
| 200 | +hasAccessRole: uniqueIdentifier=ldapsuperadmin,ou=roles |
| 201 | +hasAccessRole: uniqueIdentifier=ogilvy_elephant_user,ou=roles |
| 202 | +hasAccessRole: uniqueIdentifier=ogilvy_eagle_user,ou=roles |
| 203 | +hasAccessRole: uniqueIdentifier=greenplug_user,ou=roles |
| 204 | +hasAccessRole: uniqueIdentifier=brandplace_logging_user,ou=roles |
| 205 | +hasAccessRole: uniqueIdentifier=brandplace_report_user,ou=roles |
| 206 | +hasAccessRole: uniqueIdentifier=workorder_user,ou=roles |
| 207 | +hasAccessRole: uniqueIdentifier=bayshore_eagle_user,ou=roles |
| 208 | +hasAccessRole: uniqueIdentifier=bayshore_eagle_superuser,ou=roles |
| 209 | +hasAccessRole: uniqueIdentifier=kledaras_user,ou=roles |
| 210 | +
|
| 211 | +dn: mail=elephant@steamheat.net,ou=people,dc=bayshorenetworks,dc=com |
| 212 | +cn: Gwen Verdon |
| 213 | +mail: elephant@steamheat.net |
| 214 | +sn: Verdon |
| 215 | +ou: people |
| 216 | +objectClass: top |
| 217 | +objectClass: inetorgperson |
| 218 | +objectClass: authorizedperson |
| 219 | +hasAccessRole: uniqueIdentifier=brandplace_report_user,ou=roles |
| 220 | +hasAccessRole: uniqueIdentifier=engineer,ou=roles |
| 221 | +hasAccessRole: uniqueIdentifier=ogilvy_elephant_user,ou=roles |
| 222 | +hasAccessRole: uniqueIdentifier=ldapsuperadmin,ou=roles |
| 223 | +hasAccessRole: uniqueIdentifier=ldapadmin,ou=roles |
| 224 | +
|
| 225 | +dn: uniqueIdentifier=engineering,ou=privileges,dc=bayshorenetworks,dc=com |
| 226 | +uniqueIdentifier: engineering |
| 227 | +ou: privileges |
| 228 | +objectClass: accessPrivilege |
| 229 | +
|
| 230 | +dn: uniqueIdentifier=engineer,ou=roles,dc=bayshorenetworks,dc=com |
| 231 | +uniqueIdentifier: engineer |
| 232 | +ou: roles |
| 233 | +objectClass: accessRole |
| 234 | +hasAccessPrivilege: uniqueIdentifier=engineering,ou=privileges |
| 235 | +
|
| 236 | +dn: uniqueIdentifier=ldapadmin,ou=roles,dc=bayshorenetworks,dc=com |
| 237 | +uniqueIdentifier: ldapadmin |
| 238 | +ou: roles |
| 239 | +objectClass: accessRole |
| 240 | +
|
| 241 | +dn: uniqueIdentifier=ldapsuperadmin,ou=roles,dc=bayshorenetworks,dc=com |
| 242 | +uniqueIdentifier: ldapsuperadmin |
| 243 | +ou: roles |
| 244 | +objectClass: accessRole |
| 245 | +
|
| 246 | +dn: mail=catperson@steamheat.net,ou=people,dc=bayshorenetworks,dc=com |
| 247 | +cn: Sid Sorokin |
| 248 | +mail: catperson@steamheat.net |
| 249 | +sn: Sorokin |
| 250 | +ou: people |
| 251 | +objectClass: top |
| 252 | +objectClass: inetorgperson |
| 253 | +objectClass: authorizedperson |
| 254 | +hasAccessRole: uniqueIdentifier=engineer,ou=roles |
| 255 | +hasAccessRole: uniqueIdentifier=ogilvy_elephant_user,ou=roles |
| 256 | +hasAccessRole: uniqueIdentifier=ldapsuperadmin,ou=roles |
| 257 | +hasAccessRole: uniqueIdentifier=ogilvy_eagle_user,ou=roles |
| 258 | +hasAccessRole: uniqueIdentifier=greenplug_user,ou=roles |
| 259 | +hasAccessRole: uniqueIdentifier=workorder_user,ou=roles |
| 260 | +END |
| 261 | + hash = {} |
| 262 | + while (line = ary.shift) && line.chomp! |
| 263 | + if line =~ /^dn:[\s]*/i |
| 264 | + dn = $' |
| 265 | + hash[dn] = {} |
| 266 | + while (attr = ary.shift) && attr.chomp! && attr =~ /^([\w]+)[\s]*:[\s]*/ |
| 267 | + hash[dn][$1.downcase] ||= [] |
| 268 | + hash[dn][$1.downcase] << $' |
| 269 | + end |
| 270 | + end |
| 271 | + end |
| 272 | + hash |
| 273 | +end |
| 274 | + |
| 275 | +#------------------------------------------------ |
| 276 | + |
| 277 | +if __FILE__ == $0 |
| 278 | + require 'eventmachine' |
| 279 | + require 'logger' |
| 280 | + #$logger = Logger.new $stderr |
| 281 | + $ldif = load_test_data |
| 282 | + |
| 283 | + require 'net/ldap' |
| 284 | + LdapServerAsnSyntax = Net::BER.compile_syntax(LdapServer::LdapServerAsnSyntaxTemplate) |
| 285 | + EventMachine.run do |
| 286 | + port = (ENV['PORT'] || 3890).to_i |
| 287 | + EventMachine.start_server "127.0.0.1", port, LdapServer |
| 288 | + #EventMachine.add_periodic_timer 60, proc { $logger.info "heartbeat" } |
| 289 | + #$logger.info "started LDAP server on 127.0.0.1 port #{port}" |
| 290 | + end |
| 291 | +end |
0 commit comments