Initial QSfera import
This commit is contained in:
+534
@@ -0,0 +1,534 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user