/mandos/release

To get this branch, use:
bzr branch http://bzr.recompile.se/loggerhead/mandos/release

« back to all changes in this revision

Viewing changes to mandos

More consistent terminology: Clients are no longer "invalid" - they
are "disabled".  All code and documentation changed to reflect this.

D=Bus API change: The "properties" argument was removed from the
"ClientAdded" signal on interface "se.bsnet.fukt.Mandos".  All code in
both "mandos" and "mandos-monitor" changed to reflect this.

* mandos: Replaced "with closing(F)" with simply "with F" in all
          places where F is a file object.
  (Client.still_valid): Removed.  All callers changed to look at
                        "Client.enabled" instead.
  (dbus_service_property): Check for unsupported signatures with the
                           "byte_arrays" option.
  (DBusObjectWithProperties.Set): - '' -
  (ClientHandler.handle): Use the reverse pipe to receive the
                          "Client.enabled" attribute instead of the
                          now-removed "Client.still_valid()" method.
  (ForkingMixInWithPipe): Renamed to "ForkingMixInWithPipes" (all
                          users changed).  Now also create a reverse
                          pipe for sending data to the child process.
  (ForkingMixInWithPipes.add_pipe): Now takes two pipe fd's as
                                    arguments.  All callers changed.
  (IPv6_TCPServer.handle_ipc): Take an additional "reply_fd" argument
                               (all callers changed).  Close the reply
                               pipe when the child data pipe is
                               closed.  New "GETATTR" IPC method; will
                               pickle client attribute and send it
                               over the reply pipe FD.
  (MandosDBusService.ClientAdded): Removed "properties" argument.  All
                                   emitters changed.
* mandos-clients.conf.xml (DESCRIPTION, OPTIONS): Use
                                                  "enabled/disabled"
                                                  terminology.
* mandos-ctl: Option "--is-valid" renamed to "--is-enabled".
* mandos-monitor: Enable user locale.  Try to log exceptions.
  (MandosClientPropertyCache.__init__): Removed "properties" argument.
                                        All callers changed.
  (UserInterface.add_new_client): Remove "properties" argument.  All
                                  callers changed.  Supply "logger"
                                  argument to MandosClientWidget().
  (UserInterface.add_client): New "logger" argument.  All callers
                              changed.
* mandos.xml (BUGS, SECURITY/CLIENTS): Use "enabled/disabled"
                                       terminology.

Show diffs side-by-side

added added

removed removed

Lines of Context:
55
55
import logging
56
56
import logging.handlers
57
57
import pwd
58
 
from contextlib import closing
 
58
import contextlib
59
59
import struct
60
60
import fcntl
61
61
import functools
 
62
import cPickle as pickle
62
63
 
63
64
import dbus
64
65
import dbus.service
242
243
    enabled:    bool()
243
244
    last_checked_ok: datetime.datetime(); (UTC) or None
244
245
    timeout:    datetime.timedelta(); How long from last_checked_ok
245
 
                                      until this client is invalid
 
246
                                      until this client is disabled
246
247
    interval:   datetime.timedelta(); How often to start a new checker
247
248
    disable_hook:  If set, called by disable() as disable_hook(self)
248
249
    checker:    subprocess.Popen(); a running checker process used
290
291
        if u"secret" in config:
291
292
            self.secret = config[u"secret"].decode(u"base64")
292
293
        elif u"secfile" in config:
293
 
            with closing(open(os.path.expanduser
294
 
                              (os.path.expandvars
295
 
                               (config[u"secfile"])),
296
 
                              "rb")) as secfile:
 
294
            with open(os.path.expanduser(os.path.expandvars
 
295
                                         (config[u"secfile"])),
 
296
                      "rb") as secfile:
297
297
                self.secret = secfile.read()
298
298
        else:
299
299
            raise TypeError(u"No secret or secfile for client %s"
325
325
        self.checker_initiator_tag = (gobject.timeout_add
326
326
                                      (self.interval_milliseconds(),
327
327
                                       self.start_checker))
328
 
        # Also start a new checker *right now*.
329
 
        self.start_checker()
330
328
        # Schedule a disable() when 'timeout' has passed
331
329
        self.disable_initiator_tag = (gobject.timeout_add
332
330
                                   (self.timeout_milliseconds(),
333
331
                                    self.disable))
334
332
        self.enabled = True
 
333
        # Also start a new checker *right now*.
 
334
        self.start_checker()
335
335
    
336
 
    def disable(self):
 
336
    def disable(self, quiet=True):
337
337
        """Disable this client."""
338
338
        if not getattr(self, "enabled", False):
339
339
            return False
340
 
        logger.info(u"Disabling client %s", self.name)
 
340
        if not quiet:
 
341
            logger.info(u"Disabling client %s", self.name)
341
342
        if getattr(self, u"disable_initiator_tag", False):
342
343
            gobject.source_remove(self.disable_initiator_tag)
343
344
            self.disable_initiator_tag = None
395
396
        # client would inevitably timeout, since no checker would get
396
397
        # a chance to run to completion.  If we instead leave running
397
398
        # checkers alone, the checker would have to take more time
398
 
        # than 'timeout' for the client to be declared invalid, which
399
 
        # is as it should be.
 
399
        # than 'timeout' for the client to be disabled, which is as it
 
400
        # should be.
400
401
        
401
402
        # If a checker exists, make sure it is not a zombie
402
403
        try:
467
468
        logger.debug(u"Stopping checker for %(name)s", vars(self))
468
469
        try:
469
470
            os.kill(self.checker.pid, signal.SIGTERM)
470
 
            #os.sleep(0.5)
 
471
            #time.sleep(0.5)
471
472
            #if self.checker.poll() is None:
472
473
            #    os.kill(self.checker.pid, signal.SIGKILL)
473
474
        except OSError, error:
474
475
            if error.errno != errno.ESRCH: # No such process
475
476
                raise
476
477
        self.checker = None
477
 
    
478
 
    def still_valid(self):
479
 
        """Has the timeout not yet passed for this client?"""
480
 
        if not getattr(self, u"enabled", False):
481
 
            return False
482
 
        now = datetime.datetime.utcnow()
483
 
        if self.last_checked_ok is None:
484
 
            return now < (self.created + self.timeout)
485
 
        else:
486
 
            return now < (self.last_checked_ok + self.timeout)
487
478
 
488
479
 
489
480
def dbus_service_property(dbus_interface, signature=u"v",
498
489
    dbus.service.method, except there is only "signature", since the
499
490
    type from Get() and the type sent to Set() is the same.
500
491
    """
 
492
    # Encoding deeply encoded byte arrays is not supported yet by the
 
493
    # "Set" method, so we fail early here:
 
494
    if byte_arrays and signature != u"ay":
 
495
        raise ValueError(u"Byte arrays not supported for non-'ay'"
 
496
                         u" signature %r" % signature)
501
497
    def decorator(func):
502
498
        func._dbus_is_property = True
503
499
        func._dbus_interface = dbus_interface
589
585
        if prop._dbus_access == u"read":
590
586
            raise DBusPropertyAccessException(property_name)
591
587
        if prop._dbus_get_args_options[u"byte_arrays"]:
 
588
            # The byte_arrays option is not supported yet on
 
589
            # signatures other than "ay".
 
590
            if prop._dbus_signature != u"ay":
 
591
                raise ValueError
592
592
            value = dbus.ByteArray(''.join(unichr(byte)
593
593
                                           for byte in value))
594
594
        prop(value)
705
705
                                       variant_level=1))
706
706
        return r
707
707
    
708
 
    def disable(self, signal = True):
 
708
    def disable(self, quiet = False):
709
709
        oldstate = getattr(self, u"enabled", False)
710
 
        r = Client.disable(self)
711
 
        if signal and oldstate != self.enabled:
 
710
        r = Client.disable(self, quiet=quiet)
 
711
        if not quiet and oldstate != self.enabled:
712
712
            # Emit D-Bus signal
713
713
            self.PropertyChanged(dbus.String(u"enabled"),
714
714
                                 dbus.Boolean(False, variant_level=1))
989
989
    def handle(self):
990
990
        logger.info(u"TCP connection from: %s",
991
991
                    unicode(self.client_address))
992
 
        logger.debug(u"IPC Pipe FD: %d", self.server.pipe[1])
 
992
        logger.debug(u"IPC Pipe FD: %d", self.server.child_pipe[1])
993
993
        # Open IPC pipe to parent process
994
 
        with closing(os.fdopen(self.server.pipe[1], u"w", 1)) as ipc:
 
994
        with contextlib.nested(os.fdopen(self.server.child_pipe[1],
 
995
                                         u"w", 1),
 
996
                               os.fdopen(self.server.parent_pipe[0],
 
997
                                         u"r", 0)) as (ipc,
 
998
                                                       ipc_return):
995
999
            session = (gnutls.connection
996
1000
                       .ClientSession(self.request,
997
1001
                                      gnutls.connection
1032
1036
                return
1033
1037
            logger.debug(u"Handshake succeeded")
1034
1038
            try:
1035
 
                fpr = self.fingerprint(self.peer_certificate(session))
1036
 
            except (TypeError, gnutls.errors.GNUTLSError), error:
1037
 
                logger.warning(u"Bad certificate: %s", error)
1038
 
                session.bye()
1039
 
                return
1040
 
            logger.debug(u"Fingerprint: %s", fpr)
1041
 
            
1042
 
            for c in self.server.clients:
1043
 
                if c.fingerprint == fpr:
1044
 
                    client = c
1045
 
                    break
1046
 
            else:
1047
 
                ipc.write(u"NOTFOUND %s %s\n"
1048
 
                          % (fpr, unicode(self.client_address)))
1049
 
                session.bye()
1050
 
                return
1051
 
            # Have to check if client.still_valid(), since it is
1052
 
            # possible that the client timed out while establishing
1053
 
            # the GnuTLS session.
1054
 
            if not client.still_valid():
1055
 
                ipc.write(u"INVALID %s\n" % client.name)
1056
 
                session.bye()
1057
 
                return
1058
 
            ipc.write(u"SENDING %s\n" % client.name)
1059
 
            sent_size = 0
1060
 
            while sent_size < len(client.secret):
1061
 
                sent = session.send(client.secret[sent_size:])
1062
 
                logger.debug(u"Sent: %d, remaining: %d",
1063
 
                             sent, len(client.secret)
1064
 
                             - (sent_size + sent))
1065
 
                sent_size += sent
1066
 
            session.bye()
 
1039
                try:
 
1040
                    fpr = self.fingerprint(self.peer_certificate
 
1041
                                           (session))
 
1042
                except (TypeError, gnutls.errors.GNUTLSError), error:
 
1043
                    logger.warning(u"Bad certificate: %s", error)
 
1044
                    return
 
1045
                logger.debug(u"Fingerprint: %s", fpr)
 
1046
 
 
1047
                for c in self.server.clients:
 
1048
                    if c.fingerprint == fpr:
 
1049
                        client = c
 
1050
                        break
 
1051
                else:
 
1052
                    ipc.write(u"NOTFOUND %s %s\n"
 
1053
                              % (fpr, unicode(self.client_address)))
 
1054
                    return
 
1055
                # Have to check if client.enabled, since it is
 
1056
                # possible that the client was disabled since the
 
1057
                # GnuTLS session was established.
 
1058
                ipc.write(u"GETATTR enabled %s\n" % fpr)
 
1059
                enabled = pickle.load(ipc_return)
 
1060
                if not enabled:
 
1061
                    ipc.write(u"DISABLED %s\n" % client.name)
 
1062
                    return
 
1063
                ipc.write(u"SENDING %s\n" % client.name)
 
1064
                sent_size = 0
 
1065
                while sent_size < len(client.secret):
 
1066
                    sent = session.send(client.secret[sent_size:])
 
1067
                    logger.debug(u"Sent: %d, remaining: %d",
 
1068
                                 sent, len(client.secret)
 
1069
                                 - (sent_size + sent))
 
1070
                    sent_size += sent
 
1071
            finally:
 
1072
                session.bye()
1067
1073
    
1068
1074
    @staticmethod
1069
1075
    def peer_certificate(session):
1129
1135
        return hex_fpr
1130
1136
 
1131
1137
 
1132
 
class ForkingMixInWithPipe(socketserver.ForkingMixIn, object):
1133
 
    """Like socketserver.ForkingMixIn, but also pass a pipe."""
 
1138
class ForkingMixInWithPipes(socketserver.ForkingMixIn, object):
 
1139
    """Like socketserver.ForkingMixIn, but also pass a pipe pair."""
1134
1140
    def process_request(self, request, client_address):
1135
1141
        """Overrides and wraps the original process_request().
1136
1142
        
1137
1143
        This function creates a new pipe in self.pipe
1138
1144
        """
1139
 
        self.pipe = os.pipe()
1140
 
        super(ForkingMixInWithPipe,
 
1145
        self.child_pipe = os.pipe() # Child writes here
 
1146
        self.parent_pipe = os.pipe() # Parent writes here
 
1147
        super(ForkingMixInWithPipes,
1141
1148
              self).process_request(request, client_address)
1142
 
        os.close(self.pipe[1])  # close write end
1143
 
        self.add_pipe(self.pipe[0])
1144
 
    def add_pipe(self, pipe):
 
1149
        # Close unused ends for parent
 
1150
        os.close(self.parent_pipe[0]) # close read end
 
1151
        os.close(self.child_pipe[1])  # close write end
 
1152
        self.add_pipe_fds(self.child_pipe[0], self.parent_pipe[1])
 
1153
    def add_pipe_fds(self, child_pipe_fd, parent_pipe_fd):
1145
1154
        """Dummy function; override as necessary"""
1146
 
        os.close(pipe)
1147
 
 
1148
 
 
1149
 
class IPv6_TCPServer(ForkingMixInWithPipe,
 
1155
        os.close(child_pipe_fd)
 
1156
        os.close(parent_pipe_fd)
 
1157
 
 
1158
 
 
1159
class IPv6_TCPServer(ForkingMixInWithPipes,
1150
1160
                     socketserver.TCPServer, object):
1151
1161
    """IPv6-capable TCP server.  Accepts 'None' as address and/or port
1152
1162
    
1237
1247
            return socketserver.TCPServer.server_activate(self)
1238
1248
    def enable(self):
1239
1249
        self.enabled = True
1240
 
    def add_pipe(self, pipe):
 
1250
    def add_pipe_fds(self, child_pipe_fd, parent_pipe_fd):
1241
1251
        # Call "handle_ipc" for both data and EOF events
1242
 
        gobject.io_add_watch(pipe, gobject.IO_IN | gobject.IO_HUP,
1243
 
                             self.handle_ipc)
1244
 
    def handle_ipc(self, source, condition, file_objects={}):
 
1252
        gobject.io_add_watch(child_pipe_fd,
 
1253
                             gobject.IO_IN | gobject.IO_HUP,
 
1254
                             functools.partial(self.handle_ipc,
 
1255
                                               reply_fd
 
1256
                                               =parent_pipe_fd))
 
1257
    def handle_ipc(self, source, condition, reply_fd=None,
 
1258
                   file_objects={}):
1245
1259
        condition_names = {
1246
1260
            gobject.IO_IN: u"IN",   # There is data to read.
1247
1261
            gobject.IO_OUT: u"OUT", # Data can be written (without
1259
1273
        logger.debug(u"Handling IPC: FD = %d, condition = %s", source,
1260
1274
                     conditions_string)
1261
1275
        
1262
 
        # Turn the pipe file descriptor into a Python file object
 
1276
        # Turn the pipe file descriptors into Python file objects
1263
1277
        if source not in file_objects:
1264
1278
            file_objects[source] = os.fdopen(source, u"r", 1)
 
1279
        if reply_fd not in file_objects:
 
1280
            file_objects[reply_fd] = os.fdopen(reply_fd, u"w", 0)
1265
1281
        
1266
1282
        # Read a line from the file object
1267
1283
        cmdline = file_objects[source].readline()
1268
1284
        if not cmdline:             # Empty line means end of file
1269
 
            # close the IPC pipe
 
1285
            # close the IPC pipes
1270
1286
            file_objects[source].close()
1271
1287
            del file_objects[source]
 
1288
            file_objects[reply_fd].close()
 
1289
            del file_objects[reply_fd]
1272
1290
            
1273
1291
            # Stop calling this function
1274
1292
            return False
1279
1297
        cmd, args = cmdline.rstrip(u"\r\n").split(None, 1)
1280
1298
        
1281
1299
        if cmd == u"NOTFOUND":
1282
 
            logger.warning(u"Client not found for fingerprint: %s",
1283
 
                           args)
 
1300
            fpr, address = args.split(None, 1)
 
1301
            logger.warning(u"Client not found for fingerprint: %s, ad"
 
1302
                           u"dress: %s", fpr, address)
1284
1303
            if self.use_dbus:
1285
1304
                # Emit D-Bus signal
1286
 
                mandos_dbus_service.ClientNotFound(args)
1287
 
        elif cmd == u"INVALID":
 
1305
                mandos_dbus_service.ClientNotFound(fpr, address)
 
1306
        elif cmd == u"DISABLED":
1288
1307
            for client in self.clients:
1289
1308
                if client.name == args:
1290
 
                    logger.warning(u"Client %s is invalid", args)
 
1309
                    logger.warning(u"Client %s is disabled", args)
1291
1310
                    if self.use_dbus:
1292
1311
                        # Emit D-Bus signal
1293
1312
                        client.Rejected()
1294
1313
                    break
1295
1314
            else:
1296
 
                logger.error(u"Unknown client %s is invalid", args)
 
1315
                logger.error(u"Unknown client %s is disabled", args)
1297
1316
        elif cmd == u"SENDING":
1298
1317
            for client in self.clients:
1299
1318
                if client.name == args:
1306
1325
            else:
1307
1326
                logger.error(u"Sending secret to unknown client %s",
1308
1327
                             args)
 
1328
        elif cmd == u"GETATTR":
 
1329
            attr_name, fpr = args.split(None, 1)
 
1330
            for client in self.clients:
 
1331
                if client.fingerprint == fpr:
 
1332
                    attr_value = getattr(client, attr_name, None)
 
1333
                    logger.debug("IPC reply: %r", attr_value)
 
1334
                    pickle.dump(attr_value, file_objects[reply_fd])
 
1335
                    break
 
1336
            else:
 
1337
                logger.error(u"Client %s on address %s requesting "
 
1338
                             u"attribute %s not found", fpr, address,
 
1339
                             attr_name)
 
1340
                pickle.dump(None, file_objects[reply_fd])
1309
1341
        else:
1310
1342
            logger.error(u"Unknown IPC command: %r", cmdline)
1311
1343
        
1345
1377
            elif suffix == u"w":
1346
1378
                delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
1347
1379
            else:
1348
 
                raise ValueError
1349
 
        except (ValueError, IndexError):
1350
 
            raise ValueError
 
1380
                raise ValueError(u"Unknown suffix %r" % suffix)
 
1381
        except (ValueError, IndexError), e:
 
1382
            raise ValueError(e.message)
1351
1383
        timevalue += delta
1352
1384
    return timevalue
1353
1385
 
1366
1398
        def if_nametoindex(interface):
1367
1399
            "Get an interface index the hard way, i.e. using fcntl()"
1368
1400
            SIOCGIFINDEX = 0x8933  # From /usr/include/linux/sockios.h
1369
 
            with closing(socket.socket()) as s:
 
1401
            with contextlib.closing(socket.socket()) as s:
1370
1402
                ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
1371
1403
                                    struct.pack(str(u"16s16x"),
1372
1404
                                                interface))
1566
1598
    bus = dbus.SystemBus()
1567
1599
    # End of Avahi example code
1568
1600
    if use_dbus:
1569
 
        bus_name = dbus.service.BusName(u"se.bsnet.fukt.Mandos", bus)
 
1601
        try:
 
1602
            bus_name = dbus.service.BusName(u"se.bsnet.fukt.Mandos",
 
1603
                                            bus, do_not_queue=True)
 
1604
        except dbus.exceptions.NameExistsException, e:
 
1605
            logger.error(unicode(e) + u", disabling D-Bus")
 
1606
            use_dbus = False
 
1607
            server_settings[u"use_dbus"] = False
 
1608
            tcp_server.use_dbus = False
1570
1609
    protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
1571
1610
    service = AvahiService(name = server_settings[u"servicename"],
1572
1611
                           servicetype = u"_mandos._tcp",
1598
1637
        daemon()
1599
1638
    
1600
1639
    try:
1601
 
        with closing(pidfile):
 
1640
        with pidfile:
1602
1641
            pid = os.getpid()
1603
1642
            pidfile.write(str(pid) + "\n")
1604
1643
        del pidfile
1610
1649
        pass
1611
1650
    del pidfilename
1612
1651
    
1613
 
    def cleanup():
1614
 
        "Cleanup function; run on exit"
1615
 
        service.cleanup()
1616
 
        
1617
 
        while tcp_server.clients:
1618
 
            client = tcp_server.clients.pop()
1619
 
            client.disable_hook = None
1620
 
            client.disable()
1621
 
    
1622
 
    atexit.register(cleanup)
1623
 
    
1624
1652
    if not debug:
1625
1653
        signal.signal(signal.SIGINT, signal.SIG_IGN)
1626
1654
    signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
1633
1661
                dbus.service.Object.__init__(self, bus, u"/")
1634
1662
            _interface = u"se.bsnet.fukt.Mandos"
1635
1663
            
1636
 
            @dbus.service.signal(_interface, signature=u"oa{sv}")
1637
 
            def ClientAdded(self, objpath, properties):
 
1664
            @dbus.service.signal(_interface, signature=u"o")
 
1665
            def ClientAdded(self, objpath):
1638
1666
                "D-Bus signal"
1639
1667
                pass
1640
1668
            
1641
 
            @dbus.service.signal(_interface, signature=u"s")
1642
 
            def ClientNotFound(self, fingerprint):
 
1669
            @dbus.service.signal(_interface, signature=u"ss")
 
1670
            def ClientNotFound(self, fingerprint, address):
1643
1671
                "D-Bus signal"
1644
1672
                pass
1645
1673
            
1671
1699
                        tcp_server.clients.remove(c)
1672
1700
                        c.remove_from_connection()
1673
1701
                        # Don't signal anything except ClientRemoved
1674
 
                        c.disable(signal=False)
 
1702
                        c.disable(quiet=True)
1675
1703
                        # Emit D-Bus signal
1676
1704
                        self.ClientRemoved(object_path, c.name)
1677
1705
                        return
1678
 
                raise KeyError
 
1706
                raise KeyError(object_path)
1679
1707
            
1680
1708
            del _interface
1681
1709
        
1682
1710
        mandos_dbus_service = MandosDBusService()
1683
1711
    
 
1712
    def cleanup():
 
1713
        "Cleanup function; run on exit"
 
1714
        service.cleanup()
 
1715
        
 
1716
        while tcp_server.clients:
 
1717
            client = tcp_server.clients.pop()
 
1718
            if use_dbus:
 
1719
                client.remove_from_connection()
 
1720
            client.disable_hook = None
 
1721
            # Don't signal anything except ClientRemoved
 
1722
            client.disable(quiet=True)
 
1723
            if use_dbus:
 
1724
                # Emit D-Bus signal
 
1725
                mandos_dbus_service.ClientRemoved(client.dbus_object_path,
 
1726
                                                  client.name)
 
1727
    
 
1728
    atexit.register(cleanup)
 
1729
    
1684
1730
    for client in tcp_server.clients:
1685
1731
        if use_dbus:
1686
1732
            # Emit D-Bus signal
1687
 
            mandos_dbus_service.ClientAdded(client.dbus_object_path,
1688
 
                                            client.GetAll(u""))
 
1733
            mandos_dbus_service.ClientAdded(client.dbus_object_path)
1689
1734
        client.enable()
1690
1735
    
1691
1736
    tcp_server.enable()
1709
1754
            service.activate()
1710
1755
        except dbus.exceptions.DBusException, error:
1711
1756
            logger.critical(u"DBusException: %s", error)
 
1757
            cleanup()
1712
1758
            sys.exit(1)
1713
1759
        # End of Avahi example code
1714
1760
        
1721
1767
        main_loop.run()
1722
1768
    except AvahiError, error:
1723
1769
        logger.critical(u"AvahiError: %s", error)
 
1770
        cleanup()
1724
1771
        sys.exit(1)
1725
1772
    except KeyboardInterrupt:
1726
1773
        if debug:
1727
1774
            print >> sys.stderr
1728
1775
        logger.debug(u"Server received KeyboardInterrupt")
1729
1776
    logger.debug(u"Server exiting")
 
1777
    # Must run before the D-Bus bus name gets deregistered
 
1778
    cleanup()
1730
1779
 
1731
1780
if __name__ == '__main__':
1732
1781
    main()