socket.gethostbyname does not support IPv6 name resolution, and getaddrinfo() should be used instead for IPv4/v6 dual stack support. Closes: https://github.com/deluge-torrent/deluge/pull/376
304 lines
10 KiB
Python
304 lines
10 KiB
Python
#
|
|
# Copyright (C) Calum Lind 2017 <calumlind+deluge@gmail.com>
|
|
#
|
|
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
|
|
# the additional special exception to link portions of this program with the OpenSSL library.
|
|
# See LICENSE for more details.
|
|
#
|
|
|
|
import logging
|
|
import os
|
|
import uuid
|
|
from socket import gaierror, getaddrinfo
|
|
|
|
from twisted.internet import defer
|
|
|
|
from deluge.common import get_localhost_auth
|
|
from deluge.config import Config
|
|
from deluge.configmanager import get_config_dir
|
|
from deluge.ui.client import Client, client
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
DEFAULT_HOST = '127.0.0.1'
|
|
DEFAULT_PORT = 58846
|
|
LOCALHOST = ('127.0.0.1', 'localhost', '::1')
|
|
|
|
|
|
def default_hostlist():
|
|
"""Create a new hosts key for hostlist with a localhost entry"""
|
|
host_id = uuid.uuid4().hex
|
|
username, password = get_localhost_auth()
|
|
return {'hosts': [(host_id, DEFAULT_HOST, DEFAULT_PORT, username, password)]}
|
|
|
|
|
|
def validate_host_info(hostname, port):
|
|
"""Checks that hostname and port are valid.
|
|
|
|
Args:
|
|
hostname (str): The IP or hostname of the deluge daemon.
|
|
port (int): The port of the deluge daemon.
|
|
|
|
Raises:
|
|
ValueError: Host details are not valid with reason.
|
|
"""
|
|
|
|
try:
|
|
getaddrinfo(hostname, None)
|
|
except gaierror as ex:
|
|
raise ValueError('Host %s: %s', hostname, ex.args[1])
|
|
|
|
if not isinstance(port, int):
|
|
raise ValueError('Invalid port. Must be an integer')
|
|
|
|
|
|
def migrate_hostlist(old_filename, new_filename):
|
|
"""Check for old hostlist filename and save details to new filename"""
|
|
old_hostlist = get_config_dir(old_filename)
|
|
if os.path.isfile(old_hostlist):
|
|
config_v2 = Config(old_filename, config_dir=get_config_dir())
|
|
config_v2.save(get_config_dir(new_filename))
|
|
del config_v2
|
|
|
|
try:
|
|
os.rename(old_hostlist, old_hostlist + '.old')
|
|
except OSError as ex:
|
|
log.exception(ex)
|
|
|
|
try:
|
|
os.remove(old_hostlist + '.bak')
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def migrate_config_2_to_3(config):
|
|
"""Mirgrates old hostlist config files to new file version"""
|
|
localclient_username, localclient_password = get_localhost_auth()
|
|
if not localclient_username:
|
|
# Nothing to do here, there's no auth file
|
|
return
|
|
for idx, (__, host, __, username, __) in enumerate(config['hosts'][:]):
|
|
if host in LOCALHOST and not username:
|
|
config['hosts'][idx][3] = localclient_username
|
|
config['hosts'][idx][4] = localclient_password
|
|
return config
|
|
|
|
|
|
def mask_hosts_password(hosts):
|
|
"""Replace passwords in hosts list with *'s for log output"""
|
|
if not hosts:
|
|
return hosts
|
|
|
|
return [list(host)[:-1] + ['*' * 10] for host in hosts]
|
|
|
|
|
|
class HostList:
|
|
"""This class contains methods for adding, removing and looking up hosts in hostlist.conf."""
|
|
|
|
def __init__(self):
|
|
migrate_hostlist('hostlist.conf.1.2', 'hostlist.conf')
|
|
self.config = Config(
|
|
'hostlist.conf',
|
|
default_hostlist(),
|
|
config_dir=get_config_dir(),
|
|
file_version=3,
|
|
log_mask_funcs={'hosts': mask_hosts_password},
|
|
)
|
|
self.config.run_converter((1, 2), 3, migrate_config_2_to_3)
|
|
self.config.save()
|
|
|
|
def check_info_exists(self, hostname, port, username, skip_host_id=None):
|
|
"""Check for existing host entries with the same details.
|
|
|
|
Args:
|
|
hostname (str): The IP or hostname of the deluge daemon.
|
|
port (int): The port of the deluge daemon.
|
|
username (str): The username to login to the daemon with.
|
|
skip_host_id (str): A host_id to skip to check if other hosts match details.
|
|
|
|
Raises:
|
|
ValueError: Host details already exist.
|
|
|
|
"""
|
|
for host_entry in self.config['hosts']:
|
|
if (hostname, port, username) == (
|
|
host_entry[1],
|
|
host_entry[2],
|
|
host_entry[3],
|
|
):
|
|
if skip_host_id is not None and skip_host_id == host_entry[0]:
|
|
continue
|
|
raise ValueError('Host details already in hostlist')
|
|
|
|
def add_host(self, hostname, port, username, password):
|
|
"""Add a new host to hostlist.
|
|
|
|
Args:
|
|
hostname (str): The IP or hostname of the deluge daemon.
|
|
port (int): The port of the deluge daemon.
|
|
username (str): The username to login to the daemon with.
|
|
password (str): The password to login to the daemon with.
|
|
|
|
Returns:
|
|
str: The new host id.
|
|
"""
|
|
if (
|
|
not password and not username or username == 'localclient'
|
|
) and hostname in LOCALHOST:
|
|
username, password = get_localhost_auth()
|
|
|
|
validate_host_info(hostname, port)
|
|
self.check_info_exists(hostname, port, username)
|
|
host_id = uuid.uuid4().hex
|
|
self.config['hosts'].append((host_id, hostname, port, username, password))
|
|
self.config.save()
|
|
return host_id
|
|
|
|
def get_host_info(self, host_id):
|
|
"""Get the host details for host_id.
|
|
|
|
Args:
|
|
host_id (str): The host id to get info on.
|
|
|
|
Returns:
|
|
list: A list of (host_id, hostname, port, username).
|
|
|
|
"""
|
|
for host_entry in self.config['hosts']:
|
|
if host_entry[0] == host_id:
|
|
return host_entry[0:4]
|
|
else:
|
|
return []
|
|
|
|
def get_hosts_info(self):
|
|
"""Get information of all the hosts in the hostlist.
|
|
|
|
Returns:
|
|
list of lists: Host information in the format [(host_id, hostname, port, username)].
|
|
|
|
"""
|
|
return [host_entry[0:4] for host_entry in self.config['hosts']]
|
|
|
|
def get_host_status(self, host_id):
|
|
"""Gets the current status (online/offline) of the host
|
|
|
|
Args:
|
|
host_id (str): The host id to check status of.
|
|
|
|
Returns:
|
|
tuple: A tuple of strings (host_id, status, version).
|
|
|
|
"""
|
|
status_offline = (host_id, 'Offline', '')
|
|
|
|
def on_connect(result, c, host_id):
|
|
"""Successfully connected to a daemon"""
|
|
|
|
def on_info(info, c):
|
|
c.disconnect()
|
|
return host_id, 'Online', info
|
|
|
|
def on_info_fail(reason, c):
|
|
c.disconnect()
|
|
return status_offline
|
|
|
|
return c.daemon.info().addCallback(on_info, c).addErrback(on_info_fail, c)
|
|
|
|
def on_connect_failed(reason, host_id):
|
|
"""Connection to daemon failed"""
|
|
log.debug('Host status failed for %s: %s', host_id, reason)
|
|
return status_offline
|
|
|
|
try:
|
|
host_id, host, port, user = self.get_host_info(host_id)
|
|
except ValueError:
|
|
log.warning('Problem getting host_id info from hostlist')
|
|
return defer.succeed(status_offline)
|
|
|
|
try:
|
|
ips = list({addrinfo[4][0] for addrinfo in getaddrinfo(host, None)})
|
|
except (gaierror, IndexError) as ex:
|
|
log.warning('Unable to resolve host %s to IP: %s', host, ex.args[1])
|
|
return defer.succeed(status_offline)
|
|
|
|
host_conn_list = [
|
|
(
|
|
host_ip,
|
|
port,
|
|
'localclient' if not user and host_ip in LOCALHOST else user,
|
|
)
|
|
for host_ip in ips
|
|
]
|
|
|
|
for host_conn_info in host_conn_list:
|
|
if client.connected() and host_conn_info == client.connection_info():
|
|
# Currently connected to host_id daemon.
|
|
def on_info(info, host_id):
|
|
log.debug('Client connected, query info: %s', info)
|
|
return host_id, 'Connected', info
|
|
|
|
return client.daemon.info().addCallback(on_info, host_id)
|
|
else:
|
|
# Attempt to connect to daemon with host_id details.
|
|
c = Client()
|
|
d = c.connect(host, port, skip_authentication=True)
|
|
d.addCallback(on_connect, c, host_id)
|
|
d.addErrback(on_connect_failed, host_id)
|
|
return d
|
|
|
|
def update_host(self, host_id, hostname, port, username, password):
|
|
"""Update the supplied host id with new connection details.
|
|
|
|
Args:
|
|
host_id (str): The host id to update.
|
|
hostname (str): The new IP or hostname of the deluge daemon.
|
|
port (int): The new port of the deluge daemon.
|
|
username (str): The new username to login to the daemon with.
|
|
password (str): The new password to login to the daemon with.
|
|
|
|
"""
|
|
validate_host_info(hostname, port)
|
|
self.check_info_exists(hostname, port, username, skip_host_id=host_id)
|
|
|
|
if (
|
|
not password and not username or username == 'localclient'
|
|
) and hostname in LOCALHOST:
|
|
username, password = get_localhost_auth()
|
|
|
|
for idx, host_entry in enumerate(self.config['hosts']):
|
|
if host_id == host_entry[0]:
|
|
self.config['hosts'][idx] = host_id, hostname, port, username, password
|
|
self.config.save()
|
|
return True
|
|
return False
|
|
|
|
def remove_host(self, host_id):
|
|
"""Removes the host entry from hostlist config.
|
|
|
|
Args:
|
|
host_id (str): The host id to remove.
|
|
|
|
Returns:
|
|
bool: True is successfully removed, False otherwise.
|
|
|
|
"""
|
|
for host_entry in self.config['hosts']:
|
|
if host_id == host_entry[0]:
|
|
self.config['hosts'].remove(host_entry)
|
|
self.config.save()
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def add_default_host(self):
|
|
self.add_host(DEFAULT_HOST, DEFAULT_PORT, *get_localhost_auth())
|
|
|
|
def connect_host(self, host_id):
|
|
"""Connect to host daemon"""
|
|
for host_entry in self.config['hosts']:
|
|
if host_entry[0] == host_id:
|
|
__, host, port, username, password = host_entry
|
|
return client.connect(host, port, username, password)
|
|
|
|
return defer.fail(Exception('Bad host id'))
|