def load_custom_icons(names): """ Load custom icon theme if one is selected """ if not config.sections["ui"].get("icontheme"): return False icon_theme_path = config.sections["ui"]["icontheme"] log.add_debug("Loading custom icon theme from %s", icon_theme_path) extensions = ["jpg", "jpeg", "bmp", "png", "svg"] for name in names: path = None exts = extensions[:] loaded = False while not path or (exts and not loaded): path = os.path.expanduser( os.path.join(icon_theme_path, "%s.%s" % (name, exts.pop()))) try: if os.path.isfile(path): ICONS[name] = Gio.Icon.new_for_string(path) loaded = True except Exception as error: log.add(_("Error loading custom icon %(path)s: %(error)s"), { "path": path, "error": error }) if name not in ICONS: ICONS[name] = load_ui_icon(name) return True
def write_configuration(self): if not self.config_loaded: return # Write new config options to file for section, options in self.sections.items(): if not self.parser.has_section(section): self.parser.add_section(section) for option, value in options.items(): self.parser.set(section, option, str(value)) # Remove legacy config options for section, options in self.removed_options.items(): if not self.parser.has_section(section): continue for option in options: self.parser.remove_option(section, option) if not self.create_config_folder(): return from pynicotine.logfacility import log from pynicotine.utils import write_file_and_backup write_file_and_backup(self.filename, self.write_config_callback, protect=True) log.add_debug("Saved configuration: %(file)s", {"file": self.filename})
def check_icon_path(icon_name, icon_path, icon_type="local"): """ Check if tray icons exist in the specified icon path. There are two naming schemes for tray icons: - System-wide/local icons: "org.nicotine_plus.Nicotine-<icon_name>" - Custom icons: "trayicon_<icon_name>" """ if icon_type == "local": icon_scheme = config.application_id + "-" + icon_name + "." else: icon_scheme = "trayicon_" + icon_name + "." try: for entry in os.scandir(icon_path): if entry.is_file() and entry.name.startswith(icon_scheme): return True except OSError as error: log.add_debug( "Error accessing %(type)s tray icon path %(path)s: %(error)s" % { "type": icon_type, "path": icon_path, "error": error }) return False
def add_port_mapping(self, np): """ This function supports creating a Port Mapping via the UPnP IGDv1 and IGDv2 protocol. Need a reference to the np object to extract the internal LAN local from the protothread socket. Any UPnP port mapping done with IGDv2 will expire after a maximum of 7 days (lease period), according to the protocol. We set the lease period to a shorter 24 hours, and regularly renew the port mapping (see pynicotine.py). """ try: self._add_port_mapping(np) except Exception as error: log.add(_('UPnP exception: %(error)s'), {'error': error}) log.add( _('Failed to automate the creation of UPnP Port Mapping rule.') ) return log.add_debug( _('Managed to map external WAN port %(externalwanport)s ' + 'to your local host %(internalipaddress)s ' + 'port %(internallanport)s.'), { 'externalwanport': self.externalwanport, 'internalipaddress': self.internalipaddress, 'internallanport': self.internallanport })
def __init__(self, core, config): self.core = core self.config = config log.add_debug("Loading plugin handler") self.my_username = self.config.sections["server"]["login"] self.plugindirs = [] self.enabled_plugins = {} try: os.makedirs(config.plugin_dir) except Exception: pass # Load system-wide plugins prefix = os.path.dirname(os.path.realpath(__file__)) self.plugindirs.append(os.path.join(prefix, "plugins")) # Load home directory plugins self.plugindirs.append(config.plugin_dir) if os.path.isdir(config.plugin_dir): self.load_enabled() else: log.add( _("It appears '%s' is not a directory, not loading plugins."), config.plugin_dir)
def delete_port_mapping(cls, router, protocol, public_port): """ Deletes a port mapping from a router """ log.add_debug("Deleting port mapping (%s, %s, %s)", (router, protocol, public_port)) url = '{}{}'.format(router.base_url, router.control_url) log.add_debug('Deleting port mapping (%s/%s) at url "%s"', (public_port, protocol, url)) headers = { 'Host': '{}:{}'.format(router.ip, router.port), 'Content-Type': 'text/xml; charset=utf-8', 'SOAPACTION': '"{}#DeletePortMapping"'.format(router.svc_type) } data = UPnp._delete_port_mapping_template.format(public_port, protocol) log.add_debug('UPnP: Delete port mapping request headers: %s', headers) log.add_debug('UPnP: Delete port mapping request contents: %s', data) response = http_request(router.url_scheme, router.base_url, router.control_url, request_type="POST", body=data, headers=headers) log.add_debug('UPnP: Delete port mapping response: %s', response.encode('utf-8'))
def plugin_settings(self, plugin_name, plugin): plugin_name = plugin_name.lower() try: if not plugin.settings: return if plugin_name not in self.config.sections["plugins"]: self.config.sections["plugins"][plugin_name] = plugin.settings for i in plugin.settings: if i not in self.config.sections["plugins"][plugin_name]: self.config.sections["plugins"][plugin_name][ i] = plugin.settings[i] customsettings = self.config.sections["plugins"][plugin_name] for key in customsettings: if key in plugin.settings: plugin.settings[key] = customsettings[key] else: log.add_debug( "Stored setting '%(key)s' is no longer present in the '%(name)s' plugin", { 'key': key, 'name': plugin_name }) except KeyError: log.add_debug("No stored settings found for %s", plugin.human_name)
def ok_to_respond(self, room, nick, request, seconds_limit_min=30): self.room = room self.nick = nick self.request = request willing_to_respond = True current_time = time() if room not in self.plugin_usage: self.plugin_usage[room] = {'last_time': 0, 'last_request': "", 'last_nick': ""} last_time = self.plugin_usage[room]['last_time'] last_nick = self.plugin_usage[room]['last_nick'] last_request = self.plugin_usage[room]['last_request'] port = False try: ip, port = self.frame.np.users[nick].addr except Exception: port = True if nick in self.frame.np.config.sections["server"]["ignorelist"]: willing_to_respond, reason = False, "The nick is ignored" elif self.frame.user_ip_is_ignored(nick): willing_to_respond, reason = False, "The nick's Ip is ignored" elif not port: willing_to_respond, reason = False, "Request likely from simple PHP based griefer bot" elif [nick, request] == [last_nick, last_request]: if (current_time - last_time) < 12 * seconds_limit_min: willing_to_respond, reason = False, "Too soon for same nick to request same resource in room" elif (request == last_request): if (current_time - last_time) < 3 * seconds_limit_min: willing_to_respond, reason = False, "Too soon for different nick to request same resource in room" else: recent_responses = 0 for responded_room in self.plugin_usage: if (current_time - self.plugin_usage[responded_room]['last_time']) < seconds_limit_min: recent_responses += 1 if responded_room == room: willing_to_respond, reason = False, "Responded in specified room too recently" break if recent_responses > 3: willing_to_respond, reason = False, "Responded in multiple rooms enough" if self.logging: if not willing_to_respond: base_log_msg = "{} plugin request rejected - room '{}', nick '{}'".format(self.plugin_name, room, nick) log.add_debug("{} - {}".format(base_log_msg, reason)) return willing_to_respond
def trigger_event(self, function_name, args): """ Triggers an event for the plugins. Since events and notifications are precisely the same except for how n+ responds to them, both can be triggered by this function. """ function_name_camelcase = function_name.title().replace('_', '') for module, plugin in self.enabled_plugins.items(): try: if hasattr(plugin, function_name_camelcase): plugin.log( "%(old_function)s is deprecated, please use %(new_function)s" % { "old_function": function_name_camelcase, "new_function": function_name }) return_value = getattr(plugin, function_name_camelcase)(*args) else: return_value = getattr(plugin, function_name)(*args) except Exception: self.show_plugin_error(module, sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2]) continue if return_value is None: # Nothing changed, continue to the next plugin continue if isinstance(return_value, tuple): # The original args were modified, update them args = return_value continue if return_value == returncode['zap']: return None if return_value == returncode['break']: return args if return_value == returncode['pass']: continue log.add_debug( "Plugin %(module)s returned something weird, '%(value)s', ignoring", { 'module': module, 'value': return_value }) return args
def _add_port_mapping(self, np): """ Function that actually creates the port mapping. If a port mapping already exists, it is updated with a lease period of 24 hours. """ log.add_debug('Creating Port Mapping rule via UPnP...') # Find router router = UPnp.find_router() if not router: raise RuntimeError('UPnP does not work on this network') # Create a UDP socket s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # Send a broadcast packet on a local address (doesn't need to be reachable, but MacOS requires port to be non-zero) s.connect(('10.255.255.255', 1)) # This returns the "primary" IP on the local box, even if that IP is a NAT/private/internal IP. self.internalipaddress = s.getsockname()[0] # Close the socket s.close() # Store the Local LAN port self.internallanport = np.protothread._p.getsockname()[1] self.externalwanport = self.internallanport # Do the port mapping log.add_debug('Trying to redirect external WAN port %s TCP => %s port %s TCP', ( self.externalwanport, self.internalipaddress, self.internallanport )) try: UPnp.add_port_mapping( router=router, protocol='TCP', public_port=self.externalwanport, private_ip=self.internalipaddress, private_port=self.internallanport, mapping_description='Nicotine+', lease_duration=86400 # Expires in 24 hours ) except Exception as e: raise RuntimeError( _('Failed to map the external WAN port: %(error)s') % {'error': str(e)} )
def create_search_result_list(self, searchterm, wordindex, excluded_words, partial_words): """ Returns a list of common file indices for each word in a search term """ try: words = searchterm.split() original_length = len(words) results = None i = 0 while i < len(words): word = words[i] exclude_word = False i += 1 if word in excluded_words: # Excluded search words (e.g. -hello) if results is None and i < original_length: # Re-append the word so we can re-process it once we've found a match words.append(word) continue exclude_word = True elif word in partial_words: # Partial search words (e.g. *ello) partial_results = set() for complete_word, indices in wordindex.items(): if complete_word.endswith(word): partial_results.update(indices) if partial_results: results = self.update_search_results( results, partial_results) continue results = self.update_search_results(results, wordindex.get(word), exclude_word) if results is None: # No matches found break return results except ValueError: log.add_debug( "Error: DB closed during search, perhaps due to rescanning shares or closing the application" ) return None
def sendto(self, transport, addr): """ Send request to a given address via given transport. Args: transport (asyncio.DatagramTransport): Write transport to send the message on. addr (Tuple[str, int]): IP address and port pair to send the message to. """ msg = bytes(self) + b'\r\n\r\n' log.add_debug('UPnP: SSDP request: %s', msg) transport.sendto(msg, addr)
def load_enabled(self): enable = self.config.sections["plugins"]["enable"] if not enable: return log.add(_("Loading plugin system")) to_enable = self.config.sections["plugins"]["enabled"] log.add_debug("Enabled plugin(s): %s" % ', '.join(to_enable)) for plugin in to_enable: self.enable_plugin(plugin)
def add_port_mapping(cls, router, protocol, public_port, private_ip, private_port, mapping_description, lease_duration): """ Adds a port mapping to a router """ from xml.etree import ElementTree log.add_debug( "Adding port mapping (%s, %s, %s, %s, %s)", (router.uuid, protocol, public_port, private_ip, private_port)) url = '{}{}'.format(router.base_url, router.control_url) log.add_debug('Adding port mapping (%s %s/%s) at url "%s"', (private_ip, private_port, protocol, url)) headers = { 'Host': '{}:{}'.format(router.ip, router.port), 'Content-Type': 'text/xml; charset=utf-8', 'SOAPACTION': '"{}#AddPortMapping"'.format(router.svc_type) } data = UPnp._add_port_mapping_template.format(public_port, protocol, private_port, private_ip, mapping_description, lease_duration) log.add_debug('UPnP: Add port mapping request headers: %s', headers) log.add_debug('UPnP: Add port mapping request contents: %s', data) response = http_request(router.url_scheme, router.base_url, router.control_url, request_type="POST", body=data, headers=headers) log.add_debug('UPnP: Add port mapping response: %s', response.encode('utf-8')) xml = ElementTree.fromstring(response) error_code = next((x.text for x in xml.findall( ".//{urn:schemas-upnp-org:control-1-0}errorCode")), None) error_description = next((x.text for x in xml.findall( ".//{urn:schemas-upnp-org:control-1-0}errorDescription")), None) if error_code or error_description: raise Exception('Error code %(code)s: %(description)s' % { 'code': error_code, 'description': error_description })
def _request_port_mapping(self, router, protocol, public_port, private_ip, private_port, mapping_description, lease_duration): """ Function that adds a port mapping to the router. If a port mapping already exists, it is updated with a lease period of 24 hours. """ from xml.etree import ElementTree url = '%s%s' % (router.base_url, router.control_url) log.add_debug( "UPnP: Adding port mapping (%s %s/%s, %s) at url '%s'", (private_ip, private_port, protocol, router.search_target, url)) headers = { "Host": router.base_url, "Content-Type": "text/xml; charset=utf-8", "SOAPACTION": '"%s#AddPortMapping"' % router.service_type } body = ( self.request_body % (router.service_type, public_port, protocol, private_port, private_ip, mapping_description, lease_duration)).encode('utf-8') log.add_debug("UPnP: Add port mapping request headers: %s", headers) log.add_debug("UPnP: Add port mapping request contents: %s", body) response = http_request(router.url_scheme, router.base_url, router.control_url, request_type="POST", body=body, headers=headers) xml = ElementTree.fromstring(response) if xml.find( ".//{http://schemas.xmlsoap.org/soap/envelope/}Body") is None: raise Exception( _("Invalid response: %s") % response.encode('utf-8')) log.add_debug("UPnP: Add port mapping response: %s", response.encode('utf-8')) error_code = xml.findtext( ".//{urn:schemas-upnp-org:control-1-0}errorCode") error_description = xml.findtext( ".//{urn:schemas-upnp-org:control-1-0}errorDescription") if error_code or error_description: raise Exception( _("Error code %(code)s: %(description)s") % { "code": error_code, "description": error_description })
def find_suitable_external_wan_port(self): """Function to find a suitable external WAN port to map to the client. It will detect if a port mapping to the client already exist. """ # Output format: (e_port, protocol, (int_client, iport), desc, enabled, # rHost, duration) log.add_debug( 'Existing Port Mappings: %s', (sorted(self.existingportsmappings, key=lambda tup: tup[0]))) # Analyze ports mappings for m in sorted(self.existingportsmappings, key=lambda tup: tup[0]): (e_port, protocol, (int_client, iport), desc, enabled, rhost, duration) = m # A Port Mapping is already in place with the client: we will # rewrite it to avoid a timeout on the duration of the mapping if protocol == "TCP" and \ str(int_client) == str(self.internalipaddress) and \ iport == self.internallanport: log.add_debug('Port Mapping already in place: %s', str(m)) self.externalwanport = e_port self.foundexistingmapping = True break # If no mapping already in place we try to found a suitable external # WAN port if not self.foundexistingmapping: # Find the first external WAN port > requestedwanport that's not # already reserved tcpportsreserved = [ x[0] for x in sorted(self.existingportsmappings) if x[1] == "TCP" ] while self.externalwanport in tcpportsreserved: if self.externalwanport + 1 <= 65535: self.externalwanport += 1 else: raise AssertionError( _('Failed to find a suitable external WAN port, ' + 'bailing out.'))
def list_port_mappings(cls, router): """ Lists the port mappings for a router """ log.add_debug('Listing existing port mappings...') headers = { 'Host': '{}:{}'.format(router.ip, router.port), 'Content-Type': 'text/xml; charset=utf-8', 'SOAPACTION': '"{}#GetGenericPortMappingEntry"'.format(router.svc_type) } index = -1 portmap_found = True portmaps = [] while portmap_found: index += 1 data = UPnp._list_port_mappings_template.format(index) log.add_debug('UPnP: List port mappings request headers: %s', headers) log.add_debug('UPnP: List port mappings request contents: %s', data) response = http_request(router.url_scheme, router.base_url, router.control_url, request_type="POST", body=data, headers=headers) log.add_debug('UPnP: List port mappings response: %s', response.encode('utf-8')) portmap = PortMapping.parse_port_map_xml(response, router.svc_type) if not portmap: portmap_found = False else: portmaps.append(portmap) log.add_debug('Existing port mappings: %s', portmaps) return portmaps
def _trigger_command(self, command, source, args, public_command): self.command_source = (public_command, source) for module, plugin in self.enabled_plugins.items(): if plugin is None: continue return_value = None commands = plugin.__publiccommands__ if public_command else plugin.__privatecommands__ try: for trigger, func in commands: if trigger == command: return_value = getattr(plugin, func.__name__)(source, args) except Exception: self.show_plugin_error(module, sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2]) continue if return_value is None: # Nothing changed, continue to the next plugin continue if return_value == returncode['zap']: self.command_source = None return True if return_value == returncode['pass']: continue log.add_debug( "Plugin %(module)s returned something weird, '%(value)s', ignoring", { 'module': module, 'value': str(return_value) }) self.command_source = None return False
def parse_ssdp_response(cls, ssdp_response, sender): response_headers = dict(ssdp_response.headers) if 'LOCATION' not in response_headers: log.add_debug( 'The M-SEARCH response from %s:%d did not contain a Location header.', (sender[0], sender[1])) log.add_debug(ssdp_response) return None from urllib.parse import urlsplit urlparts = urlsplit(response_headers['LOCATION']) return Router(ip=sender[0], port=sender[1], wan_ip_type=response_headers['ST'], url_scheme=urlparts.scheme, base_url=urlparts.netloc, root_url=urlparts.path)
def get_router_control_url(url_scheme, base_url, root_url): service_type = None control_url = None try: from xml.etree import ElementTree response = http_request(url_scheme, base_url, root_url, timeout=2) log.add_debug( "UPnP: Device description response from %s://%s%s: %s", (url_scheme, base_url, root_url, response.encode('utf-8'))) xml = ElementTree.fromstring(response) for service in xml.findall( ".//{urn:schemas-upnp-org:device-1-0}service"): found_service_type = service.find( ".//{urn:schemas-upnp-org:device-1-0}serviceType").text if found_service_type in ( "urn:schemas-upnp-org:service:WANIPConnection:1", "urn:schemas-upnp-org:service:WANPPPConnection:1", "urn:schemas-upnp-org:service:WANIPConnection:2"): # We found a router with UPnP enabled service_type = found_service_type control_url = service.find( ".//{urn:schemas-upnp-org:device-1-0}controlURL").text break except Exception as error: # Invalid response log.add_debug( "UPnP: Invalid device description response from %s://%s%s: %s", (url_scheme, base_url, root_url, error)) return service_type, control_url
def __init__(self, frame): super().__init__(frame) try: # Check if AyatanaAppIndicator3 is available gi.require_version('AyatanaAppIndicator3', '0.1') from gi.repository import AyatanaAppIndicator3 self.implementation_class = AyatanaAppIndicator3 except (ImportError, ValueError): try: # Check if AppIndicator3 is available gi.require_version('AppIndicator3', '0.1') from gi.repository import AppIndicator3 self.implementation_class = AppIndicator3 except (ImportError, ValueError) as error: raise AttributeError( "AppIndicator implementation not available") from error self.tray_icon = self.implementation_class.Indicator.new( id=config.application_name, icon_name="", category=self.implementation_class.IndicatorCategory. APPLICATION_STATUS) self.tray_icon.set_menu(self.menu) # Action to hide/unhide main window when middle clicking the tray icon self.tray_icon.set_secondary_activate_target( self.menu.get_children()[0]) if self.final_icon_path: log.add_debug("Using tray icon path %s", self.final_icon_path) self.tray_icon.set_icon_theme_path(self.final_icon_path)
def add_router(routers, ssdp_response): from urllib.parse import urlsplit response_headers = {k.upper(): v for k, v in ssdp_response.headers} log.add_debug("UPnP: Device search response: %s", bytes(ssdp_response)) if "LOCATION" not in response_headers: log.add_debug( "UPnP: M-SEARCH response did not contain a LOCATION header: %s", ssdp_response.headers) return url_parts = urlsplit(response_headers["LOCATION"]) service_type, control_url = SSDP.get_router_control_url( url_parts.scheme, url_parts.netloc, url_parts.path) if service_type is None or control_url is None: log.add_debug( "UPnP: No router with UPnP enabled in device search response, ignoring" ) return log.add_debug( "UPnP: Device details: service_type '%s'; control_url '%s'", (service_type, control_url)) routers.append( Router(wan_ip_type=response_headers['ST'], url_scheme=url_parts.scheme, base_url=url_parts.netloc, root_url=url_parts.path, service_type=service_type, control_url=control_url)) log.add_debug("UPnP: Added device to list")
def add_result_list(self, result_list, user, country, inqueue, ulspeed, h_speed, h_queue, color, private=False): """ Adds a list of search results to the treeview. Lists can either contain publicly or privately shared files. """ update_ui = False for result in result_list: if self.num_results_found >= self.max_limit: self.max_limited = True break fullpath = result[1] fullpath_lower = fullpath.lower() if any(word in fullpath_lower for word in self.searchterm_words_ignore): # Filter out results with filtered words (e.g. nicotine -music) log.add_debug(( "Filtered out excluded search result %(filepath)s from user %(user)s for " "search term \"%(query)s\""), { "filepath": fullpath, "user": user, "query": self.text }) continue if not any(word in fullpath_lower for word in self.searchterm_words_include): # Certain users may send us wrong results, filter out such ones log.add_search( _("Filtered out incorrect search result %(filepath)s from user %(user)s for " "search query \"%(query)s\""), { "filepath": fullpath, "user": user, "query": self.text }) continue self.num_results_found += 1 fullpath_split = fullpath.split('\\') if config.sections["ui"]["reverse_file_paths"]: # Reverse file path, file name is the first item. next() retrieves the name and removes # it from the iterator. fullpath_split = reversed(fullpath_split) name = next(fullpath_split) else: # Regular file path, file name is the last item. Retrieve it and remove it from the list. name = fullpath_split.pop() # Join the resulting items into a folder path directory = '\\'.join(fullpath_split) size = result[2] h_size = human_size(size) h_bitrate, bitrate, h_length, length = get_result_bitrate_length( size, result[4]) if private: name = _("[PRIVATE] %s") % name is_result_visible = self.append([ self.num_results_found, user, get_flag_icon_name(country), h_speed, h_queue, directory, name, h_size, h_bitrate, h_length, GObject.Value(GObject.TYPE_UINT, bitrate), fullpath, country, GObject.Value(GObject.TYPE_UINT64, size), GObject.Value(GObject.TYPE_UINT, ulspeed), GObject.Value(GObject.TYPE_UINT64, inqueue), GObject.Value(GObject.TYPE_UINT, length), GObject.Value(GObject.TYPE_STRING, color) ]) if is_result_visible: update_ui = True return update_ui
def process_search_request(self, searchterm, user, token, direct=False): """ Note: since this section is accessed every time a search request arrives several times per second, please keep it as optimized and memory sparse as possible! """ if not searchterm: return if not self.config.sections["searches"]["search_results"]: # Don't return _any_ results when this option is disabled return if not direct and user == self.core.login_username: # We shouldn't send a search response if we initiated the search request, # unless we're specifically searching our own username return maxresults = self.config.sections["searches"]["maxresults"] if maxresults == 0: return # Remember excluded/partial words for later excluded_words = [] partial_words = [] if '-' in searchterm or '*' in searchterm: for word in searchterm.split(): if len(word) < 1: continue if word.startswith('-'): for subword in word.translate( self.translatepunctuation).split(): excluded_words.append(subword) elif word.startswith('*'): for subword in word.translate( self.translatepunctuation).split(): partial_words.append(subword) # Strip punctuation searchterm_old = searchterm searchterm = searchterm.lower().translate( self.translatepunctuation).strip() if len(searchterm ) < self.config.sections["searches"]["min_search_chars"]: # Don't send search response if search term contains too few characters return checkuser, _reason = self.core.network_filter.check_user(user, None) if not checkuser: return if checkuser == 2: wordindex = self.share_dbs.get("buddywordindex") else: wordindex = self.share_dbs.get("wordindex") if wordindex is None: return # Find common file matches for each word in search term resultlist = self.create_search_result_list(searchterm, wordindex, excluded_words, partial_words) if not resultlist: return if checkuser == 2: fileindex = self.share_dbs.get("buddyfileindex") else: fileindex = self.share_dbs.get("fileindex") if fileindex is None: return fileinfos = [] numresults = min(len(resultlist), maxresults) for index in islice(resultlist, numresults): fileinfo = fileindex.get(repr(index)) if fileinfo is not None: fileinfos.append(fileinfo) if numresults != len(fileinfos): log.add_debug( ("Error: File index inconsistency while responding to search request \"%(query)s\". " "Expected %(expected_num)i results, but found %(total_num)i results in database." ), { "query": searchterm_old, "expected_num": numresults, "total_num": len(fileinfos) }) numresults = len(fileinfos) if not numresults: return uploadspeed = self.core.transfers.upload_speed queuesize = self.core.transfers.get_upload_queue_size() slotsavail = self.core.transfers.allow_new_uploads() fifoqueue = self.config.sections["transfers"]["fifoqueue"] message = slskmessages.FileSearchResult(None, self.core.login_username, token, fileinfos, slotsavail, uploadspeed, queuesize, fifoqueue) self.core.send_message_to_peer(user, message) log.add_search( _("User %(user)s is searching for \"%(query)s\", found %(num)i results" ), { 'user': user, 'query': searchterm_old, 'num': numresults })
def sendto(self, sock, addr): msg = bytes(self) sock.sendto(msg, addr) log.add_debug("UPnP: SSDP request: %s", msg)
def _update_port_mapping(self, listening_port): """ This function supports creating a Port Mapping via the UPnP IGDv1 and IGDv2 protocol. Any UPnP port mapping done with IGDv2 will expire after a maximum of 7 days (lease period), according to the protocol. We set the lease period to a shorter 24 hours, and regularly renew the port mapping. """ try: log.add_debug("UPnP: Creating Port Mapping rule...") # Find local IP address local_ip_address = self.find_local_ip_address() # Find router router = self.find_router(local_ip_address) if not router: raise RuntimeError(_("UPnP is not available on this network")) # Perform the port mapping log.add_debug( "UPnP: Trying to redirect external WAN port %s TCP => %s port %s TCP", (listening_port, local_ip_address, listening_port)) try: self._request_port_mapping( router=router, protocol="TCP", public_port=listening_port, private_ip=local_ip_address, private_port=listening_port, mapping_description="NicotinePlus", lease_duration=86400 # Expires in 24 hours ) except Exception as error: raise RuntimeError( _("Failed to map the external WAN port: %(error)s") % {"error": error}) from error except Exception as error: from traceback import format_exc log.add( _("UPnP: Failed to forward external port %(external_port)s: %(error)s" ), { "external_port": listening_port, "error": error }) log.add_debug(format_exc()) return log.add( _("UPnP: External port %(external_port)s successfully forwarded to local " "IP address %(ip_address)s port %(local_port)s"), { "external_port": listening_port, "ip_address": local_ip_address, "local_port": listening_port })
def get_routers(private_ip=None): log.add_debug("UPnP: Discovering... delay=%s seconds", SSDP.response_time_secs) # Create a UDP socket and set its timeout sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM, proto=socket.IPPROTO_UDP) sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, SSDP.response_time_secs) sock.setblocking(False) if private_ip: sock.bind((private_ip, 0)) # Protocol 1 wan_ip1_sent = False wan_ip1 = SSDPRequest("urn:schemas-upnp-org:service:WANIPConnection:1") wan_ppp1_sent = False wan_ppp1 = SSDPRequest( "urn:schemas-upnp-org:service:WANPPPConnection:1") wan_igd1_sent = False wan_igd1 = SSDPRequest( "urn:schemas-upnp-org:device:InternetGatewayDevice:1") # Protocol 2 wan_ip2_sent = False wan_ip2 = SSDPRequest("urn:schemas-upnp-org:service:WANIPConnection:2") wan_igd2_sent = False wan_igd2 = SSDPRequest( "urn:schemas-upnp-org:device:InternetGatewayDevice:2") routers = [] time_end = time.time() + SSDP.response_time_secs while time.time() < time_end: readable, writable, _ = select.select([sock], [sock], [sock], 0) for sock in readable: msg, _sender = sock.recvfrom(4096) SSDP.add_router(routers, SSDPResponse(msg.decode('utf-8'))) for sock in writable: if not wan_ip1_sent: wan_ip1.sendto(sock, (SSDP.multicast_host, SSDP.multicast_port)) log.add_debug("UPnP: Sent M-SEARCH IP request 1") time_end = time.time() + SSDP.response_time_secs wan_ip1_sent = True if not wan_ppp1_sent: wan_ppp1.sendto(sock, (SSDP.multicast_host, SSDP.multicast_port)) log.add_debug("UPnP: Sent M-SEARCH PPP request 1") time_end = time.time() + SSDP.response_time_secs wan_ppp1_sent = True if not wan_igd1_sent: wan_igd1.sendto(sock, (SSDP.multicast_host, SSDP.multicast_port)) log.add_debug("UPnP: Sent M-SEARCH IGD request 1") time_end = time.time() + SSDP.response_time_secs wan_igd1_sent = True if not wan_ip2_sent: wan_ip2.sendto(sock, (SSDP.multicast_host, SSDP.multicast_port)) log.add_debug("UPnP: Sent M-SEARCH IP request 2") time_end = time.time() + SSDP.response_time_secs wan_ip2_sent = True if not wan_igd2_sent: wan_igd2.sendto(sock, (SSDP.multicast_host, SSDP.multicast_port)) log.add_debug("UPnP: Sent M-SEARCH IGD request 2") time_end = time.time() + SSDP.response_time_secs wan_igd2_sent = True # Cooldown time.sleep(0.01) log.add_debug("UPnP: %s device(s) detected", str(len(routers))) sock.close() return routers
def load_config(self): from pynicotine.utils import load_file log_dir = os.path.join(self.data_dir, "logs") self.defaults = { "server": { "server": ("server.slsknet.org", 2242), "login": "", "passw": "", "interface": "", "ctcpmsgs": False, "autosearch": [], "autoreply": "", "portrange": (2234, 2239), "upnp": True, "upnp_interval": 4, "auto_connect_startup": True, "userlist": [], "banlist": [], "ignorelist": [], "ipignorelist": {}, "ipblocklist": {}, "autojoin": ["nicotine"], "autoaway": 15, "away": False, "private_chatrooms": False, "command_aliases": {} }, "transfers": { "incompletedir": os.path.join(self.data_dir, 'incomplete'), "downloaddir": os.path.join(self.data_dir, 'downloads'), "uploaddir": os.path.join(self.data_dir, 'received'), "usernamesubfolders": False, "shared": [], "buddyshared": [], "uploadbandwidth": 10, "uselimit": False, "usealtlimits": False, "uploadlimit": 1000, "uploadlimitalt": 100, "downloadlimit": 0, "downloadlimitalt": 100, "preferfriends": False, "useupslots": False, "uploadslots": 2, "afterfinish": "", "afterfolder": "", "lock": True, "reverseorder": False, "fifoqueue": False, "usecustomban": False, "limitby": True, "customban": "Banned, don't bother retrying", "usecustomgeoblock": False, "customgeoblock": "Sorry, your country is blocked", "queuelimit": 10000, "filelimit": 100, "buddysharestrustedonly": False, "friendsnolimits": False, "groupdownloads": "folder_grouping", "groupuploads": "folder_grouping", "geoblock": False, "geoblockcc": [""], "remotedownloads": True, "uploadallowed": 2, "autoclear_downloads": False, "autoclear_uploads": False, "uploadsinsubdirs": True, "rescanonstartup": True, "enablefilters": False, "downloadregexp": "", "downloadfilters": [["desktop.ini", 1], ["folder.jpg", 1], ["*.url", 1], ["thumbs.db", 1], [ "albumart(_{........-....-....-....-............}_)?(_?(large|small))?\\.jpg", 0 ]], "download_doubleclick": 1, "upload_doubleclick": 1, "downloadsexpanded": True, "uploadsexpanded": True }, "userinfo": { "descr": "''", "pic": "" }, "words": { "censored": [], "autoreplaced": { "teh ": "the ", "taht ": "that ", "tihng": "thing", "youre": "you're", "jsut": "just", "thier": "their", "tihs": "this" }, "censorfill": "*", "censorwords": False, "replacewords": False, "tab": True, "cycle": False, "dropdown": False, "characters": 3, "roomnames": True, "buddies": True, "roomusers": True, "commands": True, "aliases": True, "onematch": False }, "logging": { "debug": False, "debugmodes": [], "debuglogsdir": os.path.join(log_dir, "debug"), "logcollapsed": True, "transferslogsdir": os.path.join(log_dir, "transfers"), "rooms_timestamp": "%H:%M:%S", "private_timestamp": "%Y-%m-%d %H:%M:%S", "log_timestamp": "%Y-%m-%d %H:%M:%S", "timestamps": True, "privatechat": True, "chatrooms": True, "transfers": False, "debug_file_output": False, "roomlogsdir": os.path.join(log_dir, "rooms"), "privatelogsdir": os.path.join(log_dir, "private"), "readroomlogs": True, "readroomlines": 15, "readprivatelines": 15, "rooms": [] }, "privatechat": { "store": True, "users": [] }, "columns": { "file_search": {}, "download": {}, "upload": {}, "user_browse": {}, "buddy_list": {}, "chat_room": {} }, "searches": { "expand_searches": True, "group_searches": "folder_grouping", "maxresults": 50, "enable_history": True, "history": [], "enablefilters": False, "filters_visible": False, "defilter": ["", "", "", "", 0, ""], "filtercc": [], "filterin": [], "filterout": [], "filtersize": [], "filterbr": [], "filtertype": [], "search_results": True, "max_displayed_results": 1500, "min_search_chars": 3, "remove_special_chars": True, "private_search_results": True }, "ui": { "dark_mode": False, "header_bar": True, "icontheme": "", "chatme": "#908e8b", "chatremote": "", "chatlocal": "", "chathilite": "#5288ce", "urlcolor": "#5288ce", "useronline": "#16bb5c", "useraway": "#c9ae13", "useroffline": "#e04f5e", "usernamehotspots": True, "usernamestyle": "bold", "textbg": "", "search": "", "searchq": "GREY", "inputcolor": "", "spellcheck": True, "exitdialog": 1, "tab_default": "", "tab_hilite": "#497ec2", "tab_changed": "#497ec2", "tab_select_previous": True, "tabmain": "Top", "tabrooms": "Top", "tabprivate": "Top", "tabinfo": "Top", "tabbrowse": "Top", "tabsearch": "Top", "tab_status_icons": True, "globalfont": "", "chatfont": "", "tabclosers": True, "searchfont": "", "listfont": "", "browserfont": "", "transfersfont": "", "last_tab_id": "", "modes_visible": { "search": True, "downloads": True, "uploads": True, "userbrowse": True, "userinfo": True, "private": True, "chatrooms": True, "interests": True }, "modes_order": [ "search", "downloads", "uploads", "userbrowse", "userinfo", "private", "userlist", "chatrooms", "interests" ], "buddylistinchatrooms": "tab", "trayicon": True, "startup_hidden": False, "filemanager": "", "speechenabled": False, "speechprivate": "User %(user)s told you: %(message)s", "speechrooms": "In room %(room)s, user %(user)s said: %(message)s", "speechcommand": "flite -t $", "width": 1000, "height": 600, "xposition": -1, "yposition": -1, "maximized": True, "urgencyhint": True, "file_path_tooltips": True, "reverse_file_paths": True }, "private_rooms": { "rooms": {} }, "urls": { "protocols": {} }, "interests": { "likes": [], "dislikes": [] }, "players": { "default": "", "npothercommand": "", "npplayer": "mpris", "npformatlist": [], "npformat": "" }, "notifications": { "notification_window_title": True, "notification_tab_colors": False, "notification_popup_sound": False, "notification_popup_file": True, "notification_popup_folder": True, "notification_popup_private_message": True, "notification_popup_chatroom": False, "notification_popup_chatroom_mention": True }, "plugins": { "enable": True, "enabled": [] }, "statistics": { "started_downloads": 0, "completed_downloads": 0, "downloaded_size": 0, "started_uploads": 0, "completed_uploads": 0, "uploaded_size": 0 } } self.removed_options = { "transfers": ("pmqueueddir", "autoretry_downloads", "shownotification", "shownotificationperfolder", "prioritize", "sharedownloaddir", "geopanic", "enablebuddyshares", "friendsonly", "enabletransferbuttons"), "server": ("lastportstatuscheck", "serverlist", "enc", "fallbackencodings", "roomencoding", "userencoding", "firewalled"), "ui": ("enabletrans", "mozembed", "open_in_mozembed", "tooltips", "transalpha", "transfilter", "transtint", "soundenabled", "soundtheme", "soundcommand", "tab_colors", "tab_icons", "searchoffline", "chat_hidebuttons", "tab_reorderable", "private_search_results", "private_shares", "labelmain", "labelrooms", "labelprivate", "labelinfo", "labelbrowse", "labelsearch", "notexists", "roomlistcollapsed", "showaway", "decimalsep"), "columns": ("downloads", "uploads", "search", "search_widths", "downloads_columns", "downloads_widths", "uploads_columns", "uploads_widths", "userbrowse", "userbrowse_widths", "userlist", "userlist_widths", "chatrooms", "chatrooms_widths", "download_columns", "download_widths", "upload_columns", "upload_widths", "filesearch_columns", "filesearch_widths", "hideflags"), "searches": ("distrib_timer", "distrib_ignore", "reopen_tabs", "max_stored_results", "re_filter"), "userinfo": ("descrutf8"), "private_rooms": ("enabled"), "logging": ("logsdir"), "ticker": ("default", "rooms", "hide"), "language": ("language", "setlanguage"), "urls": ("urlcatching", "humanizeurls"), "notifications": ("notification_tab_icons") } # Windows specific stuff if sys.platform == "win32": self.defaults['players']['npplayer'] = 'other' # Initialize config with default values for key, value in self.defaults.items(): self.sections[key] = value.copy() self.create_config_folder() self.create_data_folder() load_file(self.filename, self.parse_config) # Update config values from file self.set_config() if sys.platform == "darwin": # Disable header bar in macOS for now due to GTK 3 performance issues self.sections["ui"]["header_bar"] = False # Convert special download folder share to regular share if self.sections["transfers"].get("sharedownloaddir", False): shares = self.sections["transfers"]["shared"] virtual_name = "Downloaded" shared_folder = (virtual_name, self.sections["transfers"]["downloaddir"]) if shared_folder not in shares and virtual_name not in ( x[0] for x in shares): shares.append(shared_folder) # Load command aliases from legacy file try: if not self.sections["server"][ "command_aliases"] and os.path.exists(self.filename + ".alias"): with open(self.filename + ".alias", 'rb') as file_handle: from pynicotine.utils import RestrictedUnpickler self.sections["server"][ "command_aliases"] = RestrictedUnpickler( file_handle, encoding='utf-8').load() except Exception: pass self.config_loaded = True from pynicotine.logfacility import log log.add_debug("Using configuration: %(file)s", {"file": self.filename})
def list(cls, refresh=False): """ list finds all devices responding to an SSDP search """ log.add_debug('UPnP: Discovering... delay=%s seconds', SSDP.response_time_secs) # Create a UDP socket and set its timeout sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM, proto=socket.IPPROTO_UDP) sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) sock.setblocking(False) # Create the WANIPConnection:1 and WANIPConnection:2 request objects headers = { 'HOST': "{}:{}".format(SSDP.multicast_host, SSDP.multicast_port), 'ST': None, 'MAN': '"ssdp:discover"', 'MX': str(SSDP.response_time_secs) } # Protocol 1 wan_ip1_sent = False wan_ip1 = SSDP._create_msearch_request( 'urn:schemas-upnp-org:service:WANIPConnection:1', headers=headers) wan_ppp1_sent = False wan_ppp1 = SSDP._create_msearch_request( 'urn:schemas-upnp-org:service:WANPPPConnection:1', headers=headers) wan_igd1_sent = False wan_igd1 = SSDP._create_msearch_request( 'urn:schemas-upnp-org:device:InternetGatewayDevice:1', headers=headers) # Protocol 2 wan_ip2_sent = False wan_ip2 = SSDP._create_msearch_request( 'urn:schemas-upnp-org:service:WANIPConnection:2', headers=headers) wan_igd2_sent = False wan_igd2 = SSDP._create_msearch_request( 'urn:schemas-upnp-org:device:InternetGatewayDevice:2', headers=headers) inputs = [sock] outputs = [sock] routers = [] time_end = time.time() + SSDP.response_time_secs while time.time() < time_end: _timeout = 1 readable, writable, _ = select.select(inputs, outputs, inputs, _timeout) for _sock in readable: msg, sender = _sock.recvfrom(SSDP.buffer_size) response = SSDPResponse.parse(msg.decode('utf-8')) log.add_debug('UPnP: Device search response: %s', bytes(response)) router = Router.parse_ssdp_response(response, sender) if router: routers.append(router) for _sock in writable: if not wan_ip1_sent: wan_ip1.sendto(_sock, (SSDP.multicast_host, SSDP.multicast_port)) log.add_debug('UPnP: Sent M-SEARCH IP request 1') time_end = time.time() + SSDP.response_time_secs wan_ip1_sent = True if not wan_ppp1_sent: wan_ppp1.sendto(_sock, (SSDP.multicast_host, SSDP.multicast_port)) log.add_debug('UPnP: Sent M-SEARCH PPP request 1') time_end = time.time() + SSDP.response_time_secs wan_ppp1_sent = True if not wan_igd1_sent: wan_igd1.sendto(_sock, (SSDP.multicast_host, SSDP.multicast_port)) log.add_debug('UPnP: Sent M-SEARCH IGD request 1') time_end = time.time() + SSDP.response_time_secs wan_igd1_sent = True if not wan_ip2_sent: wan_ip2.sendto(_sock, (SSDP.multicast_host, SSDP.multicast_port)) log.add_debug('UPnP: Sent M-SEARCH IP request 2') time_end = time.time() + SSDP.response_time_secs wan_ip2_sent = True if not wan_igd2_sent: wan_igd2.sendto(_sock, (SSDP.multicast_host, SSDP.multicast_port)) log.add_debug('UPnP: Sent M-SEARCH IGD request 2') time_end = time.time() + SSDP.response_time_secs wan_igd2_sent = True # Cooldown time.sleep(0.4) for r in routers: serial_number, control_url, uuid, svc_type = SSDP._get_router_service_description( r.url_scheme, r.base_url, r.root_url) r.serial_number = serial_number r.control_url = control_url r.uuid = uuid r.svc_type = svc_type sock.close() log.add_debug('UPnP: %s device(s) detected', str(len(routers))) return routers
def get_files_list(self, sharestype, mtimes, oldmtimes, oldfiles, oldstreams, rebuild=False): """ Get a list of files with their filelength, bitrate and track length in seconds """ files = {} streams = {} count = 0 lastpercent = 0.0 for folder in mtimes: try: count += 1 if self.ui_callback: # Truncate the percentage to two decimal places to avoid sending data to the GUI thread too often percent = float("%.2f" % (float(count) / len(mtimes) * 0.75)) if percent > lastpercent and percent <= 1.0: self.ui_callback.set_scan_progress(sharestype, percent) lastpercent = percent virtualdir = self.real2virtual(folder) if not rebuild and folder in oldmtimes: if mtimes[folder] == oldmtimes[folder]: if os.path.exists(folder): try: files[virtualdir] = oldfiles[virtualdir] streams[virtualdir] = oldstreams[virtualdir] continue except KeyError: log.add_debug(_("Inconsistent cache for '%(vdir)s', rebuilding '%(dir)s'"), { 'vdir': virtualdir, 'dir': folder }) else: log.add_debug(_("Dropping missing folder %(dir)s"), {'dir': folder}) continue files[virtualdir] = [] for entry in os.scandir(folder): if entry.is_file(): filename = entry.name if self.is_hidden(folder, filename): continue # Get the metadata of the file data = self.get_file_info(filename, entry.path, entry) if data is not None: files[virtualdir].append(data) streams[virtualdir] = self.get_dir_stream(files[virtualdir]) except OSError as errtuple: log.add(_("Error while scanning folder %(path)s: %(error)s"), {'path': folder, 'error': errtuple}) continue return files, streams