2006-04-15 04:19:55 +02:00
|
|
|
# $Id$
|
|
|
|
#
|
|
|
|
# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
|
|
|
|
# Gmail account: garbagecat10.
|
|
|
|
#
|
|
|
|
# This is an LDAP server intended for unit testing of Net::LDAP.
|
|
|
|
# It implements as much of the protocol as we have the stomach
|
|
|
|
# to implement but serves static data. Use ldapsearch to test
|
|
|
|
# this server!
|
|
|
|
#
|
|
|
|
# To make this easier to write, we use the Ruby/EventMachine
|
|
|
|
# reactor library.
|
|
|
|
#
|
|
|
|
|
|
|
|
|
|
|
|
require 'stringio'
|
|
|
|
|
|
|
|
#------------------------------------------------
|
|
|
|
|
|
|
|
module LdapServer
|
|
|
|
|
|
|
|
LdapServerAsnSyntax = {
|
|
|
|
:application => {
|
|
|
|
:constructed => {
|
|
|
|
0 => :array, # LDAP BindRequest
|
|
|
|
3 => :array # LDAP SearchRequest
|
|
|
|
},
|
|
|
|
:primitive => {
|
2006-04-17 02:35:42 +02:00
|
|
|
2 => :string, # ldapsearch sends this to unbind
|
2006-04-15 04:19:55 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
:context_specific => {
|
|
|
|
:primitive => {
|
2006-04-17 02:35:42 +02:00
|
|
|
0 => :string, # simple auth (password)
|
|
|
|
7 => :string # present filter
|
2006-04-15 04:19:55 +02:00
|
|
|
},
|
2006-04-17 19:57:33 +02:00
|
|
|
:constructed => {
|
|
|
|
3 => :array # equality filter
|
|
|
|
},
|
2006-04-15 04:19:55 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
def post_init
|
|
|
|
$logger.info "Accepted LDAP connection"
|
|
|
|
@authenticated = false
|
|
|
|
end
|
|
|
|
|
|
|
|
def receive_data data
|
|
|
|
@data ||= ""; @data << data
|
|
|
|
while pdu = @data.read_ber!(LdapServerAsnSyntax)
|
|
|
|
begin
|
|
|
|
handle_ldap_pdu pdu
|
|
|
|
rescue
|
|
|
|
$logger.error "closing connection due to error #{$!}"
|
|
|
|
close_connection
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def handle_ldap_pdu pdu
|
|
|
|
tag_id = pdu[1].ber_identifier
|
|
|
|
case tag_id
|
|
|
|
when 0x60
|
|
|
|
handle_bind_request pdu
|
|
|
|
when 0x63
|
|
|
|
handle_search_request pdu
|
|
|
|
when 0x42
|
|
|
|
# bizarre thing, it's a null object (primitive application-2)
|
|
|
|
# sent by ldapsearch to request an unbind (or a kiss-off, not sure which)
|
|
|
|
close_connection_after_writing
|
|
|
|
else
|
|
|
|
$logger.error "received unknown packet-type #{tag_id}"
|
|
|
|
close_connection_after_writing
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def handle_bind_request pdu
|
|
|
|
# TODO, return a proper LDAP error instead of blowing up on version error
|
|
|
|
if pdu[1][0] != 3
|
2006-04-15 06:43:36 +02:00
|
|
|
send_ldap_response 1, pdu[0].to_i, 2, "", "We only support version 3"
|
2006-04-15 04:19:55 +02:00
|
|
|
elsif pdu[1][1] != "cn=bigshot,dc=bayshorenetworks,dc=com"
|
2006-04-15 06:43:36 +02:00
|
|
|
send_ldap_response 1, pdu[0].to_i, 48, "", "Who are you?"
|
2006-04-15 04:19:55 +02:00
|
|
|
elsif pdu[1][2].ber_identifier != 0x80
|
2006-04-15 06:43:36 +02:00
|
|
|
send_ldap_response 1, pdu[0].to_i, 7, "", "Keep it simple, man"
|
2006-04-15 04:19:55 +02:00
|
|
|
elsif pdu[1][2] != "opensesame"
|
2006-04-15 06:43:36 +02:00
|
|
|
send_ldap_response 1, pdu[0].to_i, 49, "", "Make my day"
|
2006-04-15 04:19:55 +02:00
|
|
|
else
|
|
|
|
@authenticated = true
|
2006-04-15 06:43:36 +02:00
|
|
|
send_ldap_response 1, pdu[0].to_i, 0, pdu[1][1], "I'll take it"
|
2006-04-15 04:19:55 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2006-04-17 14:41:50 +02:00
|
|
|
|
|
|
|
|
|
|
|
#--
|
|
|
|
# Search Response ::=
|
|
|
|
# CHOICE {
|
|
|
|
# entry [APPLICATION 4] SEQUENCE {
|
|
|
|
# objectName LDAPDN,
|
|
|
|
# attributes SEQUENCE OF SEQUENCE {
|
|
|
|
# AttributeType,
|
|
|
|
# SET OF AttributeValue
|
|
|
|
# }
|
|
|
|
# },
|
|
|
|
# resultCode [APPLICATION 5] LDAPResult
|
|
|
|
# }
|
2006-04-15 04:19:55 +02:00
|
|
|
def handle_search_request pdu
|
|
|
|
unless @authenticated
|
2006-04-17 22:39:54 +02:00
|
|
|
# NOTE, early exit.
|
2006-04-15 04:19:55 +02:00
|
|
|
send_ldap_response 5, pdu[0].to_i, 50, "", "Who did you say you were?"
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
treebase = pdu[1][0]
|
2006-04-17 05:44:55 +02:00
|
|
|
if treebase != "dc=bayshorenetworks,dc=com"
|
2006-04-15 04:19:55 +02:00
|
|
|
send_ldap_response 5, pdu[0].to_i, 32, "", "unknown treebase"
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
2006-04-17 05:44:55 +02:00
|
|
|
msgid = pdu[0].to_i.to_ber
|
|
|
|
|
2006-04-17 14:41:50 +02:00
|
|
|
# pdu[1][7] is the list of requested attributes.
|
|
|
|
# If it's an empty array, that means that *all* attributes were requested.
|
|
|
|
requested_attrs = if pdu[1][7].length > 0
|
|
|
|
pdu[1][7].map {|a| a.downcase}
|
|
|
|
else
|
|
|
|
:all
|
|
|
|
end
|
|
|
|
|
2006-04-17 19:57:33 +02:00
|
|
|
filters = pdu[1][6]
|
2006-04-17 22:39:54 +02:00
|
|
|
if filters.length == 0
|
|
|
|
# NOTE, early exit.
|
|
|
|
send_ldap_response 5, pdu[0].to_i, 53, "", "No filter specified"
|
2006-04-17 19:57:33 +02:00
|
|
|
end
|
2006-04-17 14:41:50 +02:00
|
|
|
|
2006-04-17 22:39:54 +02:00
|
|
|
# TODO, what if this returns nil?
|
|
|
|
filter = Net::LDAP::Filter.parse_ldap_filter( filters )
|
2006-04-17 05:44:55 +02:00
|
|
|
|
2006-04-17 22:39:54 +02:00
|
|
|
$ldif.each {|dn, entry|
|
|
|
|
if filter.match( entry )
|
|
|
|
attrs = []
|
|
|
|
entry.each {|k, v|
|
|
|
|
if requested_attrs == :all or requested_attrs.include?(k.downcase)
|
|
|
|
attrvals = v.map {|v1| v1.to_ber}.to_ber_set
|
|
|
|
attrs << [k.to_ber, attrvals].to_ber_sequence
|
|
|
|
end
|
|
|
|
}
|
|
|
|
|
|
|
|
appseq = [dn.to_ber, attrs.to_ber_sequence].to_ber_appsequence(4)
|
|
|
|
pkt = [msgid.to_ber, appseq].to_ber_sequence
|
|
|
|
send_data pkt
|
|
|
|
end
|
2006-04-17 05:44:55 +02:00
|
|
|
}
|
|
|
|
|
2006-04-15 04:19:55 +02:00
|
|
|
|
|
|
|
send_ldap_response 5, pdu[0].to_i, 0, "", "Was that what you wanted?"
|
|
|
|
end
|
|
|
|
|
2006-04-17 14:41:50 +02:00
|
|
|
|
|
|
|
|
2006-04-15 04:19:55 +02:00
|
|
|
def send_ldap_response pkt_tag, msgid, code, dn, text
|
|
|
|
send_data( [msgid.to_ber, [code.to_ber, dn.to_ber, text.to_ber].to_ber_appsequence(pkt_tag) ].to_ber )
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
2006-04-17 05:44:55 +02:00
|
|
|
#------------------------------------------------
|
|
|
|
|
|
|
|
# Rather bogus, a global method, which reads a HARDCODED filename
|
|
|
|
# parses out LDIF data. It will be used to serve LDAP queries out of this server.
|
|
|
|
#
|
|
|
|
def load_test_data
|
|
|
|
ary = File.readlines( "./testdata.ldif" )
|
|
|
|
hash = {}
|
|
|
|
while line = ary.shift and line.chomp!
|
|
|
|
if line =~ /^dn:[\s]*/i
|
|
|
|
dn = $'
|
|
|
|
hash[dn] = {}
|
|
|
|
while attr = ary.shift and attr.chomp! and attr =~ /^([\w]+)[\s]*:[\s]*/
|
|
|
|
hash[dn][$1.downcase] ||= []
|
|
|
|
hash[dn][$1.downcase] << $'
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
hash
|
|
|
|
end
|
|
|
|
|
|
|
|
|
2006-04-15 04:19:55 +02:00
|
|
|
#------------------------------------------------
|
|
|
|
|
|
|
|
if __FILE__ == $0
|
|
|
|
|
|
|
|
require 'rubygems'
|
|
|
|
require 'eventmachine'
|
|
|
|
|
|
|
|
require 'logger'
|
|
|
|
$logger = Logger.new $stderr
|
|
|
|
|
|
|
|
$logger.info "adding ../lib to loadpath, to pick up dev version of Net::LDAP."
|
|
|
|
$:.unshift "../lib"
|
|
|
|
|
2006-04-17 05:44:55 +02:00
|
|
|
$ldif = load_test_data
|
|
|
|
|
2006-04-17 02:35:42 +02:00
|
|
|
require 'net/ldap'
|
2006-04-15 04:19:55 +02:00
|
|
|
|
|
|
|
EventMachine.run {
|
|
|
|
$logger.info "starting LDAP server on 127.0.0.1 port 3890"
|
|
|
|
EventMachine.start_server "127.0.0.1", 3890, LdapServer
|
|
|
|
EventMachine.add_periodic_timer 60, proc {$logger.info "heartbeat"}
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|