From 990cd5e48f62fb6ce027ca3de04a7f25c307fc65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matteo=20=E2=84=B1an?= Date: Mon, 21 Sep 2020 00:29:26 +0200 Subject: [PATCH] Support for multi-address connection --- py-kms/pykms_Connect.py | 215 +++++++++++++++++++++++++++++++ py-kms/pykms_Misc.py | 105 ++++++++++++--- py-kms/pykms_Server.py | 275 +++++++++++++++++++++++++++------------- 3 files changed, 493 insertions(+), 102 deletions(-) create mode 100644 py-kms/pykms_Connect.py diff --git a/py-kms/pykms_Connect.py b/py-kms/pykms_Connect.py new file mode 100644 index 0000000..cf8b3dd --- /dev/null +++ b/py-kms/pykms_Connect.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 + +import os +import socket +import selectors +import ipaddress + +# https://github.com/python/cpython/blob/master/Lib/socket.py +def has_dualstack_ipv6(): + """ Return True if the platform supports creating a SOCK_STREAM socket + which can handle both AF_INET and AF_INET6 (IPv4 / IPv6) connections. + """ + if not socket.has_ipv6 or not hasattr(socket._socket, 'IPPROTO_IPV6') or not hasattr(socket._socket, 'IPV6_V6ONLY'): + return False + try: + with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as sock: + sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) + return True + except socket.error: + return False + +def create_server_sock(address, *, family = socket.AF_INET, backlog = None, reuse_port = False, dualstack_ipv6 = False): + """ Convenience function which creates a SOCK_STREAM type socket + bound to *address* (a 2-tuple (host, port)) and return the socket object. + Internally it takes care of choosing the right address family (IPv4 or IPv6),depending on + the host specified in *address* tuple. + + *family* should be either AF_INET or AF_INET6. + *backlog* is the queue size passed to socket.listen(). + *reuse_port* dictates whether to use the SO_REUSEPORT socket option. + *dualstack_ipv6* if True and the platform supports it, it will create an AF_INET6 socket able to accept both IPv4 or IPv6 connections; + when False it will explicitly disable this option on platforms that enable it by default (e.g. Linux). + """ + if reuse_port and not hasattr(socket._socket, "SO_REUSEPORT"): + raise ValueError("SO_REUSEPORT not supported on this platform") + + if dualstack_ipv6: + if not has_dualstack_ipv6(): + raise ValueError("dualstack_ipv6 not supported on this platform") + if family != socket.AF_INET6: + raise ValueError("dualstack_ipv6 requires AF_INET6 family") + + sock = socket.socket(family, socket.SOCK_STREAM) + try: + # Note about Windows. We don't set SO_REUSEADDR because: + # 1) It's unnecessary: bind() will succeed even in case of a + # previous closed socket on the same address and still in + # TIME_WAIT state. + # 2) If set, another socket is free to bind() on the same + # address, effectively preventing this one from accepting + # connections. Also, it may set the process in a state where + # it'll no longer respond to any signals or graceful kills. + # See: msdn2.microsoft.com/en-us/library/ms740621(VS.85).aspx + if os.name not in ('nt', 'cygwin') and hasattr(socket._socket, 'SO_REUSEADDR'): + try: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + except socket.error: + # Fail later on bind(), for platforms which may not + # support this option. + pass + if reuse_port: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + if socket.has_ipv6 and family == socket.AF_INET6: + if dualstack_ipv6: + sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) + elif hasattr(socket._socket, "IPV6_V6ONLY") and hasattr(socket._socket, "IPPROTO_IPV6"): + sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1) + try: + sock.bind(address) + except socket.error as err: + msg = '%s (while attempting to bind on address %r)' %(err.strerror, address) + raise socket.error(err.errno, msg) from None + + if backlog is None: + sock.listen() + else: + sock.listen(backlog) + return sock + except socket.error: + sock.close() + raise + +# Giampaolo Rodola' class (license MIT) revisited for py-kms. +# http://code.activestate.com/recipes/578504-server-supporting-ipv4-and-ipv6/ +class MultipleListener(object): + """ Listen on multiple addresses specified as a list of + (`host`, `port`, `backlog`, `reuse_port`) tuples. + Useful to listen on both IPv4 and IPv6 on those systems where a dual stack + is not supported natively (Windows and many UNIXes). + + Calls like settimeout() and setsockopt() will be applied to all sockets. + Calls like gettimeout() or getsockopt() will refer to the first socket in the list. + """ + def __init__(self, addresses = [], want_dual = False): + self.socks, self.sockmap = [], {} + completed = False + self.cant_dual = [] + + try: + for addr in addresses: + addr = self.check(addr) + ip_ver = ipaddress.ip_address(addr[0]) + + if ip_ver.version == 4 and want_dual: + self.cant_dual.append(addr[0]) + + sock = create_server_sock((addr[0], addr[1]), + family = (socket.AF_INET if ip_ver.version == 4 else socket.AF_INET6), + backlog = addr[2], + reuse_port = addr[3], + dualstack_ipv6 = (False if ip_ver.version == 4 else want_dual)) + self.socks.append(sock) + self.sockmap[sock.fileno()] = sock + + completed = True + finally: + if not completed: + self.close() + + def __enter__(self): + return self + + def __exit__(self): + self.close() + + def __repr__(self): + addrs = [] + for sock in self.socks: + try: + addrs.append(sock.getsockname()) + except socket.error: + addrs.append(()) + return "<%s(%r) at %#x>" %(self.__class__.__name__, addrs, id(self)) + + def filenos(self): + """ Return sockets' file descriptors as a list of integers. """ + return list(self.sockmap.keys()) + + def register(self, pollster): + for fd in self.filenos(): + pollster.register(fileobj = fd, events = selectors.EVENT_READ) + + def multicall(self, name, *args, **kwargs): + for sock in self.socks: + meth = getattr(sock, name) + meth(*args, **kwargs) + + def poll(self): + """ Return the first readable fd. """ + if hasattr(selectors, 'PollSelector'): + pollster = selectors.PollSelector + else: + pollster = selectors.SelectSelector + + timeout = self.gettimeout() + + with pollster() as pollster: + self.register(pollster) + fds = pollster.select(timeout) + + if timeout and fds == []: + raise socket.timeout('timed out') + try: + return fds[0][0].fd + except IndexError: + # non-blocking socket + pass + + def accept(self): + """ Accept a connection from the first socket which is ready to do so. """ + fd = self.poll() + sock = (self.sockmap[fd] if fd else self.socks[0]) + return sock.accept() + + def getsockname(self): + """ Return first registered socket's own address. """ + return self.socks[0].getsockname() + + def getsockopt(self, level, optname, buflen = 0): + """ Return first registered socket's options. """ + return self.socks[0].getsockopt(level, optname, buflen) + + def gettimeout(self): + """ Return first registered socket's timeout. """ + return self.socks[0].gettimeout() + + def settimeout(self, timeout): + """ Set timeout for all registered sockets. """ + self.multicall('settimeout', timeout) + + def setblocking(self, flag): + """ Set non-blocking mode for all registered sockets. """ + self.multicall('setblocking', flag) + + def setsockopt(self, level, optname, value): + """ Set option for all registered sockets. """ + self.multicall('setsockopt', level, optname, value) + + def shutdown(self, how): + """ Shut down all registered sockets. """ + self.multicall('shutdown', how) + + def close(self): + """ Close all registered sockets. """ + self.multicall('close') + self.socks, self.sockmap = [], {} + + def check(self, address): + if len(address) == 1: + raise socket.error("missing `host` or `port` parameter.") + if len(address) == 2: + address += (None, True,) + elif len(address) == 3: + address += (True,) + return address diff --git a/py-kms/pykms_Misc.py b/py-kms/pykms_Misc.py index d9643e0..41a6a5c 100644 --- a/py-kms/pykms_Misc.py +++ b/py-kms/pykms_Misc.py @@ -338,22 +338,32 @@ class KmsParserHelp(object): return help_list def printer(self, parsers): - if len(parsers) == 3: - parser_base, parser_adj, parser_sub = parsers - replace_epilog_with = 80 * '*' + '\n' - elif len(parsers) == 1: - parser_base = parsers[0] + parser_base = parsers[0] + if len(parsers) == 1: replace_epilog_with = '' + else: + parser_adj_0, parser_sub_0 = parsers[1] + replace_epilog_with = 80 * '*' + '\n' + if len(parsers) == 3: + parser_adj_1, parser_sub_1 = parsers[2] + print('\n' + parser_base.description) print(len(parser_base.description) * '-' + '\n') for line in self.replace(parser_base, replace_epilog_with): print(line) - try: - print(parser_adj.description + '\n') - for line in self.replace(parser_sub, replace_epilog_with): + + def subprinter(adj, sub, replace): + print(adj.description + '\n') + for line in self.replace(sub, replace): print(line) - except: - pass + print('\n') + + if len(parsers) >= 2: + subprinter(parser_adj_0, parser_sub_0, replace_epilog_with) + if len(parsers) == 3: + print(replace_epilog_with) + subprinter(parser_adj_1, parser_sub_1, replace_epilog_with) + print('\n' + len(parser_base.epilog) * '-') print(parser_base.epilog + '\n') parser_base.exit() @@ -363,13 +373,13 @@ def kms_parser_get(parser): act = vars(parser)['_actions'] for i in range(len(act)): if act[i].option_strings not in ([], ['-h', '--help']): - if isinstance(act[i], argparse._StoreAction): + if isinstance(act[i], argparse._StoreAction) or isinstance(act[i], argparse._AppendAction): onearg.append(act[i].option_strings) else: zeroarg.append(act[i].option_strings) return zeroarg, onearg -def kms_parser_check_optionals(userarg, zeroarg, onearg, msg = 'optional py-kms server', exclude_opt_len = []): +def kms_parser_check_optionals(userarg, zeroarg, onearg, msg = 'optional py-kms server', exclude_opt_len = [], exclude_opt_dup = []): """ For optionals arguments: Don't allow duplicates, @@ -399,12 +409,13 @@ def kms_parser_check_optionals(userarg, zeroarg, onearg, msg = 'optional py-kms # Check duplicates. founds = [i for i in userarg if i in allarg] dup = [item for item in set(founds) if founds.count(item) > 1] - if dup != []: - raise KmsParserException("%s argument `%s` appears several times" %(msg, ', '.join(dup))) + for d in dup: + if d not in exclude_opt_dup: + raise KmsParserException("%s argument `%s` appears several times" %(msg, ', '.join(dup))) # Check length. elem = None - for found in founds: + for found in set(founds): if found not in exclude_opt_len: pos = userarg.index(found) try: @@ -433,6 +444,70 @@ def kms_parser_check_positionals(config, parse_method, arguments = [], force_par else: raise KmsParserException("unrecognized %s arguments: '%s'" %(msg, e.split(': ')[1])) +def kms_parser_check_connect(config, options, userarg, zeroarg, onearg): + if 'listen' in config: + try: + lung = len(config['listen']) + except TypeError: + raise KmsParserException("optional connect arguments missing") + + rng = range(lung - 1) + config['backlog_primary'] = options['backlog']['def'] + config['reuse_primary'] = options['reuse']['def'] + + def assign(arguments, index, options, config, default, islast = False): + if all(opt not in arguments for opt in options): + if config and islast: + config.append(default) + elif config: + config.insert(index, default) + else: + config.append(default) + + def assign_primary(arguments, config): + if any(opt in arguments for opt in ['-b', '--backlog']): + config['backlog_primary'] = config['backlog'][0] + config['backlog'].pop(0) + if any(opt in arguments for opt in ['-u', '--no-reuse']): + config['reuse_primary'] = config['reuse'][0] + config['reuse'].pop(0) + + if config['listen']: + # check before. + pos = userarg.index(config['listen'][0]) + assign_primary(userarg[1 : pos - 1], config) + + # check middle. + for indx in rng: + pos1 = userarg.index(config['listen'][indx]) + pos2 = userarg.index(config['listen'][indx + 1]) + arguments = userarg[pos1 + 1 : pos2 - 1] + kms_parser_check_optionals(arguments, zeroarg, onearg, msg = 'optional connect') + assign(arguments, indx, ['-b', '--backlog'], config['backlog'], options['backlog']['def']) + assign(arguments, indx, ['-u', '--no-reuse'], config['reuse'], options['reuse']['def']) + + if not arguments: + config['backlog'][indx] = config['backlog_primary'] + config['reuse'][indx] = config['reuse_primary'] + + # check after. + if lung == 1: + indx = -1 + + pos = userarg.index(config['listen'][indx + 1]) + arguments = userarg[pos + 1:] + kms_parser_check_optionals(arguments, zeroarg, onearg, msg = 'optional connect') + assign(arguments, None, ['-b', '--backlog'], config['backlog'], options['backlog']['def'], islast = True) + assign(arguments, None, ['-u', '--no-reuse'], config['reuse'], options['reuse']['def'], islast = True) + + if not arguments: + config['backlog'][indx + 1] = config['backlog_primary'] + config['reuse'][indx + 1] = config['reuse_primary'] + + else: + assign_primary(userarg[1:], config) + + #------------------------------------------------------------------------------------------------------------------------------------------------------------ def proper_none(dictionary): for key in dictionary.keys(): diff --git a/py-kms/pykms_Server.py b/py-kms/pykms_Server.py index 2a1c4c8..8dac940 100755 --- a/py-kms/pykms_Server.py +++ b/py-kms/pykms_Server.py @@ -14,16 +14,16 @@ import socketserver import queue as Queue import selectors from time import monotonic as time -import ipaddress import pykms_RpcBind, pykms_RpcRequest from pykms_RpcBase import rpcBase from pykms_Dcerpc import MSRPCHeader from pykms_Misc import check_setup, check_lcid, check_dir from pykms_Misc import KmsParser, KmsParserException, KmsParserHelp -from pykms_Misc import kms_parser_get, kms_parser_check_optionals, kms_parser_check_positionals -from pykms_Format import enco, deco, pretty_printer +from pykms_Misc import kms_parser_get, kms_parser_check_optionals, kms_parser_check_positionals, kms_parser_check_connect +from pykms_Format import enco, deco, pretty_printer, justify from Etrigan import Etrigan, Etrigan_parser, Etrigan_check, Etrigan_job +from pykms_Connect import MultipleListener srv_version = "py-kms_2020-07-01" __license__ = "The Unlicense" @@ -35,9 +35,8 @@ srv_config = {} ##--------------------------------------------------------------------------------------------------------------------------------------------------------- class KeyServer(socketserver.ThreadingMixIn, socketserver.TCPServer): daemon_threads = True - allow_reuse_address = True - def __init__(self, server_address, RequestHandlerClass, bind_and_activate = True): + def __init__(self, server_address, RequestHandlerClass, bind_and_activate = True, want_dual = False): socketserver.BaseServer.__init__(self, server_address, RequestHandlerClass) self.__shutdown_request = False self.r_service, self.w_service = socket.socketpair() @@ -47,24 +46,27 @@ class KeyServer(socketserver.ThreadingMixIn, socketserver.TCPServer): else: self._ServerSelector = selectors.SelectSelector - try: - ip_ver = ipaddress.ip_address(server_address[0]) - except ValueError as e: - pretty_printer(log_obj = loggersrv.error, to_exit = True, - put_text = "{reverse}{red}{bold}%s. Exiting...{end}" %str(e)) - if ip_ver.version == 4: - self.address_family = socket.AF_INET - elif ip_ver.version == 6: - self.address_family = socket.AF_INET6 - - self.socket = socket.socket(self.address_family, self.socket_type) if bind_and_activate: try: - self.server_bind() - self.server_activate() - except: - self.server_close() - raise + self.multisock = MultipleListener(server_address, want_dual = want_dual) + except Exception as e: + if want_dual and str(e) == "dualstack_ipv6 not supported on this platform": + try: + pretty_printer(log_obj = loggersrv.warning, + put_text = "{reverse}{yellow}{bold}%s. Creating not dualstack sockets...{end}" %str(e)) + self.multisock = MultipleListener(server_address, want_dual = False) + except Exception as e: + pretty_printer(log_obj = loggersrv.error, to_exit = True, + put_text = "{reverse}{red}{bold}%s. Exiting...{end}" %str(e)) + else: + pretty_printer(log_obj = loggersrv.error, to_exit = True, + put_text = "{reverse}{red}{bold}%s. Exiting...{end}" %str(e)) + + if self.multisock.cant_dual: + delim = ('' if len(self.multisock.cant_dual) == 1 else ', ') + pretty_printer(log_obj = loggersrv.warning, + put_text = "{reverse}{yellow}{bold}IPv4 [%s] can't be dualstack{end}" %delim.join(self.multisock.cant_dual)) + def pykms_serve(self): """ Mixing of socketserver serve_forever() and handle_request() functions, @@ -74,7 +76,7 @@ class KeyServer(socketserver.ThreadingMixIn, socketserver.TCPServer): """ # Support people who used socket.settimeout() to escape # pykms_serve() before self.timeout was available. - timeout = self.socket.gettimeout() + timeout = self.multisock.gettimeout() if timeout is None: timeout = self.timeout elif self.timeout is not None: @@ -85,7 +87,7 @@ class KeyServer(socketserver.ThreadingMixIn, socketserver.TCPServer): try: # Wait until a request arrives or the timeout expires. with self._ServerSelector() as selector: - selector.register(fileobj = self, events = selectors.EVENT_READ) + self.multisock.register(selector) # self-pipe trick. selector.register(fileobj = self.r_service.fileno(), events = selectors.EVENT_READ) @@ -101,7 +103,9 @@ class KeyServer(socketserver.ThreadingMixIn, socketserver.TCPServer): return self.handle_timeout() else: for key, mask in ready: - if key.fileobj is self: + if key.fileobj in self.multisock.filenos(): + self.socket = self.multisock.sockmap[key.fileobj] + self.server_address = self.socket.getsockname() self._handle_request_noblock() elif key.fileobj is self.r_service.fileno(): # only to clean buffer. @@ -113,6 +117,9 @@ class KeyServer(socketserver.ThreadingMixIn, socketserver.TCPServer): def shutdown(self): self.__shutdown_request = True + def server_close(self): + self.multisock.close() + def handle_timeout(self): pretty_printer(log_obj = loggersrv.error, to_exit = True, put_text = "{reverse}{red}{bold}Server connection timed out. Exiting...{end}") @@ -176,34 +183,39 @@ loggersrv = logging.getLogger('logsrv') # 'help' string - 'default' value - 'dest' string. srv_options = { - 'ip' : {'help' : 'The IP address (IPv4 or IPv6) to listen on. The default is \"0.0.0.0\" (all interfaces).', 'def' : "0.0.0.0", 'des' : "ip"}, - 'port' : {'help' : 'The network port to listen on. The default is \"1688\".', 'def' : 1688, 'des' : "port"}, - 'epid' : {'help' : 'Use this option to manually specify an ePID to use. If no ePID is specified, a random ePID will be auto generated.', - 'def' : None, 'des' : "epid"}, - 'lcid' : {'help' : 'Use this option to manually specify an LCID for use with randomly generated ePIDs. Default is \"1033\" (en-us)', - 'def' : 1033, 'des' : "lcid"}, - 'count' : {'help' : 'Use this option to specify the current client count. A number >=25 is required to enable activation of client OSes; \ + 'ip' : {'help' : 'The IP address (IPv4 or IPv6) to listen on. The default is \"0.0.0.0\" (all interfaces).', 'def' : "0.0.0.0", 'des' : "ip"}, + 'port' : {'help' : 'The network port to listen on. The default is \"1688\".', 'def' : 1688, 'des' : "port"}, + 'epid' : {'help' : 'Use this option to manually specify an ePID to use. If no ePID is specified, a random ePID will be auto generated.', + 'def' : None, 'des' : "epid"}, + 'lcid' : {'help' : 'Use this option to manually specify an LCID for use with randomly generated ePIDs. Default is \"1033\" (en-us)', + 'def' : 1033, 'des' : "lcid"}, + 'count' : {'help' : 'Use this option to specify the current client count. A number >=25 is required to enable activation of client OSes; \ for server OSes and Office >=5', 'def' : None, 'des' : "clientcount"}, 'activation' : {'help' : 'Use this option to specify the activation interval (in minutes). Default is \"120\" minutes (2 hours).', 'def' : 120, 'des': "activation"}, - 'renewal' : {'help' : 'Use this option to specify the renewal interval (in minutes). Default is \"10080\" minutes (7 days).', - 'def' : 1440 * 7, 'des' : "renewal"}, - 'sql' : {'help' : 'Use this option to store request information from unique clients in an SQLite database. Deactivated by default. \ -If enabled the default .db file is \"pykms_database.db\". You can also provide a specific location.', - 'def' : False, 'des' : "sqlite"}, - 'hwid' : {'help' : 'Use this option to specify a HWID. The HWID must be an 16-character string of hex characters. \ -The default is \"364F463A8863D35F\" or type \"RANDOM\" to auto generate the HWID.', 'def' : "364F463A8863D35F", 'des' : "hwid"}, - 'time0' : {'help' : 'Maximum inactivity time (in seconds) after which the connection with the client is closed. If \"None\" (default) serve forever.', - 'def' : None, 'des' : "timeoutidle"}, - 'asyncmsg' : {'help' : 'Prints pretty / logging messages asynchronously. Deactivated by default.', - 'def' : False, 'des' : "asyncmsg"}, - 'llevel' : {'help' : 'Use this option to set a log level. The default is \"ERROR\".', 'def' : "ERROR", 'des' : "loglevel", - 'choi' : ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "MININFO"]}, - 'lfile' : {'help' : 'Use this option to set an output log file. The default is \"pykms_logserver.log\". \ + 'renewal' : {'help' : 'Use this option to specify the renewal interval (in minutes). Default is \"10080\" minutes (7 days).', + 'def' : 1440 * 7, 'des' : "renewal"}, + 'sql' : {'help' : 'Use this option to store request information from unique clients in an SQLite database. Deactivated by default. \ +If enabled the default .db file is \"pykms_database.db\". You can also provide a specific location.', 'def' : False, 'des' : "sqlite"}, + 'hwid' : {'help' : 'Use this option to specify a HWID. The HWID must be an 16-character string of hex characters. \ +The default is \"364F463A8863D35F\" or type \"RANDOM\" to auto generate the HWID.', + 'def' : "364F463A8863D35F", 'des' : "hwid"}, + 'time0' : {'help' : 'Maximum inactivity time (in seconds) after which the connection with the client is closed. If \"None\" (default) serve forever.', + 'def' : None, 'des' : "timeoutidle"}, + 'asyncmsg' : {'help' : 'Prints pretty / logging messages asynchronously. Deactivated by default.', + 'def' : False, 'des' : "asyncmsg"}, + 'llevel' : {'help' : 'Use this option to set a log level. The default is \"ERROR\".', 'def' : "ERROR", 'des' : "loglevel", + 'choi' : ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "MININFO"]}, + 'lfile' : {'help' : 'Use this option to set an output log file. The default is \"pykms_logserver.log\". \ Type \"STDOUT\" to view log info on stdout. Type \"FILESTDOUT\" to combine previous actions. \ Use \"STDOUTOFF\" to disable stdout messages. Use \"FILEOFF\" if you not want to create logfile.', - 'def' : os.path.join('.', 'pykms_logserver.log'), 'des' : "logfile"}, - 'lsize' : {'help' : 'Use this flag to set a maximum size (in MB) to the output log file. Deactivated by default.', 'def' : 0, 'des': "logsize"}, + 'def' : os.path.join('.', 'pykms_logserver.log'), 'des' : "logfile"}, + 'lsize' : {'help' : 'Use this flag to set a maximum size (in MB) to the output log file. Deactivated by default.', 'def' : 0, 'des': "logsize"}, + 'listen' : {'help' : 'Adds multiple listening address / port couples.', 'des': "listen"}, + 'backlog' : {'help' : 'Specifies the maximum length of the queue of pending connections. Default is \"5\".', 'def' : 5, 'des': "backlog"}, + 'reuse' : {'help' : 'Allows binding / listening to the same address and port. Activated by default.', 'def' : True, 'des': "reuse"}, + 'dual' : {'help' : 'Allows binding / listening to an IPv6 address also accepting connections via IPv4. Deactivated by default.', + 'def' : False, 'des': "dual"} } def server_options(): @@ -237,6 +249,7 @@ def server_options(): server_parser.add_argument("-h", "--help", action = "help", help = "show this help message and exit") + ## Daemon (Etrigan) parsing. daemon_parser = KmsParser(description = "daemon options inherited from Etrigan", add_help = False) daemon_subparser = daemon_parser.add_subparsers(dest = "mode") @@ -245,57 +258,114 @@ def server_options(): help = "Enable py-kms GUI usage.") etrigan_parser = Etrigan_parser(parser = etrigan_parser) + ## Connection parsing. + connection_parser = KmsParser(description = "connect options", add_help = False) + connection_subparser = connection_parser.add_subparsers(dest = "mode") + + connect_parser = connection_subparser.add_parser("connect", add_help = False) + connect_parser.add_argument("-n", "--listen", action = "append", dest = srv_options['listen']['des'], default = [], + help = srv_options['listen']['help'], type = str) + connect_parser.add_argument("-b", "--backlog", action = "append", dest = srv_options['backlog']['des'], default = [], + help = srv_options['backlog']['help'], type = int) + connect_parser.add_argument("-u", "--no-reuse", action = "append_const", dest = srv_options['reuse']['des'], const = False, default = [], + help = srv_options['reuse']['help']) + connect_parser.add_argument("-d", "--dual", action = "store_true", dest = srv_options['dual']['des'], default = srv_options['dual']['def'], + help = srv_options['reuse']['help']) + try: userarg = sys.argv[1:] # Run help. if any(arg in ["-h", "--help"] for arg in userarg): - KmsParserHelp().printer(parsers = [server_parser, daemon_parser, etrigan_parser]) + KmsParserHelp().printer(parsers = [server_parser, (daemon_parser, etrigan_parser), + (connection_parser, connect_parser)]) # Get stored arguments. pykmssrv_zeroarg, pykmssrv_onearg = kms_parser_get(server_parser) etrigan_zeroarg, etrigan_onearg = kms_parser_get(etrigan_parser) - pykmssrv_zeroarg += ['etrigan'] # add subparser + connect_zeroarg, connect_onearg = kms_parser_get(connect_parser) + subpars = ['etrigan', 'connect'] + pykmssrv_zeroarg += subpars # add subparsers - # Set defaults for config. + exclude_kms = ['-F', '--logfile'] + exclude_dup = ['-n', '--listen', '-b', '--backlog', '-u', '--no-reuse'] + + # Set defaults for server dict config. # example case: # python3 pykms_Server.py srv_config.update(vars(server_parser.parse_args([]))) - try: - # Eventually set daemon options for dict server config. - pos = sys.argv[1:].index('etrigan') - # example cases: - # python3 pykms_Server.py etrigan start - # python3 pykms_Server.py etrigan start --daemon_optionals - # python3 pykms_Server.py 1.2.3.4 etrigan start - # python3 pykms_Server.py 1.2.3.4 etrigan start --daemon_optionals - # python3 pykms_Server.py 1.2.3.4 1234 etrigan start - # python3 pykms_Server.py 1.2.3.4 1234 etrigan start --daemon_optionals - # python3 pykms_Server.py --pykms_optionals etrigan start - # python3 pykms_Server.py --pykms_optionals etrigan start --daemon_optionals - # python3 pykms_Server.py 1.2.3.4 --pykms_optionals etrigan start - # python3 pykms_Server.py 1.2.3.4 --pykms_optionals etrigan start --daemon_optionals - # python3 pykms_Server.py 1.2.3.4 1234 --pykms_optionals etrigan start - # python3 pykms_Server.py 1.2.3.4 1234 --pykms_optionals etrigan start --daemon_optionals + if all(pars in userarg for pars in subpars): + ## Set `daemon options` and `connect options` for server dict config. + pos_etr = userarg.index('etrigan') + pos_con = userarg.index('connect') + pos = min(pos_etr, pos_con) - kms_parser_check_optionals(userarg[0:pos], pykmssrv_zeroarg, pykmssrv_onearg, exclude_opt_len = ['-F', '--logfile']) - kms_parser_check_positionals(srv_config, server_parser.parse_args, arguments = userarg[0:pos], force_parse = True) - kms_parser_check_optionals(userarg[pos:], etrigan_zeroarg, etrigan_onearg, msg = 'optional etrigan') - kms_parser_check_positionals(srv_config, daemon_parser.parse_args, arguments = userarg[pos:], msg = 'positional etrigan') + kms_parser_check_optionals(userarg[0 : pos], pykmssrv_zeroarg, pykmssrv_onearg, exclude_opt_len = exclude_kms) + kms_parser_check_positionals(srv_config, server_parser.parse_args, arguments = userarg[0 : pos], force_parse = True) - except ValueError: + if pos == pos_etr: + # example case: + # python3 pykms_Server.py [1.2.3.4] [1234] [--pykms_optionals] etrigan daemon_positional [--daemon_optionals] \ + # connect [--connect_optionals] + kms_parser_check_optionals(userarg[pos : pos_con], etrigan_zeroarg, etrigan_onearg, + msg = 'optional etrigan') + kms_parser_check_positionals(srv_config, daemon_parser.parse_args, arguments = userarg[pos : pos_con], + msg = 'positional etrigan') + kms_parser_check_optionals(userarg[pos_con:], connect_zeroarg, connect_onearg, + msg = 'optional connect', exclude_opt_dup = exclude_dup) + kms_parser_check_positionals(srv_config, connection_parser.parse_args, arguments = userarg[pos_con:], + msg = 'positional connect') + elif pos == pos_con: + # example case: + # python3 pykms_Server.py [1.2.3.4] [1234] [--pykms_optionals] connect [--connect_optionals] etrigan \ + # daemon_positional [--daemon_optionals] + kms_parser_check_optionals(userarg[pos : pos_etr], connect_zeroarg, connect_onearg, + msg = 'optional connect', exclude_opt_dup = exclude_dup) + kms_parser_check_positionals(srv_config, connection_parser.parse_args, arguments = userarg[pos : pos_etr], + msg = 'positional connect') + kms_parser_check_optionals(userarg[pos_etr:], etrigan_zeroarg, etrigan_onearg, + msg = 'optional etrigan') + kms_parser_check_positionals(srv_config, daemon_parser.parse_args, arguments = userarg[pos_etr:], + msg = 'positional etrigan') + + srv_config['mode'] = 'etrigan+connect' + + elif any(pars in userarg for pars in subpars): + if 'etrigan' in userarg: + pos = userarg.index('etrigan') + elif 'connect' in userarg: + pos = userarg.index('connect') + + kms_parser_check_optionals(userarg[0 : pos], pykmssrv_zeroarg, pykmssrv_onearg, exclude_opt_len = exclude_kms) + kms_parser_check_positionals(srv_config, server_parser.parse_args, arguments = userarg[0 : pos], force_parse = True) + + if 'etrigan' in userarg: + ## Set daemon options for server dict config. + # example case: + # python3 pykms_Server.py [1.2.3.4] [1234] [--pykms_optionals] etrigan daemon_positional [--daemon_optionals] + kms_parser_check_optionals(userarg[pos:], etrigan_zeroarg, etrigan_onearg, + msg = 'optional etrigan') + kms_parser_check_positionals(srv_config, daemon_parser.parse_args, arguments = userarg[pos:], + msg = 'positional etrigan') + + elif 'connect' in userarg: + ## Set connect options for server dict config. + # example case: + # python3 pykms_Server.py [1.2.3.4] [1234] [--pykms_optionals] connect [--connect_optionals] + kms_parser_check_optionals(userarg[pos:], connect_zeroarg, connect_onearg, + msg = 'optional connect', exclude_opt_dup = exclude_dup) + kms_parser_check_positionals(srv_config, connection_parser.parse_args, arguments = userarg[pos:], + msg = 'positional connect') + else: # Update pykms options for dict server config. - # example cases: - # python3 pykms_Server.py 1.2.3.4 - # python3 pykms_Server.py 1.2.3.4 --pykms_optionals - # python3 pykms_Server.py 1.2.3.4 1234 - # python3 pykms_Server.py 1.2.3.4 1234 --pykms_optionals - # python3 pykms_Server.py --pykms_optionals - - kms_parser_check_optionals(userarg, pykmssrv_zeroarg, pykmssrv_onearg, exclude_opt_len = ['-F', '--logfile']) + # example case: + # python3 pykms_Server.py [1.2.3.4] [1234] [--pykms_optionals] + kms_parser_check_optionals(userarg, pykmssrv_zeroarg, pykmssrv_onearg, exclude_opt_len = exclude_kms) kms_parser_check_positionals(srv_config, server_parser.parse_args) + kms_parser_check_connect(srv_config, srv_options, userarg, connect_zeroarg, connect_onearg) + except KmsParserException as e: pretty_printer(put_text = "{reverse}{red}{bold}%s. Exiting...{end}" %str(e), to_exit = True) @@ -423,17 +493,48 @@ def server_check(): pretty_printer(log_obj = loggersrv.error, to_exit = True, put_text = "{reverse}{red}{bold}argument `%s`: invalid with: '%s'. Exiting...{end}" %(opt, srv_config[dest])) + # Check further addresses / ports. + if 'listen' in srv_config: + addresses = [] + for elem in srv_config['listen']: + try: + addr, port = elem.split(',') + except ValueError: + pretty_printer(log_obj = loggersrv.error, to_exit = True, + put_text = "{reverse}{red}{bold}argument `-n/--listen`: %s not well defined. Exiting...{end}" %elem) + try: + port = int(port) + except ValueError: + pretty_printer(log_obj = loggersrv.error, to_exit = True, + put_text = "{reverse}{red}{bold}argument `-n/--listen`: port number '%s' is invalid. Exiting...{end}" %port) + + if not (1 <= port <= 65535): + pretty_printer(log_obj = loggersrv.error, to_exit = True, + put_text = "{reverse}{red}{bold}argument `-n/--listen`: port number '%s' is invalid. Enter between 1 - 65535. Exiting...{end}" %port) + + addresses.append((addr, port)) + srv_config['listen'] = addresses + def server_create(): - try: - server = KeyServer((srv_config['ip'], srv_config['port']), kmsServerHandler) - except (socket.gaierror, socket.error) as e: - pretty_printer(log_obj = loggersrv.error, to_exit = True, - put_text = "{reverse}{red}{bold}Connection failed '%s:%d': %s. Exiting...{end}" %(srv_config['ip'], - srv_config['port'], - str(e))) + # Create address list. + all_address = [( + srv_config['ip'], srv_config['port'], + (srv_config['backlog_primary'] if 'backlog_primary' in srv_config else srv_options['backlog']['def']), + (srv_config['reuse_primary'] if 'reuse_primary' in srv_config else srv_options['reuse']['def']) + )] + log_address = "TCP server listening at %s on port %d" %(srv_config['ip'], srv_config['port']) + + if 'listen' in srv_config: + for l, b, r in zip(srv_config['listen'], srv_config['backlog'], srv_config['reuse']): + all_address.append(l + (b,) + (r,)) + log_address += justify("at %s on port %d" %(l[0], l[1]), indent = 56) + + server = KeyServer(all_address, kmsServerHandler, want_dual = (srv_config['dual'] if 'dual' in srv_config else srv_options['dual']['def'])) server.timeout = srv_config['timeoutidle'] - loggersrv.info("TCP server listening at %s on port %d." % (srv_config['ip'], srv_config['port'])) + + loggersrv.info(log_address) loggersrv.info("HWID: %s" % deco(binascii.b2a_hex(srv_config['hwid']), 'utf-8').upper()) + return server def server_terminate(generic_srv, exit_server = False, exit_thread = False):