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
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
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
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
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
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"))