diff --git a/dovecot/dict.go b/dovecot/dict.go index 50835edd28283dac6ee062b25551095e98ddb6d1..320b9e316d7929966f2321cac49d7d4605b64ee7 100644 --- a/dovecot/dict.go +++ b/dovecot/dict.go @@ -17,7 +17,10 @@ var ( noMatchResponse = []byte{'N', '\n'} ) -const supportedDictProtocolVersion = 2 +const ( + supportedDictProtocolVersionMin = 2 + supportedDictProtocolVersionMax = 3 +) // DictDatabase is an interface to a key/value store by way of the Lookup // method. @@ -29,7 +32,7 @@ type DictDatabase interface { } // DictProxyServer exposes a Database using the Dovecot dict proxy -// protocol (see https://wiki2.dovecot.org/AuthDatabase/Dict). +// protocol (see https://doc.dovecot.org/developer_manual/design/dict_protocol/). // // It implements the unix.LineHandler interface from the // ai3/go-common/unix package. @@ -59,13 +62,13 @@ func (p *DictProxyServer) ServeLine(ctx context.Context, lw unix.LineResponseWri } func (p *DictProxyServer) handleHello(ctx context.Context, lw unix.LineResponseWriter, arg []byte) error { - fields := bytes.Split(arg, []byte{'\t'}) + fields := splitFields(arg) if len(fields) < 1 { return errors.New("could not parse HELLO") } majorVersion, _ := strconv.Atoi(string(fields[0])) - if majorVersion != supportedDictProtocolVersion { + if majorVersion < supportedDictProtocolVersionMin || majorVersion > supportedDictProtocolVersionMax { return fmt.Errorf("unsupported protocol version %d", majorVersion) } @@ -73,7 +76,15 @@ func (p *DictProxyServer) handleHello(ctx context.Context, lw unix.LineResponseW } func (p *DictProxyServer) handleLookup(ctx context.Context, lw unix.LineResponseWriter, arg []byte) error { - obj, ok, err := p.db.Lookup(ctx, string(arg)) + // Support protocol versions 2 and 3 by looking for the \t + // field separator, which should not appear in the key in + // version 2 anyway. + fields := splitFields(arg) + if len(fields) < 1 { + return errors.New("could not parse LOOKUP") + } + + obj, ok, err := p.db.Lookup(ctx, string(fields[0])) if err != nil { log.Printf("error: %v", err) return lw.WriteLine(failResponse) @@ -91,3 +102,41 @@ func (p *DictProxyServer) handleLookup(ctx context.Context, lw unix.LineResponse //buf.Write([]byte{'\n'}) return lw.WriteLine(buf.Bytes()) } + +var dovecotEscapeChars = map[byte]byte{ + '0': 0, + '1': 1, + 't': '\t', + 'r': '\r', + 'l': '\n', +} + +var fieldSeparator = []byte{'\t'} + +func unescapeInplace(b []byte) []byte { + var esc bool + var j int + for i := 0; i < len(b); i++ { + c := b[i] + if esc { + if escC, ok := dovecotEscapeChars[c]; ok { + c = escC + } + esc = false + } else if c == '\001' { + esc = true + continue + } + b[j] = c + j++ + } + return b[:j] +} + +func splitFields(b []byte) [][]byte { + fields := bytes.Split(b, fieldSeparator) + for i := 0; i < len(fields); i++ { + fields[i] = unescapeInplace(fields[i]) + } + return fields +} diff --git a/dovecot/dict_test.go b/dovecot/dict_test.go new file mode 100644 index 0000000000000000000000000000000000000000..e9800ae4f6344f8774c2136c4890474d16f73f94 --- /dev/null +++ b/dovecot/dict_test.go @@ -0,0 +1,23 @@ +package dovecot + +import ( + "testing" +) + +func TestUnescape(t *testing.T) { + for _, td := range []struct { + input, exp string + }{ + {"boo", "boo"}, + {"bo\001t", "bo\t"}, + {"bo\001t\001l", "bo\t\n"}, + {"bo\001t\0011", "bo\t\001"}, + } { + out := make([]byte, len(td.input)) + copy(out, td.input) + out = unescapeInplace(out) + if string(out) != td.exp { + t.Errorf("unescape('%s') returned '%s', expected '%s'", td.input, out, td.exp) + } + } +}