Esempio n. 1
0
def trace_text(what: str,
               sender: Endpoint,
               receiver: Endpoint,
               direction: TransferDirection,
               protocol: TransferProtocol,
               trace_type: str = "Text") -> bool:
    if get_setting(Settings.TRACING) < TRACING_TEXT:
        return False

    _trace(what, sender, receiver, direction, protocol, trace_type=trace_type)
    return True
Esempio n. 2
0
    def _verbose(args: Args) -> AnyErr:
        """ verbose - changes the verbosity level """

        # Increase verbosity (or disable if is already max)
        verbosity = args.get_positional(
            default=(get_setting(Settings.VERBOSITY) + 1) %
            (VERBOSITY_MAX + 1))

        log.i(f">> VERBOSE ({verbosity})")
        set_setting(Settings.VERBOSITY, verbosity)

        return ClientErrors.SUCCESS
Esempio n. 3
0
def trace_json(what: Union[dict, list, tuple],
               sender: Endpoint,
               receiver: Endpoint,
               direction: TransferDirection,
               protocol: TransferProtocol,
               trace_type: str = "JSON") -> bool:
    if get_setting(Settings.TRACING) < TRACING_TEXT:
        return False

    _trace(j(what),
           sender,
           receiver,
           direction,
           protocol,
           trace_type=trace_type)
    return True
Esempio n. 4
0
def trace_bin(what: Union[bytes, bytearray],
              sender: Endpoint,
              receiver: Endpoint,
              direction: TransferDirection,
              protocol: TransferProtocol,
              trace_type: str = "Binary") -> bool:
    if get_setting(Settings.TRACING) < TRACING_BIN:
        return False

    _trace(_hexdump(what),
           sender,
           receiver,
           direction,
           protocol,
           trace_type=trace_type,
           size=len(what))
    return True
Esempio n. 5
0
    def discover(self) -> bool:
        """
        Sends a discover packet and waits for 'discover_timeout' seconds,
        notifying the response_handler in the meanwhile.
        Returns True if the discover finished (timedout) or False if it has been stopped.
        """

        discover_port = get_setting(Settings.DISCOVER_PORT)
        discover_timeout = get_setting(Settings.DISCOVER_WAIT)

        discover_completed = True

        # Listening socket
        in_sock = SocketUdpIn()

        log.i(f"Client discover port: {in_sock.port()}")

        # Send discover
        discover_message = in_sock.port()
        discover_message_b = itob(discover_message, 2)
        out_sock = SocketUdpOut(broadcast=self._discover_addr == ADDR_BROADCAST)

        log.i(f"Sending DISCOVER to {self._discover_addr}:{discover_port}")

        trace_text(
            str(discover_message),
            sender=out_sock.endpoint(), receiver=(self._discover_addr, discover_port),
            direction=TransferDirection.OUT, protocol=TransferProtocol.UDP
        )

        out_sock.send(discover_message_b, self._discover_addr, discover_port,
                      trace=False)

        # Listen
        discover_start_time = datetime.now()

        while True:
            # Calculate remaining time
            remaining_seconds = \
                discover_timeout - (datetime.now() - discover_start_time).total_seconds()

            if remaining_seconds < 0:
                # No more time to wait
                log.i("DISCOVER timeout elapsed (%.3f)", discover_timeout)
                break

            log.i("Waiting for %.3f seconds...", remaining_seconds)

            # Wait for message with select()
            read_fds, write_fds, error_fds = select.select([in_sock.sock], [], [], remaining_seconds)

            if in_sock.sock not in read_fds:
                continue

            # Ready for recv
            log.d("DISCOVER socket ready for recv")
            raw_resp, endpoint = in_sock.recv(trace=False)

            log.i(f"Received DISCOVER response from: {endpoint}")
            resp: ServerInfoFull = cast(ServerInfoFull, btoj(raw_resp))

            trace_json(
                resp,
                sender=in_sock.endpoint(), receiver=endpoint,
                direction=TransferDirection.IN, protocol=TransferProtocol.UDP
            )

            # Dispatch the response and check whether go on on listening
            go_ahead = self._response_handler(endpoint, resp)

            if not go_ahead:
                log.d("Stopping DISCOVER since handle_discover_response_callback returned false")
                discover_completed = False
                break

        log.i("Stopping DISCOVER listener")

        # Close sockets
        in_sock.close()
        out_sock.close()

        return discover_completed
Esempio n. 6
0
def start(args):
    # Already called: easyshare_setup()

    # Parse arguments
    g_args = None

    try:
        g_args = Esd().parse(args)
    except ArgsParseError as err:
        log.eexception("Exception occurred while parsing args")
        abort("parse of global arguments failed: {}".format(str(err)))

    # Eventually set verbosity before anything else
    # so that the rest of the startup (config parsing, ...)
    # can be logged
    if g_args.has_option(Esd.VERBOSE):
        set_setting(
            Settings.VERBOSITY,
            g_args.get_option_param(Esd.VERBOSE, default=VERBOSITY_MAX))

    log.i("{} v. {}".format(APP_NAME_SERVER, APP_VERSION))
    log.i(f"Starting with arguments\n{g_args}")

    # Help?
    if Esd.HELP in g_args:
        _print_usage_and_quit()

    # Version?
    if Esd.VERSION in g_args:
        terminate(APP_INFO)

    # Default values
    verbosity = get_setting(Settings.VERBOSITY)
    tracing = get_setting(Settings.TRACING)
    colors = get_setting(Settings.COLORS)

    server_name = socket.gethostname()
    server_address = None
    server_port = None
    server_discover_port = None
    server_password = None
    server_ssl_enabled = False
    server_ssl_cert = None
    server_ssl_privkey = None
    server_rexec = False

    # Config file

    # Stored as a dict so that consecutive declaration of
    # sharings with the same name will keep only the last one
    sharings: Dict[str, Sharing] = {}

    def add_sharing(path: str, name: str, readonly: bool) -> bool:
        if not path:
            log.w(f"Invalid path for sharing '{name}'; skipping it")
            return False

        sh = Sharing.create(name=name, path=path, read_only=readonly)

        if not sh:
            log.w("Invalid or incomplete sharing config; skipping it")
            return False

        # Use sh.name instead of name so that we will use the autogenerated one
        # if not provided by the user
        log.d(f"Adding sharing for name = {sh.name}")
        sharings[sh.name] = sh

        log.d(
            f"Currently added sharings: \n{j([sh._info_internal() for sh in sharings.values()])}"
        )

        return True

    def add_sharing_or_die(path: str, name: str, readonly: bool):
        if not add_sharing(path=path, name=name, readonly=readonly):
            abort(f"failed to create sharing at '{path}'")

    # Read config file

    if Esd.CONFIG in g_args:
        cfg = None

        try:
            cfg = Conf.parse(path=g_args.get_option_param(Esd.CONFIG),
                             sections_parsers=ESD_CONF_SPEC,
                             comment_prefixes=["#", ";"])
        except ConfParseError as err:
            log.eexception("Exception occurred while parsing conf")
            abort(f"parse of config file failed: {err}")

        if cfg:
            _, global_section = cfg.global_section()

            log.i(f"Config file parsed successfully:\n{cfg}")

            # Config's global settings

            server_name = global_section.get(EsdConfKeys.G_NAME, server_name)

            server_address = global_section.get(EsdConfKeys.G_ADDRESS,
                                                server_address)

            server_port = global_section.get(EsdConfKeys.G_PORT, server_port)

            server_discover_port = global_section.get(
                EsdConfKeys.G_DISCOVER_PORT, server_discover_port)

            server_password = global_section.get(EsdConfKeys.G_PASSWORD,
                                                 server_password)

            server_ssl_cert = global_section.get(EsdConfKeys.G_SSL_CERT,
                                                 server_ssl_cert)
            server_ssl_privkey = global_section.get(EsdConfKeys.G_SSL_PRIVKEY,
                                                    server_ssl_privkey)

            server_ssl_enabled = global_section.get(
                EsdConfKeys.G_SSL, server_ssl_cert and server_ssl_privkey)

            server_rexec = global_section.get(EsdConfKeys.G_REXEC, server_rexec
                                              and server_rexec)

            no_colors = global_section.get(EsdConfKeys.G_NO_COLOR, not colors)

            tracing = global_section.get(EsdConfKeys.G_TRACE, tracing)

            verbosity = global_section.get(EsdConfKeys.G_VERBOSE, verbosity)

            # Config's sharings

            for s_name, s_settings in cfg.non_global_sections():
                s_path = s_settings.get(EsdConfKeys.S_PATH)
                s_readonly = s_settings.get(EsdConfKeys.S_READONLY, False)

                add_sharing_or_die(path=s_path,
                                   name=s_name,
                                   readonly=s_readonly)

    # Args from command line: eventually overwrite config settings

    # Name
    server_name = g_args.get_option_param(Esd.NAME, default=server_name)

    # Server address
    server_address = g_args.get_option_param(Esd.ADDRESS,
                                             default=server_address)

    # Server port
    server_port = g_args.get_option_param(Esd.PORT, default=server_port)

    # Discover port
    server_discover_port = g_args.get_option_param(
        Esd.DISCOVER_PORT, default=server_discover_port)

    # Password
    server_password = g_args.get_option_param(Esd.PASSWORD,
                                              default=server_password)

    # SSL cert
    server_ssl_cert = g_args.get_option_param(Esd.SSL_CERT,
                                              default=server_ssl_cert)

    # SSL privkey
    server_ssl_privkey = g_args.get_option_param(Esd.SSL_PRIVKEY,
                                                 default=server_ssl_privkey)

    # SSL enabled: obviously we need both cert and privkey
    # But for now set True if either one of the two is valid
    # will report errors at the end
    server_ssl_enabled = server_ssl_enabled or server_ssl_cert or server_ssl_privkey

    # Rexec
    if g_args.has_option(Esd.REXEC):
        server_rexec = True

    # Colors
    if g_args.has_option(Esd.NO_COLOR):
        colors = False

    # Packet tracing
    if g_args.has_option(Esd.TRACE):
        # The param of -t is optional:
        # if not specified the default is TEXT
        tracing = g_args.get_option_param(Esd.TRACE, default=TRACING_TEXT)

    # Verbosity
    if g_args.has_option(Esd.VERBOSE):
        # The param of -v is optional:
        # if not specified the default is DEBUG
        verbosity = g_args.get_option_param(Esd.VERBOSE, default=VERBOSITY_MAX)

    # Logging/Tracing/UI setup
    log.d(f"Colors: {colors}")
    log.d(f"Tracing: {tracing}")
    log.d(f"Verbosity: {verbosity}")

    if colors and not is_stdout_terminal():
        log.w("Disabling colors since detected non-terminal output file")
        colors = False

    set_setting(Settings.COLORS, is_styling_supported() and colors)
    set_setting(Settings.TRACING, tracing)
    set_setting(Settings.VERBOSITY, verbosity)

    # Parse sharing arguments
    # We have to be careful since we could find the name/path either
    # in positionals or in the params of "-s"
    # This might come either from
    # 1. Positional arguments (no -s specified), only a sharing can be provided
    # 2. Params of a -s option (multiple sharings can be provided)

    s_unparsed = g_args.get_unparsed_args(default=[])

    try:
        while True:
            s_args = SharingArgs().parse(s_unparsed)
            log.d(f"s_args: {s_args}")
            sharing_params = \
                s_args.get_option_params(SharingArgs.SHARING) or \
                s_args.get_positionals()
            log.d(f"sharing_params: {sharing_params}")

            if not sharing_params:
                break

            add_sharing(
                path=sharing_params[0],
                name=sharing_params[1] if len(sharing_params) >= 2 else None,
                readonly=s_args.get_option_param(SharingArgs.READ_ONLY))

            # Parse the unparsed args (other -s sharing definitions)
            s_unparsed = s_args.get_unparsed_args(default=[])
            log.d(f"s_unparsed: {s_unparsed}")

    except ArgsParseError as err:
        log.eexception("Exception occurred while parsing args")
        abort("Parse of sharing arguments failed: {}".format(str(err)))

    g_args.get_option_params(Esd.SHARING)

    # Validation

    # - server name
    server_name = keepchars(server_name, SERVER_NAME_ALPHABET)
    if not server_name:
        abort("invalid server name")

    # - ports
    for p in [server_port, server_discover_port]:
        # -1 is a special value for disable discovery
        if p and not is_valid_port(p) and p != -1:
            abort("invalid port number {}".format(p))

    # - is a useful server?
    if not sharings and not server_rexec:
        log.e("No sharings found, and rexec disabled; nothing to do")
        abort("provide at least one valid sharing")

    if not sharings:
        log.w("No sharings found, it will be an empty esd")

    # SSL

    ssl_context = None
    if server_ssl_enabled:
        if server_ssl_cert and server_ssl_privkey:
            log.i("Creating SSL context")
            log.i(f"SSL cert path: {server_ssl_cert}")
            log.i(f"SSL privkey path: {server_ssl_privkey}")
            ssl_context = create_server_ssl_context(cert=server_ssl_cert,
                                                    privkey=server_ssl_privkey)
        else:
            if not server_ssl_cert:
                log.w("ssl_cert not specified; SSL will be disabled")
            if not server_ssl_privkey:
                log.w("ssl_privkey not specified; SSL will be disabled")
            server_ssl_enabled = False

    if not server_ssl_enabled or not ssl_context:
        log.w("Server will start in plaintext mode; please consider using SSL")

    set_ssl_context(ssl_context)

    # Auth
    auth = AuthFactory.parse(server_password)

    log.i(f"Required server name: {server_name}")
    log.i(f"Required server address: {server_address}")
    log.i(f"Required server port: {server_port}")
    log.i(f"Required server discover port: {server_discover_port}")
    log.i(f"Required auth: {auth.algo_type()}")

    # Compute real name/port/discover port
    server_name = server_name or socket.gethostname()
    server_address = server_address or get_primary_ip()
    server_port = server_port if server_port is not None else DEFAULT_SERVER_PORT
    server_discover_port = server_discover_port if server_discover_port is not None else DEFAULT_DISCOVER_PORT

    # INIT api daemon
    api_d = ApiDaemon(address=server_address,
                      port=server_port,
                      sharings=list(sharings.values()),
                      name=server_name,
                      auth=AuthFactory.parse(server_password),
                      rexec=server_rexec)

    # build server info
    server_info_full: ServerInfoFull = cast(ServerInfoFull,
                                            api_d.server_info())

    server_info_full["ip"] = api_d.address()
    server_info_full["port"] = api_d.port()

    server_info_full["discoverable"] = True if is_valid_port(
        server_discover_port) else False
    if server_info_full["discoverable"]:
        server_info_full["discover_port"] = server_discover_port

    # INIT discover daemon

    discover_d = None
    if is_valid_port(server_discover_port):
        discover_d = DiscoverDaemon(port=server_discover_port,
                                    trace=True,
                                    server_info=server_info_full)

    log.i(f"ApiDaemon started at {api_d.address()}:{api_d.port()}")

    if discover_d:
        log.i(
            f"DiscoverDaemon started at {discover_d.address()}:{discover_d.port()}"
        )

    if not sharings:
        sharings_str = "NONE"
    else:
        sharings_str = "\n".join([
            f"* {sh.name} --> {sh.path}{'  (readonly)' if sh.read_only else ''}"
            for sh in sharings.values()
        ])

    # PRINT info

    if not auth.algo_security():
        auth_str = "no"
    else:
        auth_str = f"yes ({auth.algo_type()})"

    print(f"""\
================================

{bold("SERVER INFO")}

Name:               {server_name}
Address:            {api_d.address()}
Server port:        {api_d.port()}
Discover port:      {discover_d.port() if discover_d else "disabled"}
Authentication:     {auth_str}
SSL:                {tf(get_ssl_context(), "enabled", "disabled")}
Remote execution:   {tf(server_rexec, "enabled", "disabled")}
Version:            {APP_VERSION}

================================

{bold("SHARINGS")}

{sharings_str}

================================

{bold("RUNNING...")}
""")

    # START daemons

    # - discover
    th_discover = None
    if discover_d:
        th_discover = threading.Thread(target=discover_d.run, daemon=True)

    # - api
    th_api = threading.Thread(target=api_d.run, daemon=True)

    try:
        if th_discover:
            log.i("Starting DISCOVER daemon")
            th_discover.start()
        else:
            # Might be disabled for public server (for which discover won't work anyway)
            log.w("NOT starting DISCOVER daemon")

        log.i("Starting API daemon")
        th_api.start()

        log.i("Ready to handle requests")

        is_running_event.set()
        running_sync.acquire()
        log.d("Semaphore acquired; quitting")

    except KeyboardInterrupt:
        log.d("CTRL+C detected; quitting")

    # log.i("Killing daemons")
    # if discover_d:
    #     discover_d.kill()
    # api_d.kill()

    print("\n" + bold("DONE"))