|
|
@ -70,7 +70,7 @@ class NetstringProtocol(asyncio.Protocol):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SocketmapProtocol(NetstringProtocol):
|
|
|
|
class SocketmapProtocol(NetstringProtocol):
|
|
|
|
""" TCP protocol to answer Postfix socketmap and proxify lookups to
|
|
|
|
""" Protocol to answer Postfix socketmap and proxify lookups to
|
|
|
|
an outside object.
|
|
|
|
an outside object.
|
|
|
|
|
|
|
|
|
|
|
|
See http://www.postfix.org/socketmap_table.5.html for details on the
|
|
|
|
See http://www.postfix.org/socketmap_table.5.html for details on the
|
|
|
@ -122,6 +122,79 @@ class SocketmapProtocol(NetstringProtocol):
|
|
|
|
return lambda: cls(table_map)
|
|
|
|
return lambda: cls(table_map)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DictProtocol(asyncio.Protocol):
|
|
|
|
|
|
|
|
""" Protocol to answer Dovecot dict requests, as implemented in Dict proxy.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
There is very little documentation about the protocol, most of it was
|
|
|
|
|
|
|
|
reverse-engineered from :
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
https://github.com/dovecot/core/blob/master/src/dict/dict-connection.c
|
|
|
|
|
|
|
|
https://github.com/dovecot/core/blob/master/src/dict/dict-commands.c
|
|
|
|
|
|
|
|
https://github.com/dovecot/core/blob/master/src/lib-dict/dict-client.h
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
DATA_TYPES = {0: str, 1: int}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, table_map):
|
|
|
|
|
|
|
|
self.table_map = table_map
|
|
|
|
|
|
|
|
self.major_version = None
|
|
|
|
|
|
|
|
self.minor_version = None
|
|
|
|
|
|
|
|
self.dict = None
|
|
|
|
|
|
|
|
super(DictProtocol, self).__init__()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def connection_made(self, transport):
|
|
|
|
|
|
|
|
logging.info('Connect {}'.format(transport.get_extra_info('peername')))
|
|
|
|
|
|
|
|
self.transport = transport
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def data_received(self, data):
|
|
|
|
|
|
|
|
logging.debug("Received {}".format(data))
|
|
|
|
|
|
|
|
for line in data.split(b"\n"):
|
|
|
|
|
|
|
|
if len(line) < 2:
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
command = DictProtocol.COMMANDS.get(line[0])
|
|
|
|
|
|
|
|
if command is None:
|
|
|
|
|
|
|
|
logging.warning('Unknown command {}'.format(line[0]))
|
|
|
|
|
|
|
|
return self.transport.abort()
|
|
|
|
|
|
|
|
args = line[1:].strip().split(b"\t")
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
command(self, *args)
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
|
|
logging.exception("Error when processing request")
|
|
|
|
|
|
|
|
return self.transport.abort()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def process_hello(self, major, minor, value_type, user, dict_name):
|
|
|
|
|
|
|
|
self.major, self.minor = int(major), int(minor)
|
|
|
|
|
|
|
|
logging.debug('Client version {}.{}'.format(self.major, self.minor))
|
|
|
|
|
|
|
|
assert self.major == 2
|
|
|
|
|
|
|
|
self.value_type = DictProtocol.DATA_TYPES[int(value_type)]
|
|
|
|
|
|
|
|
self.user = user
|
|
|
|
|
|
|
|
self.dict = self.table_map[dict_name.decode("ascii")]
|
|
|
|
|
|
|
|
logging.debug("Value type {}, user {}, dict {}".format(
|
|
|
|
|
|
|
|
self.value_type, self.user, dict_name))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def process_lookup(self, key):
|
|
|
|
|
|
|
|
logging.debug("Looking up {}".format(key))
|
|
|
|
|
|
|
|
self.reply(b"O", json.dumps({}))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def reply(self, command, *args):
|
|
|
|
|
|
|
|
logging.debug("Replying {} with {}".format(command, args))
|
|
|
|
|
|
|
|
self.transport.write(command)
|
|
|
|
|
|
|
|
for arg in args:
|
|
|
|
|
|
|
|
self.transport.write("b\t" + arg.replace(b"\t", b"\t\t"))
|
|
|
|
|
|
|
|
self.transport.write("\n")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
|
|
def factory(cls, table_map):
|
|
|
|
|
|
|
|
""" Provide a protocol factory for a given map instance.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
return lambda: cls(table_map)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
COMMANDS = {
|
|
|
|
|
|
|
|
ord("H"): process_hello,
|
|
|
|
|
|
|
|
ord("L"): process_lookup
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UrlTable(object):
|
|
|
|
class UrlTable(object):
|
|
|
|
""" Resolve an entry by querying a parametrized GET URL.
|
|
|
|
""" Resolve an entry by querying a parametrized GET URL.
|
|
|
|
"""
|
|
|
|
"""
|
|
|
@ -144,23 +217,26 @@ class UrlTable(object):
|
|
|
|
def main():
|
|
|
|
def main():
|
|
|
|
""" Run the asyncio loop.
|
|
|
|
""" Run the asyncio loop.
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
parser = argparse.ArgumentParser("Postfix Socketmap proxy")
|
|
|
|
# Reference tables
|
|
|
|
parser.add_argument("--bind", help="address to bind to", required=True)
|
|
|
|
server_types = dict(postfix=SocketmapProtocol, dovecot=DictProtocol)
|
|
|
|
parser.add_argument("--port", type=int, help="port to bind to", required=True)
|
|
|
|
table_types = dict(url=UrlTable)
|
|
|
|
|
|
|
|
# Argument parsing
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser("Postfix and Dovecot map proxy")
|
|
|
|
|
|
|
|
parser.add_argument("--socket", help="path to a socket", required=True)
|
|
|
|
|
|
|
|
parser.add_argument("--mode", choices=server_types.keys(), required=True)
|
|
|
|
parser.add_argument("--name", help="name of the table", action="append")
|
|
|
|
parser.add_argument("--name", help="name of the table", action="append")
|
|
|
|
parser.add_argument("--type", help="type of the table", action="append")
|
|
|
|
parser.add_argument("--type", choices=table_types.keys(), action="append")
|
|
|
|
parser.add_argument("--param", help="table parameter", action="append")
|
|
|
|
parser.add_argument("--param", help="table parameter", action="append")
|
|
|
|
args = parser.parse_args()
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Prepare the maps
|
|
|
|
# Prepare the maps
|
|
|
|
table_types = dict(url=UrlTable)
|
|
|
|
|
|
|
|
table_map = {name: table_types[table_type](param)
|
|
|
|
table_map = {name: table_types[table_type](param)
|
|
|
|
for name, table_type, param
|
|
|
|
for name, table_type, param
|
|
|
|
in zip(args.name, args.type, args.param)}
|
|
|
|
in zip(args.name, args.type, args.param)} if args.name else {}
|
|
|
|
# Run the main loop
|
|
|
|
# Run the main loop
|
|
|
|
logging.basicConfig(level=logging.DEBUG)
|
|
|
|
logging.basicConfig(level=logging.DEBUG)
|
|
|
|
loop = asyncio.get_event_loop()
|
|
|
|
loop = asyncio.get_event_loop()
|
|
|
|
server = loop.run_until_complete(loop.create_server(
|
|
|
|
server = loop.run_until_complete(loop.create_unix_server(
|
|
|
|
SocketmapProtocol.factory(table_map), args.bind, args.port
|
|
|
|
server_types[args.mode].factory(table_map), args.socket
|
|
|
|
))
|
|
|
|
))
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
loop.run_forever()
|
|
|
|
loop.run_forever()
|
|
|
|