Files
opencloud/vendor/github.com/go-ldap/ldif/ldif.go
2023-04-19 20:24:34 +02:00

535 lines
12 KiB
Go

// Package ldif contains an LDIF parser and marshaller (RFC 2849).
package ldif
import (
"bufio"
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
"io/ioutil"
"net/url"
"strconv"
"strings"
"github.com/go-ldap/ldap/v3"
)
// Entry is one entry in the LDIF
type Entry struct {
Entry *ldap.Entry
Add *ldap.AddRequest
Del *ldap.DelRequest
Modify *ldap.ModifyRequest
}
// The LDIF struct is used for parsing an LDIF. The Controls
// is used to tell the parser to ignore any controls found
// when parsing (default: false to ignore the controls).
// FoldWidth is used for the line lenght when marshalling.
type LDIF struct {
Entries []*Entry
Version int
changeType string
FoldWidth int
Controls bool
firstEntry bool
}
// The ParseError holds the error message and the line in the ldif
// where the error occurred.
type ParseError struct {
Line int
Message string
}
// Error implements the error interface
func (e *ParseError) Error() string {
return fmt.Sprintf("Error in line %d: %s", e.Line, e.Message)
}
var cr byte = '\x0D'
var lf byte = '\x0A'
var sep = string([]byte{cr, lf})
var comment byte = '#'
var space byte = ' '
var spaces = string(space)
// Parse wraps Unmarshal to parse an LDIF from a string
func Parse(str string) (l *LDIF, err error) {
buf := bytes.NewBuffer([]byte(str))
l = &LDIF{}
err = Unmarshal(buf, l)
return
}
// ParseWithControls wraps Unmarshal to parse an LDIF from
// a string, controls are added to change records
func ParseWithControls(str string) (l *LDIF, err error) {
buf := bytes.NewBuffer([]byte(str))
l = &LDIF{Controls: true}
err = Unmarshal(buf, l)
return
}
// Unmarshal parses the LDIF from the given io.Reader into the LDIF struct.
// The caller is responsible for closing the io.Reader if that is
// needed.
func Unmarshal(r io.Reader, l *LDIF) (err error) {
if r == nil {
return &ParseError{Line: 0, Message: "No reader present"}
}
curLine := 0
l.Version = 0
l.changeType = ""
isComment := false
reader := bufio.NewReader(r)
var lines []string
var line, nextLine string
l.firstEntry = true
for {
curLine++
nextLine, err = reader.ReadString(lf)
nextLine = strings.TrimRight(nextLine, sep)
switch err {
case nil, io.EOF:
switch len(nextLine) {
case 0:
if len(line) == 0 && err == io.EOF {
return nil
}
if len(line) == 0 && len(lines) == 0 {
continue
}
lines = append(lines, line)
entry, perr := l.parseEntry(lines)
if perr != nil {
return &ParseError{Line: curLine, Message: perr.Error()}
}
l.Entries = append(l.Entries, entry)
line = ""
lines = []string{}
if err == io.EOF {
return nil
}
default:
switch nextLine[0] {
case comment:
isComment = true
continue
case space:
if isComment {
continue
}
line += nextLine[1:]
continue
default:
isComment = false
if len(line) != 0 {
lines = append(lines, line)
}
line = nextLine
continue
}
}
default:
return &ParseError{Line: curLine, Message: err.Error()}
}
}
}
func (l *LDIF) parseEntry(lines []string) (entry *Entry, err error) {
if len(lines) == 0 {
return nil, errors.New("empty entry?")
}
if l.firstEntry && strings.HasPrefix(lines[0], "version:") {
l.firstEntry = false
line := strings.TrimLeft(lines[0][8:], spaces)
if l.Version, err = strconv.Atoi(line); err != nil {
return nil, err
}
if l.Version != 1 {
return nil, errors.New("Invalid version spec " + string(line))
}
l.Version = 1
if len(lines) == 1 {
return nil, nil
}
lines = lines[1:]
}
l.firstEntry = false
if len(lines) == 0 {
return nil, nil
}
if !strings.HasPrefix(lines[0], "dn:") {
return nil, errors.New("missing 'dn:'")
}
_, val, err := l.parseLine(lines[0])
if err != nil {
return nil, err
}
dn := val
if len(lines) == 1 {
return nil, errors.New("only a dn: line")
}
lines = lines[1:]
var controls []ldap.Control
controls, lines, err = l.parseControls(lines)
if err != nil {
return nil, err
}
if strings.HasPrefix(lines[0], "changetype:") {
_, val, err := l.parseLine(lines[0])
if err != nil {
return nil, err
}
l.changeType = val
if len(lines) > 1 {
lines = lines[1:]
}
}
switch l.changeType {
case "":
if len(controls) != 0 {
return nil, errors.New("controls found without changetype")
}
attrs, err := l.parseAttrs(lines)
if err != nil {
return nil, err
}
return &Entry{Entry: ldap.NewEntry(dn, attrs)}, nil
case "add":
attrs, err := l.parseAttrs(lines)
if err != nil {
return nil, err
}
// FIXME: controls for add - see https://github.com/go-ldap/ldap/issues/81
add := ldap.NewAddRequest(dn, controls)
for attr, vals := range attrs {
add.Attribute(attr, vals)
}
return &Entry{Add: add}, nil
case "delete":
if len(lines) > 1 {
return nil, errors.New("no attributes allowed for changetype delete")
}
return &Entry{Del: ldap.NewDelRequest(dn, controls)}, nil
case "modify":
// FIXME: controls for modify - see https://github.com/go-ldap/ldap/issues/81
mod := ldap.NewModifyRequest(dn, controls)
var op, attribute string
var values []string
if lines[len(lines)-1] != "-" {
return nil, errors.New("modify request does not close with a single dash")
}
for i := 0; i < len(lines); i++ {
if lines[i] == "-" {
switch op {
case "":
return nil, fmt.Errorf("empty operation")
case "add":
mod.Add(attribute, values)
op = ""
attribute = ""
values = nil
case "replace":
mod.Replace(attribute, values)
op = ""
attribute = ""
values = nil
case "delete":
mod.Delete(attribute, values)
op = ""
attribute = ""
values = nil
default:
return nil, fmt.Errorf("invalid operation %s in modify request", op)
}
continue
}
attr, val, err := l.parseLine(lines[i])
if err != nil {
return nil, err
}
if op == "" {
op = attr
attribute = val
} else {
if attr != attribute {
return nil, fmt.Errorf("invalid attribute %s in %s request for %s", attr, op, attribute)
}
values = append(values, val)
}
}
return &Entry{Modify: mod}, nil
case "moddn", "modrdn":
return nil, fmt.Errorf("unsupported changetype %s", l.changeType)
default:
return nil, fmt.Errorf("invalid changetype %s", l.changeType)
}
}
func (l *LDIF) parseAttrs(lines []string) (map[string][]string, error) {
attrs := make(map[string][]string)
for i := 0; i < len(lines); i++ {
attr, val, err := l.parseLine(lines[i])
if err != nil {
return nil, err
}
attrs[attr] = append(attrs[attr], val)
}
return attrs, nil
}
func (l *LDIF) parseLine(line string) (attr, val string, err error) {
off := 0
for len(line) > off && line[off] != ':' {
off++
if off >= len(line) {
err = fmt.Errorf("Missing : in line `%s`", line)
return
}
}
if off == len(line) {
err = fmt.Errorf("Missing : in the line `%s`", line)
return
}
if off > len(line)-2 {
err = errors.New("empty value")
// FIXME: this is allowed for some attributes, e.g. seeAlso
return
}
attr = line[0:off]
if err = validAttr(attr); err != nil {
attr = ""
val = ""
return
}
switch line[off+1] {
case ':':
val, err = decodeBase64(strings.TrimLeft(line[off+2:], spaces))
if err != nil {
return
}
case '<':
val, err = readURLValue(strings.TrimLeft(line[off+2:], spaces))
if err != nil {
return
}
default:
val = strings.TrimLeft(line[off+1:], spaces)
}
return
}
func (l *LDIF) parseControls(lines []string) ([]ldap.Control, []string, error) {
var controls []ldap.Control
for {
if !strings.HasPrefix(lines[0], "control:") {
break
}
if !l.Controls {
if len(lines) == 1 {
return nil, nil, errors.New("only controls found")
}
lines = lines[1:]
continue
}
_, val, err := l.parseLine(lines[0])
if err != nil {
return nil, nil, err
}
var oid, ctrlValue string
criticality := false
parts := strings.SplitN(val, " ", 3)
if err = validOID(parts[0]); err != nil {
return nil, nil, fmt.Errorf("%s is not a valid oid: %s", oid, err)
}
oid = parts[0]
if len(parts) > 1 {
switch parts[1] {
case "true":
criticality = true
if len(parts) > 2 {
parts[1] = parts[2]
parts = parts[0:2]
}
case "false":
criticality = false
if len(parts) > 2 {
parts[1] = parts[2]
parts = parts[0:2]
}
}
}
if len(parts) == 2 {
ctrlValue = parts[1]
}
if ctrlValue == "" {
switch oid {
case ldap.ControlTypeManageDsaIT:
controls = append(controls, &ldap.ControlManageDsaIT{Criticality: criticality})
default:
return nil, nil, fmt.Errorf("unsupported control found: %s", oid)
}
} else {
switch ctrlValue[0] { // where is this documented?
case ':':
if len(ctrlValue) == 1 {
return nil, nil, errors.New("missing value for base64 encoded control value")
}
ctrlValue, err = decodeBase64(strings.TrimLeft(ctrlValue[1:], spaces))
if err != nil {
return nil, nil, err
}
if ctrlValue == "" {
return nil, nil, errors.New("base64 decoded to empty value")
}
case '<':
if len(ctrlValue) == 1 {
return nil, nil, errors.New("missing value for url control value")
}
ctrlValue, err = readURLValue(strings.TrimLeft(ctrlValue[1:], spaces))
if err != nil {
return nil, nil, err
}
if ctrlValue == "" {
return nil, nil, errors.New("url resolved to an empty value")
}
}
// TODO:
// convert ctrlValue to *ber.Packet and decode with something like
// ctrl := ldap.DecodeControl()
// ... FIXME: the controls need a Decode() interface
// so we can just do a
// ctrl := ldap.ControlByOID(oid) // returns an empty &ControlSomething{}
// ctrl.Decode((*ber.Packet)(ctrlValue))
// ctrl.Criticality = criticality
// that should be usable in github.com/go-ldap/ldap/control.go also
// to decode the incoming control
// controls = append(controls, ctrl)
return nil, nil, fmt.Errorf("controls with values are not supported, oid: %s", oid)
}
if len(lines) == 1 {
return nil, nil, errors.New("only controls found")
}
lines = lines[1:]
}
return controls, lines, nil
}
func readURLValue(val string) (string, error) {
u, err := url.Parse(val)
if err != nil {
return "", fmt.Errorf("failed to parse URL: %s", err)
}
if u.Scheme != "file" {
return "", fmt.Errorf("unsupported URL scheme %s", u.Scheme)
}
data, err := ioutil.ReadFile(toPath(u))
if err != nil {
return "", fmt.Errorf("failed to read %s: %s", u.Path, err)
}
val = string(data) // FIXME: safe?
return val, nil
}
func decodeBase64(enc string) (string, error) {
dec := make([]byte, base64.StdEncoding.DecodedLen(len([]byte(enc))))
n, err := base64.StdEncoding.Decode(dec, []byte(enc))
if err != nil {
return "", err
}
return string(dec[:n]), nil
}
func validOID(oid string) error {
lastDot := true
for _, c := range oid {
switch {
case c == '.' && lastDot:
return errors.New("OID with at least 2 consecutive dots")
case c == '.':
lastDot = true
case c >= '0' && c <= '9':
lastDot = false
default:
return errors.New("Invalid character in OID")
}
}
return nil
}
func validAttr(attr string) error {
if len(attr) == 0 {
return errors.New("empty attribute name")
}
switch {
case attr[0] >= 'A' && attr[0] <= 'Z':
// A-Z
case attr[0] >= 'a' && attr[0] <= 'z':
// a-z
default:
if attr[0] >= '0' && attr[0] <= '9' {
return validOID(attr)
}
return errors.New("invalid first character in attribute")
}
for i := 1; i < len(attr); i++ {
c := attr[i]
switch {
case c >= '0' && c <= '9':
case c >= 'A' && c <= 'Z':
case c >= 'a' && c <= 'z':
case c == '-':
case c == ';':
default:
return errors.New("invalid character in attribute name")
}
}
return nil
}
// AllEntries returns all *ldap.Entries in the LDIF
func (l *LDIF) AllEntries() (entries []*ldap.Entry) {
for _, entry := range l.Entries {
if entry.Entry != nil {
entries = append(entries, entry.Entry)
}
}
return entries
}