1e2b1b9c0Schristos# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
2e2b1b9c0Schristos#
3*497bf0b8Schristos# SPDX-License-Identifier: MPL-2.0
4*497bf0b8Schristos#
5e2b1b9c0Schristos# This Source Code Form is subject to the terms of the Mozilla Public
6e2b1b9c0Schristos# License, v. 2.0.  If a copy of the MPL was not distributed with this
78260f9a8Schristos# file, you can obtain one at https://mozilla.org/MPL/2.0/.
8e2b1b9c0Schristos#
9e2b1b9c0Schristos# See the COPYRIGHT file distributed with this work for additional
10e2b1b9c0Schristos# information regarding copyright ownership.
11e2b1b9c0Schristos
12e2b1b9c0Schristos############################################################################
13e2b1b9c0Schristos# rndc.py
14e2b1b9c0Schristos# This module implements the RNDC control protocol.
15e2b1b9c0Schristos############################################################################
16e2b1b9c0Schristos
17e2b1b9c0Schristosfrom collections import OrderedDict
18e2b1b9c0Schristosimport time
19e2b1b9c0Schristosimport struct
20e2b1b9c0Schristosimport hashlib
21e2b1b9c0Schristosimport hmac
22e2b1b9c0Schristosimport base64
23e2b1b9c0Schristosimport random
24e2b1b9c0Schristosimport socket
25e2b1b9c0Schristos
26e2b1b9c0Schristos
27e2b1b9c0Schristosclass rndc(object):
28e2b1b9c0Schristos    """RNDC protocol client library"""
29*497bf0b8Schristos
30*497bf0b8Schristos    __algos = {
31*497bf0b8Schristos        "md5": 157,
32*497bf0b8Schristos        "sha1": 161,
33*497bf0b8Schristos        "sha224": 162,
34*497bf0b8Schristos        "sha256": 163,
35*497bf0b8Schristos        "sha384": 164,
36*497bf0b8Schristos        "sha512": 165,
37*497bf0b8Schristos    }
38e2b1b9c0Schristos
39e2b1b9c0Schristos    def __init__(self, host, algo, secret):
40e2b1b9c0Schristos        """Creates a persistent connection to RNDC and logs in
41e2b1b9c0Schristos        host - (ip, port) tuple
42e2b1b9c0Schristos        algo - HMAC algorithm: one of md5, sha1, sha224, sha256, sha384, sha512
43e2b1b9c0Schristos               (with optional prefix 'hmac-')
44e2b1b9c0Schristos        secret - HMAC secret, base64 encoded"""
45e2b1b9c0Schristos        self.host = host
46e2b1b9c0Schristos        algo = algo.lower()
47*497bf0b8Schristos        if algo.startswith("hmac-"):
48e2b1b9c0Schristos            algo = algo[5:]
49e2b1b9c0Schristos        self.algo = algo
50e2b1b9c0Schristos        self.hlalgo = getattr(hashlib, algo)
51e2b1b9c0Schristos        self.secret = base64.b64decode(secret)
52e2b1b9c0Schristos        self.ser = random.randint(0, 1 << 24)
53e2b1b9c0Schristos        self.nonce = None
54e2b1b9c0Schristos        self.__connect_login()
55e2b1b9c0Schristos
56e2b1b9c0Schristos    def call(self, cmd):
57e2b1b9c0Schristos        """Call a RNDC command, all parsing is done on the server side
58e2b1b9c0Schristos        cmd - a complete string with a command (eg 'reload zone example.com')
59e2b1b9c0Schristos        """
60*497bf0b8Schristos        return dict(self.__command(type=cmd)["_data"])
61e2b1b9c0Schristos
62e2b1b9c0Schristos    def __serialize_dict(self, data, ignore_auth=False):
63e2b1b9c0Schristos        rv = bytearray()
64e2b1b9c0Schristos        for k, v in data.items():
65*497bf0b8Schristos            if ignore_auth and k == "_auth":
66e2b1b9c0Schristos                continue
67*497bf0b8Schristos            rv += struct.pack("B", len(k)) + k.encode("ascii")
68e2b1b9c0Schristos            if type(v) == str:
69*497bf0b8Schristos                rv += struct.pack(">BI", 1, len(v)) + v.encode("ascii")
70e2b1b9c0Schristos            elif type(v) == bytes:
71*497bf0b8Schristos                rv += struct.pack(">BI", 1, len(v)) + v
72e2b1b9c0Schristos            elif type(v) == bytearray:
73*497bf0b8Schristos                rv += struct.pack(">BI", 1, len(v)) + v
74e2b1b9c0Schristos            elif type(v) == OrderedDict:
75e2b1b9c0Schristos                sd = self.__serialize_dict(v)
76*497bf0b8Schristos                rv += struct.pack(">BI", 2, len(sd)) + sd
77e2b1b9c0Schristos            else:
78*497bf0b8Schristos                raise NotImplementedError(
79*497bf0b8Schristos                    "Cannot serialize element of type %s" % type(v)
80*497bf0b8Schristos                )
81e2b1b9c0Schristos        return rv
82e2b1b9c0Schristos
83e2b1b9c0Schristos    def __prep_message(self, *args, **kwargs):
84e2b1b9c0Schristos        self.ser += 1
85e2b1b9c0Schristos        now = int(time.time())
86e2b1b9c0Schristos        data = OrderedDict(*args, **kwargs)
87e2b1b9c0Schristos
88e2b1b9c0Schristos        d = OrderedDict()
89*497bf0b8Schristos        d["_auth"] = OrderedDict()
90*497bf0b8Schristos        d["_ctrl"] = OrderedDict()
91*497bf0b8Schristos        d["_ctrl"]["_ser"] = str(self.ser)
92*497bf0b8Schristos        d["_ctrl"]["_tim"] = str(now)
93*497bf0b8Schristos        d["_ctrl"]["_exp"] = str(now + 60)
94e2b1b9c0Schristos        if self.nonce is not None:
95*497bf0b8Schristos            d["_ctrl"]["_nonce"] = self.nonce
96*497bf0b8Schristos        d["_data"] = data
97e2b1b9c0Schristos
98e2b1b9c0Schristos        msg = self.__serialize_dict(d, ignore_auth=True)
99e2b1b9c0Schristos        hash = hmac.new(self.secret, msg, self.hlalgo).digest()
100e2b1b9c0Schristos        bhash = base64.b64encode(hash)
101*497bf0b8Schristos        if self.algo == "md5":
102*497bf0b8Schristos            d["_auth"]["hmd5"] = struct.pack("22s", bhash)
103e2b1b9c0Schristos        else:
104*497bf0b8Schristos            d["_auth"]["hsha"] = bytearray(
105*497bf0b8Schristos                struct.pack("B88s", self.__algos[self.algo], bhash)
106*497bf0b8Schristos            )
107e2b1b9c0Schristos        msg = self.__serialize_dict(d)
108*497bf0b8Schristos        msg = struct.pack(">II", len(msg) + 4, 1) + msg
109e2b1b9c0Schristos        return msg
110e2b1b9c0Schristos
111e2b1b9c0Schristos    def __verify_msg(self, msg):
112*497bf0b8Schristos        if self.nonce is not None and msg["_ctrl"]["_nonce"] != self.nonce:
113e2b1b9c0Schristos            return False
114*497bf0b8Schristos        if self.algo == "md5":
115*497bf0b8Schristos            bhash = msg["_auth"]["hmd5"]
116e2b1b9c0Schristos        else:
117*497bf0b8Schristos            bhash = msg["_auth"]["hsha"][1:]
118e2b1b9c0Schristos        if type(bhash) == bytes:
119*497bf0b8Schristos            bhash = bhash.decode("ascii")
120*497bf0b8Schristos        bhash += "=" * (4 - (len(bhash) % 4))
121e2b1b9c0Schristos        remote_hash = base64.b64decode(bhash)
122e2b1b9c0Schristos        my_msg = self.__serialize_dict(msg, ignore_auth=True)
123e2b1b9c0Schristos        my_hash = hmac.new(self.secret, my_msg, self.hlalgo).digest()
124*497bf0b8Schristos        return my_hash == remote_hash
125e2b1b9c0Schristos
126e2b1b9c0Schristos    def __command(self, *args, **kwargs):
127e2b1b9c0Schristos        msg = self.__prep_message(*args, **kwargs)
128e2b1b9c0Schristos        sent = self.socket.send(msg)
129e2b1b9c0Schristos        if sent != len(msg):
130e2b1b9c0Schristos            raise IOError("Cannot send the message")
131e2b1b9c0Schristos
132e2b1b9c0Schristos        header = self.socket.recv(8)
133e2b1b9c0Schristos        if len(header) != 8:
134e2b1b9c0Schristos            # What should we throw here? Bad auth can cause this...
135e2b1b9c0Schristos            raise IOError("Can't read response header")
136e2b1b9c0Schristos
137*497bf0b8Schristos        length, version = struct.unpack(">II", header)
138e2b1b9c0Schristos        if version != 1:
139*497bf0b8Schristos            raise NotImplementedError("Wrong message version %d" % version)
140e2b1b9c0Schristos
141e2b1b9c0Schristos        # it includes the header
142e2b1b9c0Schristos        length -= 4
143e2b1b9c0Schristos        data = self.socket.recv(length, socket.MSG_WAITALL)
144e2b1b9c0Schristos        if len(data) != length:
145e2b1b9c0Schristos            raise IOError("Can't read response data")
146e2b1b9c0Schristos
147e2b1b9c0Schristos        if type(data) == str:
148e2b1b9c0Schristos            data = bytearray(data)
149e2b1b9c0Schristos        msg = self.__parse_message(data)
150e2b1b9c0Schristos        if not self.__verify_msg(msg):
151e2b1b9c0Schristos            raise IOError("Authentication failure")
152e2b1b9c0Schristos
153e2b1b9c0Schristos        return msg
154e2b1b9c0Schristos
155e2b1b9c0Schristos    def __connect_login(self):
156e2b1b9c0Schristos        self.socket = socket.create_connection(self.host)
157e2b1b9c0Schristos        self.nonce = None
158*497bf0b8Schristos        msg = self.__command(type="null")
159*497bf0b8Schristos        self.nonce = msg["_ctrl"]["_nonce"]
160e2b1b9c0Schristos
161e2b1b9c0Schristos    def __parse_element(self, input):
162e2b1b9c0Schristos        pos = 0
163e2b1b9c0Schristos        labellen = input[pos]
164e2b1b9c0Schristos        pos += 1
165*497bf0b8Schristos        label = input[pos : pos + labellen].decode("ascii")
166e2b1b9c0Schristos        pos += labellen
167e2b1b9c0Schristos        type = input[pos]
168e2b1b9c0Schristos        pos += 1
169*497bf0b8Schristos        datalen = struct.unpack(">I", input[pos : pos + 4])[0]
170e2b1b9c0Schristos        pos += 4
171e2b1b9c0Schristos        data = input[pos : pos + datalen]
172e2b1b9c0Schristos        pos += datalen
173e2b1b9c0Schristos        rest = input[pos:]
174e2b1b9c0Schristos
175e2b1b9c0Schristos        if type == 1:  # raw binary value
176e2b1b9c0Schristos            return label, data, rest
177e2b1b9c0Schristos        elif type == 2:  # dictionary
178e2b1b9c0Schristos            d = OrderedDict()
179e2b1b9c0Schristos            while len(data) > 0:
180e2b1b9c0Schristos                ilabel, value, data = self.__parse_element(data)
181e2b1b9c0Schristos                d[ilabel] = value
182e2b1b9c0Schristos            return label, d, rest
183e2b1b9c0Schristos        # TODO type 3 - list
184e2b1b9c0Schristos        else:
185*497bf0b8Schristos            raise NotImplementedError("Unknown element type %d" % type)
186e2b1b9c0Schristos
187e2b1b9c0Schristos    def __parse_message(self, input):
188e2b1b9c0Schristos        rv = OrderedDict()
189e2b1b9c0Schristos        hdata = None
190e2b1b9c0Schristos        while len(input) > 0:
191e2b1b9c0Schristos            label, value, input = self.__parse_element(input)
192e2b1b9c0Schristos            rv[label] = value
193e2b1b9c0Schristos        return rv
194