diff options
-rw-r--r-- | Makefile | 16 | ||||
-rw-r--r-- | README | 27 | ||||
-rw-r--r-- | bind.go | 50 | ||||
-rw-r--r-- | conn.go | 270 | ||||
-rw-r--r-- | control.go | 157 | ||||
-rw-r--r-- | filter.go | 249 | ||||
-rw-r--r-- | filter_test.go | 78 | ||||
-rw-r--r-- | ldap.go | 291 | ||||
-rw-r--r-- | search.go | 244 |
9 files changed, 1382 insertions, 0 deletions
diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d03d676 --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +# Copyright 2009 The Go Authors. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +include $(GOROOT)/src/Make.inc + +TARG=github.com/mmitton/ldap +GOFILES=\ + bind.go\ + conn.go\ + control.go\ + filter.go\ + ldap.go\ + search.go\ + +include $(GOROOT)/src/Make.pkg @@ -0,0 +1,27 @@ +Basic LDAP v3 functionality for the GO programming language. + +Required Librarys: + github.com/mmitton/asn1/ber + +Working: + Connecting to LDAP server + Binding to LDAP server + Searching for entries + Compiling string filters to LDAP filters + Paging Search Results + Mulitple internal goroutines to handle network traffic + Makes library goroutine safe + Can perform multiple search requests at the same time and return + the results to the proper goroutine. All requests are blocking + requests, so the goroutine does not need special handling + +Tests Implemented: + Filter Compile / Decompile + +TODO: + Modify Requests / Responses + Add Requests / Responses + Delete Requests / Responses + Modify DN Requests / Responses + Compare Requests / Responses + Implement Tests / Benchmarks @@ -0,0 +1,50 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// File contains Bind functionality +package ldap + +import ( + "github.com/mmitton/asn1-ber" + "os" +) + +func (l *Conn) Bind( username, password string ) *Error { + messageID := l.nextMessageID() + + packet := ber.Encode( ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request" ) + packet.AppendChild( ber.NewInteger( ber.ClassUniversal, ber.TypePrimative, ber.TagInteger, messageID, "MessageID" ) ) + bindRequest := ber.Encode( ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request" ) + bindRequest.AppendChild( ber.NewInteger( ber.ClassUniversal, ber.TypePrimative, ber.TagInteger, 3, "Version" ) ) + bindRequest.AppendChild( ber.NewString( ber.ClassUniversal, ber.TypePrimative, ber.TagOctetString, username, "User Name" ) ) + bindRequest.AppendChild( ber.NewString( ber.ClassContext, ber.TypePrimative, 0, password, "Password" ) ) + packet.AppendChild( bindRequest ) + + if l.Debug { + ber.PrintPacket( packet ) + } + + channel, err := l.sendMessage( packet ) + if err != nil { + return err + } + if channel == nil { + return NewError( ErrorNetwork, os.NewError( "Could not send message" ) ) + } + defer l.finishMessage( messageID ) + packet = <-channel + + if packet != nil { + return NewError( ErrorNetwork, os.NewError( "Could not retrieve response" ) ) + } + + if l.Debug { + if err := addLDAPDescriptions( packet ); err != nil { + return NewError( ErrorDebugging, err ) + } + ber.PrintPacket( packet ) + } + + return nil +} @@ -0,0 +1,270 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This package provides LDAP client functions. +package ldap + +import ( + "github.com/mmitton/asn1-ber" + "crypto/tls" + "fmt" + "net" + "os" +) + +// LDAP Connection +type Conn struct { + conn net.Conn + isSSL bool + Debug bool + + chanResults map[ uint64 ] chan *ber.Packet + chanProcessMessage chan *messagePacket + chanMessageID chan uint64 +} + +// Dial connects to the given address on the given network using net.Dial +// and then returns a new Conn for the connection. +func Dial(network, addr string) (*Conn, *Error) { + c, err := net.Dial(network, "", addr) + if err != nil { + return nil, NewError( ErrorNetwork, err ) + } + conn := NewConn(c) + conn.start() + return conn, nil +} + +// Dial connects to the given address on the given network using net.Dial +// and then sets up SSL connection and returns a new Conn for the connection. +func DialSSL(network, addr string) (*Conn, *Error) { + c, err := tls.Dial(network, "", addr, nil) + if err != nil { + return nil, NewError( ErrorNetwork, err ) + } + conn := NewConn(c) + conn.isSSL = true + + conn.start() + return conn, nil +} + +// Dial connects to the given address on the given network using net.Dial +// and then starts a TLS session and returns a new Conn for the connection. +func DialTLS(network, addr string) (*Conn, *Error) { + c, err := net.Dial(network, "", addr) + if err != nil { + return nil, NewError( ErrorNetwork, err ) + } + conn := NewConn(c) + + err = conn.startTLS() + if err != nil { + conn.Close() + return nil, NewError( ErrorNetwork, err ) + } + conn.start() + return conn, nil +} + +// NewConn returns a new Conn using conn for network I/O. +func NewConn(conn net.Conn) *Conn { + return &Conn{ + conn: conn, + isSSL: false, + Debug: false, + chanResults: map[uint64] chan *ber.Packet{}, + chanProcessMessage: make( chan *messagePacket ), + chanMessageID: make( chan uint64 ), + } +} + +func (l *Conn) start() { + go l.reader() + go l.processMessages() +} + +// Close closes the connection. +func (l *Conn) Close() *Error { + if l.chanProcessMessage != nil { + message_packet := &messagePacket{ Op: MessageQuit } + l.chanProcessMessage <- message_packet + l.chanProcessMessage = nil + } + + if l.conn != nil { + err := l.conn.Close() + if err != nil { + return NewError( ErrorNetwork, err ) + } + l.conn = nil + } + return nil +} + +// Returns the next available messageID +func (l *Conn) nextMessageID() uint64 { + messageID := <-l.chanMessageID + return messageID +} + +// StartTLS sends the command to start a TLS session and then creates a new TLS Client +func (l *Conn) startTLS() *Error { + messageID := l.nextMessageID() + + if l.isSSL { + return NewError( ErrorNetwork, os.NewError( "Already encrypted" ) ) + } + + packet := ber.Encode( ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request" ) + packet.AppendChild( ber.NewInteger( ber.ClassUniversal, ber.TypePrimative, ber.TagInteger, messageID, "MessageID" ) ) + startTLS := ber.Encode( ber.ClassApplication, ber.TypeConstructed, ApplicationExtendedRequest, nil, "Start TLS" ) + startTLS.AppendChild( ber.NewString( ber.ClassContext, ber.TypePrimative, 0, "1.3.6.1.4.1.1466.20037", "TLS Extended Command" ) ) + packet.AppendChild( startTLS ) + if l.Debug { + ber.PrintPacket( packet ) + } + + _, err := l.conn.Write( packet.Bytes() ) + if err != nil { + return NewError( ErrorNetwork, err ) + } + + packet, err = ber.ReadPacket( l.conn ) + if err != nil { + return NewError( ErrorNetwork, err ) + } + + if l.Debug { + if err := addLDAPDescriptions( packet ); err != nil { + return NewError( ErrorDebugging, err ) + } + ber.PrintPacket( packet ) + } + + if packet.Children[ 1 ].Children[ 0 ].Value.(uint64) == 0 { + conn := tls.Client( l.conn, nil ) + l.isSSL = true + l.conn = conn + } + + return nil +} + +const ( + MessageQuit = 0 + MessageRequest = 1 + MessageResponse = 2 + MessageFinish = 3 +) + +type messagePacket struct { + Op int + MessageID uint64 + Packet *ber.Packet + Channel chan *ber.Packet +} + +func (l *Conn) sendMessage( p *ber.Packet ) (out chan *ber.Packet, err *Error) { + message_id := p.Children[ 0 ].Value.(uint64) + out = make(chan *ber.Packet) + + message_packet := &messagePacket{ Op: MessageRequest, MessageID: message_id, Packet: p, Channel: out } + if l.chanProcessMessage == nil { + err = NewError( ErrorNetwork, os.NewError( "Connection closed" ) ) + return + } + l.chanProcessMessage <- message_packet + return +} + +func (l *Conn) processMessages() { + defer l.closeAllChannels() + + var message_id uint64 = 1 + var message_packet *messagePacket + for { + select { + case l.chanMessageID <- message_id: + if l.conn == nil { + return + } + message_id++ + case message_packet = <-l.chanProcessMessage: + if l.conn == nil { + return + } + switch message_packet.Op { + case MessageQuit: + // Close all channels and quit + if l.Debug { + fmt.Printf( "Shutting down\n" ) + } + return + case MessageRequest: + // Add to message list and write to network + if l.Debug { + fmt.Printf( "Sending message %d\n", message_packet.MessageID ) + } + l.chanResults[ message_packet.MessageID ] = message_packet.Channel + l.conn.Write( message_packet.Packet.Bytes() ) + case MessageResponse: + // Pass back to waiting goroutine + if l.Debug { + fmt.Printf( "Receiving message %d\n", message_packet.MessageID ) + } + chanResult := l.chanResults[ message_packet.MessageID ] + if chanResult == nil { + fmt.Printf( "Unexpected Message Result: %d", message_id ) + } else { + chanResult <- message_packet.Packet + } + case MessageFinish: + // Remove from message list + if l.Debug { + fmt.Printf( "Finished message %d\n", message_packet.MessageID ) + } + l.chanResults[ message_packet.MessageID ] = nil, false + } + } + } +} + +func (l *Conn) closeAllChannels() { + for MessageID, Channel := range l.chanResults { + if l.Debug { + fmt.Printf( "Closing channel for MessageID %d\n", MessageID ); + } + close( Channel ) + l.chanResults[ MessageID ] = nil, false + } + close( l.chanMessageID ) + l.chanMessageID = nil +} + +func (l *Conn) finishMessage( MessageID uint64 ) { + message_packet := &messagePacket{ Op: MessageFinish, MessageID: MessageID } + if l.chanProcessMessage != nil { + l.chanProcessMessage <- message_packet + } +} + +func (l *Conn) reader() { + for { + p, err := ber.ReadPacket( l.conn ) + if err != nil { + if l.Debug { + fmt.Printf( "ldap.reader: %s\n", err.String() ) + } + break + } + + message_id := p.Children[ 0 ].Value.(uint64) + message_packet := &messagePacket{ Op: MessageResponse, MessageID: message_id, Packet: p } + l.chanProcessMessage <- message_packet + } + + l.Close() +} + diff --git a/control.go b/control.go new file mode 100644 index 0000000..af145b2 --- /dev/null +++ b/control.go @@ -0,0 +1,157 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This package provides LDAP client functions. +package ldap + +import ( + "github.com/mmitton/asn1-ber" + "fmt" +) + +const ( + ControlTypePaging = "1.2.840.113556.1.4.319" +) + +var ControlTypeMap = map[ string ] string { + ControlTypePaging : "Paging", +} + +type Control interface { + GetControlType() string + Encode() *ber.Packet + String() string +} + +type ControlString struct { + ControlType string + Criticality bool + ControlValue string +} + +func (c *ControlString) GetControlType() string { + return c.ControlType +} + +func (c *ControlString) Encode() (p *ber.Packet) { + p = ber.Encode( ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control" ) + p.AppendChild( ber.NewString( ber.ClassUniversal, ber.TypePrimative, ber.TagOctetString, c.ControlType, "Control Type (" + ControlTypeMap[ c.ControlType ] + ")" ) ) + if c.Criticality { + p.AppendChild( ber.NewBoolean( ber.ClassUniversal, ber.TypePrimative, ber.TagBoolean, c.Criticality, "Criticality" ) ) + } + p.AppendChild( ber.NewString( ber.ClassUniversal, ber.TypePrimative, ber.TagOctetString, c.ControlValue, "Control Value" ) ) + return +} + +func (c *ControlString) String() string { + return fmt.Sprintf( "Control Type: %s (%q) Criticality: %s Control Value: %s", ControlTypeMap[ c.ControlType ], c.ControlType, c.Criticality, c.ControlValue ) +} + +type ControlPaging struct { + PagingSize uint32 + Cookie []byte +} + +func (c *ControlPaging) GetControlType() string { + return ControlTypePaging +} + +func (c *ControlPaging) Encode() (p *ber.Packet) { + p = ber.Encode( ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control" ) + p.AppendChild( ber.NewString( ber.ClassUniversal, ber.TypePrimative, ber.TagOctetString, ControlTypePaging, "Control Type (" + ControlTypeMap[ ControlTypePaging ] + ")" ) ) + + p2 := ber.Encode( ber.ClassUniversal, ber.TypePrimative, ber.TagOctetString, nil, "Control Value (Paging)" ) + seq := ber.Encode( ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Search Control Value" ) + seq.AppendChild( ber.NewInteger( ber.ClassUniversal, ber.TypePrimative, ber.TagInteger, uint64(c.PagingSize), "Paging Size" ) ) + cookie := ber.Encode( ber.ClassUniversal, ber.TypePrimative, ber.TagOctetString, nil, "Cookie" ) + cookie.Value = c.Cookie + cookie.Data.Write( c.Cookie ) + seq.AppendChild( cookie ) + p2.AppendChild( seq ) + + p.AppendChild( p2 ) + return +} + +func (c *ControlPaging) String() string { + return fmt.Sprintf( + "Control Type: %s (%q) Criticality: %s PagingSize: %d Cookie: %q", + ControlTypeMap[ ControlTypePaging ], + ControlTypePaging, + false, + c.PagingSize, + c.Cookie ) +} + +func (c *ControlPaging) SetCookie( Cookie []byte ) { + c.Cookie = Cookie +} + +func FindControl( Controls []Control, ControlType string ) Control { + for _, c := range Controls { + if c.GetControlType() == ControlType { + return c + } + } + return nil +} + +func DecodeControl( p *ber.Packet ) Control { + ControlType := p.Children[ 0 ].Value.(string) + Criticality := false + + p.Children[ 0 ].Description = "Control Type (" + ControlTypeMap[ ControlType ] + ")" + value := p.Children[ 1 ] + if len( p.Children ) == 3 { + value = p.Children[ 2 ] + p.Children[ 1 ].Description = "Criticality" + Criticality = p.Children[ 1 ].Value.(bool) + } + + value.Description = "Control Value" + switch ControlType { + case ControlTypePaging: + value.Description += " (Paging)" + c := new( ControlPaging ) + if value.Value != nil { + value_children := ber.DecodePacket( value.Data.Bytes() ) + value.Data.Truncate( 0 ) + value.Value = nil + value.AppendChild( value_children ) + } + value = value.Children[ 0 ] + value.Description = "Search Control Value" + value.Children[ 0 ].Description = "Paging Size" + value.Children[ 1 ].Description = "Cookie" + c.PagingSize = uint32( value.Children[ 0 ].Value.(uint64) ) + c.Cookie = value.Children[ 1 ].Data.Bytes() + value.Children[ 1 ].Value = c.Cookie + return c + } + c := new( ControlString ) + c.ControlType = ControlType + c.Criticality = Criticality + c.ControlValue = value.Value.(string) + return c +} + +func NewControlString( ControlType string, Criticality bool, ControlValue string ) *ControlString { + return &ControlString{ + ControlType: ControlType, + Criticality: Criticality, + ControlValue: ControlValue, + } +} + +func NewControlPaging( PagingSize uint32 ) *ControlPaging { + return &ControlPaging{ PagingSize: PagingSize } +} + +func encodeControls( Controls []Control ) *ber.Packet { + p := ber.Encode( ber.ClassContext, ber.TypeConstructed, 0, nil, "Controls" ) + for _, control := range Controls { + p.AppendChild( control.Encode() ) + } + return p +} diff --git a/filter.go b/filter.go new file mode 100644 index 0000000..1b8fd1e --- /dev/null +++ b/filter.go @@ -0,0 +1,249 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// File contains a filter compiler/decompiler +package ldap + +import ( + "fmt" + "os" + "github.com/mmitton/asn1-ber" +) + +const ( + FilterAnd = 0 + FilterOr = 1 + FilterNot = 2 + FilterEqualityMatch = 3 + FilterSubstrings = 4 + FilterGreaterOrEqual = 5 + FilterLessOrEqual = 6 + FilterPresent = 7 + FilterApproxMatch = 8 + FilterExtensibleMatch = 9 +) + +var FilterMap = map[ uint64 ] string { + FilterAnd : "And", + FilterOr : "Or", + FilterNot : "Not", + FilterEqualityMatch : "Equality Match", + FilterSubstrings : "Substrings", + FilterGreaterOrEqual : "Greater Or Equal", + FilterLessOrEqual : "Less Or Equal", + FilterPresent : "Present", + FilterApproxMatch : "Approx Match", + FilterExtensibleMatch : "Extensible Match", +} + +const ( + FilterSubstringsInitial = 0 + FilterSubstringsAny = 1 + FilterSubstringsFinal = 2 +) + +var FilterSubstringsMap = map[ uint64 ] string { + FilterSubstringsInitial : "Substrings Initial", + FilterSubstringsAny : "Substrings Any", + FilterSubstringsFinal : "Substrings Final", +} + +func CompileFilter( filter string ) ( *ber.Packet, *Error ) { + if len( filter ) == 0 || filter[ 0 ] != '(' { + return nil, NewError( ErrorFilterCompile, os.NewError( "Filter does not start with an '('" ) ) + } + packet, pos, err := compileFilter( filter, 1 ) + if err != nil { + return nil, err + } + if pos != len( filter ) { + return nil, NewError( ErrorFilterCompile, os.NewError( "Finished compiling filter with extra at end.\n" + fmt.Sprint( filter[pos:] ) ) ) + } + return packet, nil +} + +func DecompileFilter( packet *ber.Packet ) (ret string, err *Error) { + defer func() { + if r := recover(); r != nil { + err = NewError( ErrorFilterDecompile, os.NewError( "Error decompiling filter" ) ) + } + }() + ret = "(" + err = nil + child_str := "" + + switch packet.Tag { + case FilterAnd: + ret += "&" + for _, child := range packet.Children { + child_str, err = DecompileFilter( child ) + if err != nil { + return + } + ret += child_str + } + case FilterOr: + ret += "|" + for _, child := range packet.Children { + child_str, err = DecompileFilter( child ) + if err != nil { + return + } + ret += child_str + } + case FilterNot: + ret += "!" + child_str, err = DecompileFilter( packet.Children[ 0 ] ) + if err != nil { + return + } + ret += child_str + + case FilterSubstrings: + ret += ber.DecodeString( packet.Children[ 0 ].Data.Bytes() ) + ret += "=" + switch packet.Children[ 1 ].Children[ 0 ].Tag { + case FilterSubstringsInitial: + ret += ber.DecodeString( packet.Children[ 1 ].Children[ 0 ].Data.Bytes() ) + "*" + case FilterSubstringsAny: + ret += "*" + ber.DecodeString( packet.Children[ 1 ].Children[ 0 ].Data.Bytes() ) + "*" + case FilterSubstringsFinal: + ret += "*" + ber.DecodeString( packet.Children[ 1 ].Children[ 0 ].Data.Bytes() ) + } + case FilterEqualityMatch: + ret += ber.DecodeString( packet.Children[ 0 ].Data.Bytes() ) + ret += "=" + ret += ber.DecodeString( packet.Children[ 1 ].Data.Bytes() ) + case FilterGreaterOrEqual: + ret += ber.DecodeString( packet.Children[ 0 ].Data.Bytes() ) + ret += ">=" + ret += ber.DecodeString( packet.Children[ 1 ].Data.Bytes() ) + case FilterLessOrEqual: + ret += ber.DecodeString( packet.Children[ 0 ].Data.Bytes() ) + ret += "<=" + ret += ber.DecodeString( packet.Children[ 1 ].Data.Bytes() ) + case FilterPresent: + ret += ber.DecodeString( packet.Children[ 0 ].Data.Bytes() ) + ret += "=*" + case FilterApproxMatch: + ret += ber.DecodeString( packet.Children[ 0 ].Data.Bytes() ) + ret += "~=" + ret += ber.DecodeString( packet.Children[ 1 ].Data.Bytes() ) + } + + ret += ")" + return +} + +func compileFilterSet( filter string, pos int, parent *ber.Packet ) ( int, *Error ) { + for pos < len( filter ) && filter[ pos ] == '(' { + child, new_pos, err := compileFilter( filter, pos + 1 ) + if err != nil { + return pos, err + } + pos = new_pos + parent.AppendChild( child ) + } + if pos == len( filter ) { + return pos, NewError( ErrorFilterCompile, os.NewError( "Unexpected end of filter" ) ) + } + + return pos + 1, nil +} + +func compileFilter( filter string, pos int ) ( p *ber.Packet, new_pos int, err *Error ) { + defer func() { + if r := recover(); r != nil { + err = NewError( ErrorFilterCompile, os.NewError( "Error compiling filter" ) ) + } + }() + p = nil + new_pos = pos + err = nil + + switch filter[pos] { + case '(': + p, new_pos, err = compileFilter( filter, pos + 1 ) + new_pos++ + return + case '&': + p = ber.Encode( ber.ClassContext, ber.TypeConstructed, FilterAnd, nil, FilterMap[ FilterAnd ] ) + new_pos, err = compileFilterSet( filter, pos + 1, p ) + return + case '|': + p = ber.Encode( ber.ClassContext, ber.TypeConstructed, FilterOr, nil, FilterMap[ FilterOr ] ) + new_pos, err = compileFilterSet( filter, pos + 1, p ) + return + case '!': + p = ber.Encode( ber.ClassContext, ber.TypeConstructed, FilterNot, nil, FilterMap[ FilterNot ] ) + var child *ber.Packet + child, new_pos, err = compileFilter( filter, pos + 1 ) + p.AppendChild( child ) + return + default: + attribute := "" + condition := "" + for new_pos < len( filter ) && filter[ new_pos ] != ')' { + switch { + case p != nil: + condition += fmt.Sprintf( "%c", filter[ new_pos ] ) + case filter[ new_pos ] == '=': + p = ber.Encode( ber.ClassContext, ber.TypeConstructed, FilterEqualityMatch, nil, FilterMap[ FilterEqualityMatch ] ) + case filter[ new_pos ] == '>' && filter[ new_pos + 1 ] == '=': + p = ber.Encode( ber.ClassContext, ber.TypeConstructed, FilterGreaterOrEqual, nil, FilterMap[ FilterGreaterOrEqual ] ) + new_pos++ + case filter[ new_pos ] == '<' && filter[ new_pos + 1 ] == '=': + p = ber.Encode( ber.ClassContext, ber.TypeConstructed, FilterLessOrEqual, nil, FilterMap[ FilterLessOrEqual ] ) + new_pos++ + case filter[ new_pos ] == '~' && filter[ new_pos + 1 ] == '=': + p = ber.Encode( ber.ClassContext, ber.TypeConstructed, FilterApproxMatch, nil, FilterMap[ FilterLessOrEqual ] ) + new_pos++ + case p == nil: + attribute += fmt.Sprintf( "%c", filter[ new_pos ] ) + } + new_pos++ + } + if new_pos == len( filter ) { + err = NewError( ErrorFilterCompile, os.NewError( "Unexpected end of filter" ) ) + return + } + if p == nil { + err = NewError( ErrorFilterCompile, os.NewError( "Error parsing filter" ) ) + return + } + p.AppendChild( ber.NewString( ber.ClassUniversal, ber.TypePrimative, ber.TagOctetString, attribute, "Attribute" ) ) + switch { + case p.Tag == FilterEqualityMatch && condition == "*": + p.Tag = FilterPresent + p.Description = FilterMap[ uint64(p.Tag) ] + case p.Tag == FilterEqualityMatch && condition[ 0 ] == '*' && condition[ len( condition ) - 1 ] == '*': + // Any + p.Tag = FilterSubstrings + p.Description = FilterMap[ uint64(p.Tag) ] + seq := ber.Encode( ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Substrings" ) + seq.AppendChild( ber.NewString( ber.ClassContext, ber.TypePrimative, FilterSubstringsAny, condition[ 1 : len( condition ) - 1 ], "Any Substring" ) ) + p.AppendChild( seq ) + case p.Tag == FilterEqualityMatch && condition[ 0 ] == '*': + // Final + p.Tag = FilterSubstrings + p.Description = FilterMap[ uint64(p.Tag) ] + seq := ber.Encode( ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Substrings" ) + seq.AppendChild( ber.NewString( ber.ClassContext, ber.TypePrimative, FilterSubstringsFinal, condition[ 1: ], "Final Substring" ) ) + p.AppendChild( seq ) + case p.Tag == FilterEqualityMatch && condition[ len( condition ) - 1 ] == '*': + // Initial + p.Tag = FilterSubstrings + p.Description = FilterMap[ uint64(p.Tag) ] + seq := ber.Encode( ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Substrings" ) + seq.AppendChild( ber.NewString( ber.ClassContext, ber.TypePrimative, FilterSubstringsInitial, condition[ :len( condition ) - 1 ], "Initial Substring" ) ) + p.AppendChild( seq ) + default: + p.AppendChild( ber.NewString( ber.ClassUniversal, ber.TypePrimative, ber.TagOctetString, condition, "Condition" ) ) + } + new_pos++ + return + } + err = NewError( ErrorFilterCompile, os.NewError( "Reached end of filter without closing parens" ) ) + return +} diff --git a/filter_test.go b/filter_test.go new file mode 100644 index 0000000..4b403e4 --- /dev/null +++ b/filter_test.go @@ -0,0 +1,78 @@ +package ldap + +import ( + "github.com/mmitton/asn1-ber" + "testing" +) + +type compile_test struct { + filter_str string + filter_type int +} + + +var test_filters = []compile_test { + compile_test{ filter_str: "(&(sn=Miller)(givenName=Bob))", filter_type: FilterAnd }, + compile_test{ filter_str: "(|(sn=Miller)(givenName=Bob))", filter_type: FilterOr }, + compile_test{ filter_str: "(!(sn=Miller))", filter_type: FilterNot }, + compile_test{ filter_str: "(sn=Miller)", filter_type: FilterEqualityMatch }, + compile_test{ filter_str: "(sn=Mill*)", filter_type: FilterSubstrings }, + compile_test{ filter_str: "(sn=*Mill)", filter_type: FilterSubstrings }, + compile_test{ filter_str: "(sn=*Mill*)", filter_type: FilterSubstrings }, + compile_test{ filter_str: "(sn>=Miller)", filter_type: FilterGreaterOrEqual }, + compile_test{ filter_str: "(sn<=Miller)", filter_type: FilterLessOrEqual }, + compile_test{ filter_str: "(sn=*)", filter_type: FilterPresent }, + compile_test{ filter_str: "(sn~=Miller)", filter_type: FilterApproxMatch }, + // compile_test{ filter_str: "()", filter_type: FilterExtensibleMatch }, +} + +func TestFilter( t *testing.T ) { + // Test Compiler and Decompiler + for _, i := range test_filters { + filter, err := CompileFilter( i.filter_str ) + if err != nil { + t.Errorf( "Problem compiling %s - %s", err.String() ) + } else if filter.Tag != uint8(i.filter_type) { + t.Errorf( "%q Expected %q got %q", i.filter_str, FilterMap[ uint64(i.filter_type) ], FilterMap[ uint64(filter.Tag) ] ) + } else { + o, err := DecompileFilter( filter ) + if err != nil { + t.Errorf( "Problem compiling %s - %s", i, err.String() ) + } else if i.filter_str != o { + t.Errorf( "%q expected, got %q", i.filter_str, o ) + } + } + } +} + +func BenchmarkFilterCompile( b *testing.B ) { + b.StopTimer() + filters := make([]string, len( test_filters ) ) + + // Test Compiler and Decompiler + for idx, i := range test_filters { + filters[ idx ] = i.filter_str + } + + max_idx := len( filters ) + b.StartTimer() + for i := 0; i < b.N; i++ { + CompileFilter( filters[ i % max_idx ] ) + } +} + +func BenchmarkFilterDecompile( b *testing.B ) { + b.StopTimer() + filters := make([]*ber.Packet, len( test_filters ) ) + + // Test Compiler and Decompiler + for idx, i := range test_filters { + filters[ idx ], _ = CompileFilter( i.filter_str ) + } + + max_idx := len( filters ) + b.StartTimer() + for i := 0; i < b.N; i++ { + DecompileFilter( filters[ i % max_idx ] ) + } +} @@ -0,0 +1,291 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This package provides LDAP client functions. +package ldap + +import ( + "github.com/mmitton/asn1-ber" + "fmt" + "io/ioutil" + "os" +) + +// LDAP Application Codes +const ( + ApplicationBindRequest = 0 + ApplicationBindResponse = 1 + ApplicationUnbindRequest = 2 + ApplicationSearchRequest = 3 + ApplicationSearchResultEntry = 4 + ApplicationSearchResultDone = 5 + ApplicationModifyRequest = 6 + ApplicationModifyResponse = 7 + ApplicationAddRequest = 8 + ApplicationAddResponse = 9 + ApplicationDelRequest = 10 + ApplicationDelResponse = 11 + ApplicationModifyDNRequest = 12 + ApplicationModifyDNResponse = 13 + ApplicationCompareRequest = 14 + ApplicationCompareResponse = 15 + ApplicationAbandonRequest = 16 + ApplicationSearchResultReference = 19 + ApplicationExtendedRequest = 23 + ApplicationExtendedResponse = 24 +) + +var ApplicationMap = map[ uint8 ] string { + ApplicationBindRequest : "Bind Request", + ApplicationBindResponse : "Bind Response", + ApplicationUnbindRequest : "Unbind Request", + ApplicationSearchRequest : "Search Request", + ApplicationSearchResultEntry : "Search Result Entry", + ApplicationSearchResultDone : "Search Result Done", + ApplicationModifyRequest : "Modify Request", + ApplicationModifyResponse : "Modify Response", + ApplicationAddRequest : "Add Request", + ApplicationAddResponse : "Add Response", + ApplicationDelRequest : "Del Request", + ApplicationDelResponse : "Del Response", + ApplicationModifyDNRequest : "Modify DN Request", + ApplicationModifyDNResponse : "Modify DN Response", + ApplicationCompareRequest : "Compare Request", + ApplicationCompareResponse : "Compare Response", + ApplicationAbandonRequest : "Abandon Request", + ApplicationSearchResultReference : "Search Result Reference", + ApplicationExtendedRequest : "Extended Request", + ApplicationExtendedResponse : "Extended Response", +} + +// LDAP Result Codes +const ( + LDAPResultSuccess = 0 + LDAPResultOperationsError = 1 + LDAPResultProtocolError = 2 + LDAPResultTimeLimitExceeded = 3 + LDAPResultSizeLimitExceeded = 4 + LDAPResultCompareFalse = 5 + LDAPResultCompareTrue = 6 + LDAPResultAuthMethodNotSupported = 7 + LDAPResultStrongAuthRequired = 8 + LDAPResultReferral = 10 + LDAPResultAdminLimitExceeded = 11 + LDAPResultUnavailableCriticalExtension = 12 + LDAPResultConfidentialityRequired = 13 + LDAPResultSaslBindInProgress = 14 + LDAPResultNoSuchAttribute = 16 + LDAPResultUndefinedAttributeType = 17 + LDAPResultInappropriateMatching = 18 + LDAPResultConstraintViolation = 19 + LDAPResultAttributeOrValueExists = 20 + LDAPResultInvalidAttributeSyntax = 21 + LDAPResultNoSuchObject = 32 + LDAPResultAliasProblem = 33 + LDAPResultInvalidDNSyntax = 34 + LDAPResultAliasDereferencingProblem = 36 + LDAPResultInappropriateAuthentication = 48 + LDAPResultInvalidCredentials = 49 + LDAPResultInsufficientAccessRights = 50 + LDAPResultBusy = 51 + LDAPResultUnavailable = 52 + LDAPResultUnwillingToPerform = 53 + LDAPResultLoopDetect = 54 + LDAPResultNamingViolation = 64 + LDAPResultObjectClassViolation = 65 + LDAPResultNotAllowedOnNonLeaf = 66 + LDAPResultNotAllowedOnRDN = 67 + LDAPResultEntryAlreadyExists = 68 + LDAPResultObjectClassModsProhibited = 69 + LDAPResultAffectsMultipleDSAs = 71 + LDAPResultOther = 80 + + ErrorNetwork = 200 + ErrorFilterCompile = 201 + ErrorFilterDecompile = 202 + ErrorDebugging = 203 +) + +var LDAPResultCodeMap = map[uint8] string { + LDAPResultSuccess : "Success", + LDAPResultOperationsError : "Operations Error", + LDAPResultProtocolError : "Protocol Error", + LDAPResultTimeLimitExceeded : "Time Limit Exceeded", + LDAPResultSizeLimitExceeded : "Size Limit Exceeded", + LDAPResultCompareFalse : "Compare False", + LDAPResultCompareTrue : "Compare True", + LDAPResultAuthMethodNotSupported : "Auth Method Not Supported", + LDAPResultStrongAuthRequired : "Strong Auth Required", + LDAPResultReferral : "Referral", + LDAPResultAdminLimitExceeded : "Admin Limit Exceeded", + LDAPResultUnavailableCriticalExtension : "Unavailable Critical Extension", + LDAPResultConfidentialityRequired : "Confidentiality Required", + LDAPResultSaslBindInProgress : "Sasl Bind In Progress", + LDAPResultNoSuchAttribute : "No Such Attribute", + LDAPResultUndefinedAttributeType : "Undefined Attribute Type", + LDAPResultInappropriateMatching : "Inappropriate Matching", + LDAPResultConstraintViolation : "Constraint Violation", + LDAPResultAttributeOrValueExists : "Attribute Or Value Exists", + LDAPResultInvalidAttributeSyntax : "Invalid Attribute Syntax", + LDAPResultNoSuchObject : "No Such Object", + LDAPResultAliasProblem : "Alias Problem", + LDAPResultInvalidDNSyntax : "Invalid DN Syntax", + LDAPResultAliasDereferencingProblem : "Alias Dereferencing Problem", + LDAPResultInappropriateAuthentication : "Inappropriate Authentication", + LDAPResultInvalidCredentials : "Invalid Credentials", + LDAPResultInsufficientAccessRights : "Insufficient Access Rights", + LDAPResultBusy : "Busy", + LDAPResultUnavailable : "Unavailable", + LDAPResultUnwillingToPerform : "Unwilling To Perform", + LDAPResultLoopDetect : "Loop Detect", + LDAPResultNamingViolation : "Naming Violation", + LDAPResultObjectClassViolation : "Object Class Violation", + LDAPResultNotAllowedOnNonLeaf : "Not Allowed On Non Leaf", + LDAPResultNotAllowedOnRDN : "Not Allowed On RDN", + LDAPResultEntryAlreadyExists : "Entry Already Exists", + LDAPResultObjectClassModsProhibited : "Object Class Mods Prohibited", + LDAPResultAffectsMultipleDSAs : "Affects Multiple DSAs", + LDAPResultOther : "Other", +} + +// Adds descriptions to an LDAP Response packet for debugging +func addLDAPDescriptions( packet *ber.Packet ) (err *Error) { + defer func() { + if r := recover(); r != nil { + err = NewError( ErrorDebugging, os.NewError( "Cannot process packet to add descriptions" ) ) + } + }() + packet.Description = "LDAP Response" + packet.Children[ 0 ].Description = "Message ID"; + + application := packet.Children[ 1 ].Tag + packet.Children[ 1 ].Description = ApplicationMap[ application ] + + switch application { + case ApplicationBindRequest: + addRequestDescriptions( packet ) + case ApplicationBindResponse: + addDefaultLDAPResponseDescriptions( packet ) + case ApplicationUnbindRequest: + addRequestDescriptions( packet ) + case ApplicationSearchRequest: + addRequestDescriptions( packet ) + case ApplicationSearchResultEntry: + packet.Children[ 1 ].Children[ 0 ].Description = "Object Name" + packet.Children[ 1 ].Children[ 1 ].Description = "Attributes" + for _, child := range packet.Children[ 1 ].Children[ 1 ].Children { + child.Description = "Attribute" + child.Children[ 0 ].Description = "Attribute Name" + child.Children[ 1 ].Description = "Attribute Values" + for _, grandchild := range child.Children[ 1 ].Children { + grandchild.Description = "Attribute Value" + } + } + if len( packet.Children ) == 3 { + addControlDescriptions( packet.Children[ 2 ] ) + } + case ApplicationSearchResultDone: + addDefaultLDAPResponseDescriptions( packet ) + case ApplicationModifyRequest: + addRequestDescriptions( packet ) + case ApplicationModifyResponse: + case ApplicationAddRequest: + addRequestDescriptions( packet ) + case ApplicationAddResponse: + case ApplicationDelRequest: + addRequestDescriptions( packet ) + case ApplicationDelResponse: + case ApplicationModifyDNRequest: + addRequestDescriptions( packet ) + case ApplicationModifyDNResponse: + case ApplicationCompareRequest: + addRequestDescriptions( packet ) + case ApplicationCompareResponse: + case ApplicationAbandonRequest: + addRequestDescriptions( packet ) + case ApplicationSearchResultReference: + case ApplicationExtendedRequest: + addRequestDescriptions( packet ) + case ApplicationExtendedResponse: + } + + return nil +} + +func addControlDescriptions( packet *ber.Packet ) { + packet.Description = "Controls" + for _, child := range packet.Children { + child.Description = "Control" + child.Children[ 0 ].Description = "Control Type (" + ControlTypeMap[ child.Children[ 0 ].Value.(string) ] + ")" + value := child.Children[ 1 ] + if len( child.Children ) == 3 { + child.Children[ 1 ].Description = "Criticality" + value = child.Children[ 2 ] + } + value.Description = "Control Value" + + switch child.Children[ 0 ].Value.(string) { + case ControlTypePaging: + value.Description += " (Paging)" + if value.Value != nil { + value_children := ber.DecodePacket( value.Data.Bytes() ) + value.Data.Truncate( 0 ) + value.Value = nil + value_children.Children[ 1 ].Value = value_children.Children[ 1 ].Data.Bytes() + value.AppendChild( value_children ) + } + value.Children[ 0 ].Description = "Real Search Control Value" + value.Children[ 0 ].Children[ 0 ].Description = "Paging Size" + value.Children[ 0 ].Children[ 1 ].Description = "Cookie" + } + } +} + +func addRequestDescriptions( packet *ber.Packet ) { + packet.Description = "LDAP Request" + packet.Children[ 0 ].Description = "Message ID" + packet.Children[ 1 ].Description = ApplicationMap[ packet.Children[ 1 ].Tag ]; + if len( packet.Children ) == 3 { + addControlDescriptions( packet.Children[ 2 ] ) + } +} + +func addDefaultLDAPResponseDescriptions( packet *ber.Packet ) { + resultCode := packet.Children[ 1 ].Children[ 0 ].Value.(uint64) + packet.Children[ 1 ].Children[ 0 ].Description = "Result Code (" + LDAPResultCodeMap[ uint8(resultCode) ] + ")"; + packet.Children[ 1 ].Children[ 1 ].Description = "Matched DN"; + packet.Children[ 1 ].Children[ 2 ].Description = "Error Message"; + if len( packet.Children[ 1 ].Children ) > 3 { + packet.Children[ 1 ].Children[ 3 ].Description = "Referral"; + } + if len( packet.Children ) == 3 { + addControlDescriptions( packet.Children[ 2 ] ) + } +} + +func DebugBinaryFile( FileName string ) *Error { + file, err := ioutil.ReadFile( FileName ) + if err != nil { + return NewError( ErrorDebugging, err ) + } + ber.PrintBytes( file, "" ) + packet := ber.DecodePacket( file ) + addLDAPDescriptions( packet ) + ber.PrintPacket( packet ) + + return nil +} + +type Error struct { + Err os.Error + ResultCode uint8 +} + +func (e *Error) String() string { + return fmt.Sprintf( "LDAP Result Code %d %q: %s", e.ResultCode, LDAPResultCodeMap[ e.ResultCode ], e.Err.String() ) +} + +func NewError( ResultCode uint8, Err os.Error ) (* Error) { + return &Error{ ResultCode: ResultCode, Err: Err } +} diff --git a/search.go b/search.go new file mode 100644 index 0000000..011dc94 --- /dev/null +++ b/search.go @@ -0,0 +1,244 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// File contains Search functionality +package ldap + +import ( + "github.com/mmitton/asn1-ber" + "fmt" + "os" +) + +const ( + ScopeBaseObject = 0 + ScopeSingleLevel = 1 + ScopeWholeSubtree = 2 +) + +var ScopeMap = map[ int ] string { + ScopeBaseObject : "Base Object", + ScopeSingleLevel : "Single Level", + ScopeWholeSubtree : "Whole Subtree", +} + +const ( + NeverDerefAliases = 0 + DerefInSearching = 1 + DerefFindingBaseObj = 2 + DerefAlways = 3 +) + +var DerefMap = map[ int ] string { + NeverDerefAliases : "NeverDerefAliases", + DerefInSearching : "DerefInSearching", + DerefFindingBaseObj : "DerefFindingBaseObj", + DerefAlways : "DerefAlways", +} + +type Entry struct { + DN string + Attributes []*EntryAttribute +} + +type EntryAttribute struct { + Name string + Values []string +} + +type SearchResult struct { + Entries []*Entry + Referrals []string + Controls []Control +} + +func (e *Entry) GetAttributeValues( Attribute string ) []string { + for _, attr := range e.Attributes { + if attr.Name == Attribute { + return attr.Values + } + } + + return []string{ } +} + +func (e *Entry) GetAttributeValue( Attribute string ) string { + values := e.GetAttributeValues( Attribute ) + if len( values ) == 0 { + return "" + } + return values[ 0 ] +} + +type SearchRequest struct { + BaseDN string + Scope int + DerefAliases int + SizeLimit int + TimeLimit int + TypesOnly bool + Filter string + Attributes []string + Controls []Control +} + +func NewSearchRequest( + BaseDN string, + Scope, DerefAliases, SizeLimit, TimeLimit int, + TypesOnly bool, + Filter string, + Attributes []string, + Controls []Control, + ) (*SearchRequest) { + return &SearchRequest{ + BaseDN: BaseDN, + Scope: Scope, + DerefAliases: DerefAliases, + SizeLimit: SizeLimit, + TimeLimit: TimeLimit, + TypesOnly: TypesOnly, + Filter: Filter, + Attributes: Attributes, + Controls: Controls, + } +} + +func (l *Conn) SearchWithPaging( SearchRequest *SearchRequest, PagingSize uint32 ) (*SearchResult, *Error) { + if SearchRequest.Controls == nil { + SearchRequest.Controls = make( []Control, 0 ) + } + + PagingControl := NewControlPaging( PagingSize ) + SearchRequest.Controls = append( SearchRequest.Controls, PagingControl ) + SearchResult := new( SearchResult ) + for { + result, err := l.Search( SearchRequest ) + if err != nil { + return SearchResult, err + } + if result == nil { + return SearchResult, NewError( ErrorNetwork, os.NewError( "Packet not received" ) ) + } + + for _, entry := range result.Entries { + SearchResult.Entries = append( SearchResult.Entries, entry ) + } + for _, referral := range result.Referrals { + SearchResult.Referrals = append( SearchResult.Referrals, referral ) + } + for _, control := range result.Controls { + SearchResult.Controls = append( SearchResult.Controls, control ) + } + + paging_result := FindControl( result.Controls, ControlTypePaging ) + if paging_result == nil { + PagingControl = nil + break + } + + cookie := paging_result.(*ControlPaging).Cookie + if len( cookie ) == 0 { + PagingControl = nil + break + } + PagingControl.SetCookie( cookie ) + } + + if PagingControl != nil { + PagingControl.PagingSize = 0 + l.Search( SearchRequest ) + } + + return SearchResult, nil +} + +func (l *Conn) Search( SearchRequest *SearchRequest ) (*SearchResult, *Error) { + messageID := l.nextMessageID() + + packet := ber.Encode( ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request" ) + packet.AppendChild( ber.NewInteger( ber.ClassUniversal, ber.TypePrimative, ber.TagInteger, messageID, "MessageID" ) ) + searchRequest := ber.Encode( ber.ClassApplication, ber.TypeConstructed, ApplicationSearchRequest, nil, "Search Request" ) + searchRequest.AppendChild( ber.NewString( ber.ClassUniversal, ber.TypePrimative, ber.TagOctetString, SearchRequest.BaseDN, "Base DN" ) ) + searchRequest.AppendChild( ber.NewInteger( ber.ClassUniversal, ber.TypePrimative, ber.TagEnumerated, uint64(SearchRequest.Scope), "Scope" ) ) + searchRequest.AppendChild( ber.NewInteger( ber.ClassUniversal, ber.TypePrimative, ber.TagEnumerated, uint64(SearchRequest.DerefAliases), "Deref Aliases" ) ) + searchRequest.AppendChild( ber.NewInteger( ber.ClassUniversal, ber.TypePrimative, ber.TagInteger, uint64(SearchRequest.SizeLimit), "Size Limit" ) ) + searchRequest.AppendChild( ber.NewInteger( ber.ClassUniversal, ber.TypePrimative, ber.TagInteger, uint64(SearchRequest.TimeLimit), "Time Limit" ) ) + searchRequest.AppendChild( ber.NewBoolean( ber.ClassUniversal, ber.TypePrimative, ber.TagBoolean, SearchRequest.TypesOnly, "Types Only" ) ) + filterPacket, err := CompileFilter( SearchRequest.Filter ) + if err != nil { + return nil, err + } + searchRequest.AppendChild( filterPacket ) + attributesPacket := ber.Encode( ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attributes" ) + for _, attribute := range SearchRequest.Attributes { + attributesPacket.AppendChild( ber.NewString( ber.ClassUniversal, ber.TypePrimative, ber.TagOctetString, attribute, "Attribute" ) ) + } + searchRequest.AppendChild( attributesPacket ) + packet.AppendChild( searchRequest ) + if SearchRequest.Controls != nil { + packet.AppendChild( encodeControls( SearchRequest.Controls ) ) + } + + if l.Debug { + ber.PrintPacket( packet ) + } + + channel, err := l.sendMessage( packet ) + if err != nil { + return nil, err + } + if channel == nil { + return nil, NewError( ErrorNetwork, os.NewError( "Could not send message" ) ) + } + defer l.finishMessage( messageID ) + + result := new( SearchResult ) + + foundSearchResultDone := false + for !foundSearchResultDone { + if l.Debug { + fmt.Printf( "%d: waiting for response\n", messageID ) + } + packet = <-channel + if l.Debug { + fmt.Printf( "%d: got response\n", messageID, packet ) + } + if packet == nil { + return nil, NewError( ErrorNetwork, os.NewError( "Could not retrieve message" ) ) + } + + if l.Debug { + if err := addLDAPDescriptions( packet ); err != nil { + return nil, NewError( ErrorDebugging, err ) + } + ber.PrintPacket( packet ) + } + + switch packet.Children[ 1 ].Tag { + case 4: + entry := new( Entry ) + entry.DN = packet.Children[ 1 ].Children[ 0 ].Value.(string) + for _, child := range packet.Children[ 1 ].Children[ 1 ].Children { + attr := new( EntryAttribute ) + attr.Name = child.Children[ 0 ].Value.(string) + for _, value := range child.Children[ 1 ].Children { + attr.Values = append( attr.Values, value.Value.(string) ) + } + entry.Attributes = append( entry.Attributes, attr ) + } + result.Entries = append( result.Entries, entry ) + case 5: + if len( packet.Children ) == 3 { + for _, child := range packet.Children[ 2 ].Children { + result.Controls = append( result.Controls, DecodeControl( child ) ) + } + } + foundSearchResultDone = true + case 19: + result.Referrals = append( result.Referrals, packet.Children[ 1 ].Children[ 0 ].Value.(string) ) + } + } + + return result, nil +} |