1# coding: utf-8
2
3# nslcd.py - functions for doing nslcd requests
4#
5# Copyright (C) 2013-2019 Arthur de Jong
6#
7# This library is free software; you can redistribute it and/or
8# modify it under the terms of the GNU Lesser General Public
9# License as published by the Free Software Foundation; either
10# version 2.1 of the License, or (at your option) any later version.
11#
12# This library is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15# Lesser General Public License for more details.
16#
17# You should have received a copy of the GNU Lesser General Public
18# License along with this library; if not, write to the Free Software
19# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
20# 02110-1301 USA
21
22import fcntl
23import os
24import socket
25import struct
26import sys
27
28import constants
29
30
31# definition for reading and writing INT32 values
32_int32 = struct.Struct('!i')
33
34
35class NslcdClient(object):
36
37    def __init__(self, action):
38        # set up the socket (store in class to avoid closing it)
39        self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
40        fcntl.fcntl(self.sock, fcntl.F_SETFD, fcntl.FD_CLOEXEC)
41        # connect to nslcd
42        self.sock.connect(constants.NSLCD_SOCKET)
43        # self.sock.setblocking(1)
44        self.fp = os.fdopen(self.sock.fileno(), 'r+b', 0)
45        # write a request header with a request code
46        self.action = action
47        self.write_int32(constants.NSLCD_VERSION)
48        self.write_int32(action)
49
50    def write(self, value):
51        self.fp.write(value)
52
53    def write_int32(self, value):
54        self.write(_int32.pack(value))
55
56    def write_bytes(self, value):
57        self.write_int32(len(value))
58        if value:
59            self.write(value)
60
61    def write_string(self, value):
62        if sys.version_info[0] >= 3:
63            value = value.encode('utf-8')
64        self.write_bytes(value)
65
66    def write_ether(self, value):
67        value = struct.pack('BBBBBB', *(int(x, 16) for x in value.split(':')))
68        self.write(value)
69
70    def write_address(self, af, value):
71        self.write_int32(af)
72        self.write_bytes(value)
73
74    def read(self, size):
75        value = b''
76        while len(value) < size:
77            data = self.fp.read(size - len(value))
78            if not data:
79                raise IOError('NSLCD protocol cut short')
80            value += data
81        return value
82
83    def read_int32(self):
84        return _int32.unpack(self.read(_int32.size))[0]
85
86    def read_bytes(self):
87        return self.read(self.read_int32())
88
89    def read_string(self):
90        value = self.read_bytes()
91        if sys.version_info[0] >= 3:
92            value = value.decode('utf-8')
93        return value
94
95    def read_stringlist(self):
96        num = self.read_int32()
97        return [self.read_string() for x in range(num)]
98
99    def read_ether(self):
100        value = self.fp.read(6)
101        return ':'.join('%x' % x for x in struct.unpack('6B', value))
102
103    def read_address(self):
104        af = self.read_int32()
105        return af, socket.inet_ntop(af, self.read_bytes())
106
107    def read_addresslist(self):
108        num = self.read_int32()
109        return [self.read_address() for x in range(num)]
110
111    def get_response(self):
112        # complete the request if required and check response header
113        if self.action:
114            # flush the stream
115            self.fp.flush()
116            # read and check response version number
117            if self.read_int32() != constants.NSLCD_VERSION:
118                raise IOError('NSLCD protocol error')
119            if self.read_int32() != self.action:
120                raise IOError('NSLCD protocol error')
121            # reset action to ensure that it is only the first time
122            self.action = None
123        # get the NSLCD_RESULT_* marker and return it
124        return self.read_int32()
125
126    def close(self):
127        if hasattr(self, 'fp'):
128            try:
129                self.fp.close()
130            except IOError:
131                pass
132
133    def __del__(self):
134        self.close()
135
136
137def usermod(username, asroot=False, password=None, args=None):
138    # open a connection to nslcd
139    con = NslcdClient(constants.NSLCD_ACTION_USERMOD)
140    # write the request information
141    con.write_string(username)
142    con.write_int32(1 if asroot else 0)
143    con.write_string(password)
144    for k, v in args.items():
145        con.write_int32(k)
146        con.write_string(v)
147    con.write_int32(constants.NSLCD_USERMOD_END)
148    # read the response
149    if con.get_response() != constants.NSLCD_RESULT_BEGIN:
150        raise IOError('NSLCD protocol error')
151    response = {}
152    while True:
153        key = con.read_int32()
154        if key == constants.NSLCD_USERMOD_END:
155            break
156        response[key] = con.read_string()
157    # return the response
158    return response
159