From 37c3b4334ce37ec02e05289731f95fca7f520801 Mon Sep 17 00:00:00 2001 From: Andrew Resch Date: Wed, 10 Dec 2008 07:56:40 +0000 Subject: [PATCH] Add basic authentication -- your connectionmanager config will be cleared and this will likely break other UIs --- deluge/configmanager.py | 1 - deluge/core/authmanager.py | 85 +++++++++ deluge/core/core.py | 14 +- deluge/core/rpcserver.py | 20 ++- deluge/ui/client.py | 2 +- deluge/ui/gtkui/connectionmanager.py | 162 +++++++++++++----- .../ui/gtkui/glade/connection_manager.glade | 60 ++++++- 7 files changed, 291 insertions(+), 53 deletions(-) create mode 100644 deluge/core/authmanager.py diff --git a/deluge/configmanager.py b/deluge/configmanager.py index c162b6f35..b8dd38516 100644 --- a/deluge/configmanager.py +++ b/deluge/configmanager.py @@ -58,7 +58,6 @@ class _ConfigManager: self.config_directory = directory def get_config_dir(self): - log.debug("get_config_dir: %s", self.config_directory) return self.config_directory def close(self, config): diff --git a/deluge/core/authmanager.py b/deluge/core/authmanager.py new file mode 100644 index 000000000..da1c9cfb3 --- /dev/null +++ b/deluge/core/authmanager.py @@ -0,0 +1,85 @@ +# +# authmanager.py +# +# Copyright (C) 2008 Andrew Resch +# +# Deluge is free software. +# +# You may redistribute it and/or modify it under the terms of the +# GNU General Public License, as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) +# any later version. +# +# deluge is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with deluge. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# + +import os.path +import random +import stat + +import deluge.component as component +import deluge.configmanager as configmanager + +class AuthManager(component.Component): + def __init__(self): + component.Component.__init__(self, "AuthManager") + self.auth = {} + + def start(self): + self.__load_auth_file() + + def stop(self): + self.auth = {} + + def shutdown(self): + pass + + def authorize(self, username, password): + """ + Authorizes users based on username and password + + :param username: str, username + :param password: str, password + :returns: True or False + :rtype: bool + + """ + + if username not in self.auth: + return False + + if self.auth[username] == password: + return True + + return False + + def __load_auth_file(self): + auth_file = configmanager.get_config_dir("auth") + # Check for auth file and create if necessary + if not os.path.exists(auth_file): + # We create a 'localclient' account with a random password + try: + from hashlib import sha1 as sha_hash + except ImportError: + from sha import new as sha_hash + open(auth_file, "w").write("localclient:" + sha_hash(str(random.random())).hexdigest()) + # Change the permissions on the file so only this user can read/write it + os.chmod(auth_file, stat.S_IREAD | stat.S_IWRITE) + + # Load the auth file into a dictionary: {username: password, ...} + f = open(auth_file, "r") + for line in f: + if line.startswith("#"): + # This is a comment line + continue + username, password = line.split(":") + self.auth[username] = password diff --git a/deluge/core/core.py b/deluge/core/core.py index b319a16dd..a1d3896bc 100644 --- a/deluge/core/core.py +++ b/deluge/core/core.py @@ -53,6 +53,9 @@ from deluge.core.signalmanager import SignalManager from deluge.core.filtermanager import FilterManager from deluge.core.preferencesmanager import PreferencesManager from deluge.core.autoadd import AutoAdd +from deluge.core.authmanager import AuthManager +from deluge.core.rpcserver import BasicAuthXMLRPCRequestHandler + from deluge.log import LOG as log STATUS_KEYS = ['active_time', 'compact', 'distributed_copies', 'download_payload_rate', 'eta', @@ -91,7 +94,9 @@ class Core( try: log.info("Starting XMLRPC server on port %s", port) SimpleXMLRPCServer.SimpleXMLRPCServer.__init__( - self, (hostname, port), logRequests=False, allow_none=True) + self, (hostname, port), + requestHandler=BasicAuthXMLRPCRequestHandler, + logRequests=False, allow_none=True) except: log.info("Daemon already running or port not available..") sys.exit(0) @@ -202,6 +207,9 @@ class Core( # Create the AutoAdd component self.autoadd = AutoAdd() + # Start the AuthManager + self.authmanager = AuthManager() + # New release check information self.new_release = None @@ -530,7 +538,7 @@ class Core( return None return value - + def export_get_config_values(self, keys): """Get the config values for the entered keys""" config = {} @@ -540,7 +548,7 @@ class Core( except KeyError: pass return config - + def export_set_config(self, config): """Set the config with values from dictionary""" diff --git a/deluge/core/rpcserver.py b/deluge/core/rpcserver.py index f66cb31a5..0713de675 100644 --- a/deluge/core/rpcserver.py +++ b/deluge/core/rpcserver.py @@ -26,6 +26,7 @@ import gobject from deluge.SimpleXMLRPCServer import SimpleXMLRPCServer +from deluge.SimpleXMLRPCServer import SimpleXMLRPCRequestHandler from SocketServer import ThreadingMixIn from base64 import decodestring, encodestring @@ -99,10 +100,15 @@ class XMLRPCServer(ThreadingMixIn, SimpleXMLRPCServer): class BasicAuthXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): def do_POST(self): - auth = self.headers['authorization'] - auth = auth.replace("Basic ","") - decoded_auth = decodestring(auth) - # Check authentication here - # if cannot authenticate, end the connection or - # otherwise call original - return SimpleXMLRPCRequestHandler.do_POST(self) + if "authorization" in self.headers: + auth = self.headers['authorization'] + auth = auth.replace("Basic ","") + decoded_auth = decodestring(auth) + # Check authentication here + if component.get("AuthManager").authorize(*decoded_auth.split(":")): + # User authorized, call the real do_POST now + return SimpleXMLRPCRequestHandler.do_POST(self) + + # if cannot authenticate, end the connection + self.send_response(401) + self.end_headers() diff --git a/deluge/ui/client.py b/deluge/ui/client.py index df8e594ef..3cb2eac2f 100644 --- a/deluge/ui/client.py +++ b/deluge/ui/client.py @@ -64,7 +64,7 @@ class Transport(xmlrpclib.Transport): errcode, errmsg, headers = h.getreply() if errcode != 200: - raise ProtocolError( + raise xmlrpclib.ProtocolError( host + handler, errcode, errmsg, headers diff --git a/deluge/ui/gtkui/connectionmanager.py b/deluge/ui/gtkui/connectionmanager.py index 862ea24b0..2319f7a56 100644 --- a/deluge/ui/gtkui/connectionmanager.py +++ b/deluge/ui/gtkui/connectionmanager.py @@ -31,6 +31,7 @@ import os import subprocess import time import threading +import urlparse import deluge.component as component import deluge.xmlrpclib as xmlrpclib @@ -45,7 +46,7 @@ DEFAULT_HOST = DEFAULT_URI.split(":")[1][2:] DEFAULT_PORT = DEFAULT_URI.split(":")[-1] DEFAULT_CONFIG = { - "hosts": [DEFAULT_HOST + ":" + DEFAULT_PORT] + "hosts": [DEFAULT_URI] } HOSTLIST_COL_PIXBUF = 0 @@ -58,9 +59,31 @@ HOSTLIST_STATUS = [ "Connected" ] +HOSTLIST_PIXBUFS = [ + # This is populated in ConnectionManager.__init__ +] + if deluge.common.windows_check(): import win32api + +def cell_render_host(column, cell, model, row, data): + host = model[row][data] + u = urlparse.urlsplit(host) + if not u.hostname: + host = "http://" + host + u = urlparse.urlsplit(host) + if u.username: + text = u.username + ":*@" + u.hostname + ":" + str(u.port) + else: + text = u.hostname + ":" + str(u.port) + + cell.set_property('text', text) + +def cell_render_pixbuf(column, cell, model, row, data): + state = model[row][data] + cell.set_property('pixbuf', HOSTLIST_PIXBUFS[state]) + class ConnectionManager(component.Component): def __init__(self): component.Component.__init__(self, "ConnectionManager") @@ -71,15 +94,32 @@ class ConnectionManager(component.Component): self.window = component.get("MainWindow") self.config = ConfigManager("hostlist.conf", DEFAULT_CONFIG) + + # Test to see if it's an old config + if DEFAULT_HOST + ":" + DEFAULT_PORT in self.config.config: + # This is likely an older 1.0 config, so lets mv it and start fresh + self.config = None + hostlist_conf = deluge.configmanager.get_config_dir("hostlist.conf") + shutil.move(hostlist_conf, hostlist_conf + ".1.0") + self.config = ConfigManager("hostlist.conf", DEFAULT_CONFIG) + # Change the permissions on the file so only this user can read/write it + os.chmod(hostlist_conf, stat.S_IREAD | stat.S_IWRITE) + self.gtkui_config = ConfigManager("gtkui.conf") self.connection_manager = self.glade.get_widget("connection_manager") # Make the Connection Manager window a transient for the main window. self.connection_manager.set_transient_for(self.window.window) + + # Create status pixbufs + for stock_id in (gtk.STOCK_NO, gtk.STOCK_YES, gtk.STOCK_CONNECT): + HOSTLIST_PIXBUFS.append(self.connection_manager.render_icon(stock_id, gtk.ICON_SIZE_MENU)) + self.hostlist = self.glade.get_widget("hostlist") self.connection_manager.set_icon(common.get_logo(32)) self.glade.get_widget("image1").set_from_pixbuf(common.get_logo(32)) + # connection status pixbuf, hostname:port, status self.liststore = gtk.ListStore(gtk.gdk.Pixbuf, str, int) # Holds the online status of hosts @@ -95,9 +135,11 @@ class ConnectionManager(component.Component): render = gtk.CellRendererPixbuf() column = gtk.TreeViewColumn( "Status", render, pixbuf=HOSTLIST_COL_PIXBUF) + column.set_cell_data_func(render, cell_render_pixbuf, 2) self.hostlist.append_column(column) render = gtk.CellRendererText() column = gtk.TreeViewColumn("Host", render, text=HOSTLIST_COL_URI) + column.set_cell_data_func(render, cell_render_host, 1) self.hostlist.append_column(column) self.glade.signal_autoconnect({ @@ -138,28 +180,50 @@ class ConnectionManager(component.Component): if self.gtkui_config["autoconnect"] and \ self.gtkui_config["autoconnect_host_uri"] != None: uri = self.gtkui_config["autoconnect_host_uri"] - # Make sure the uri is proper - if uri[:7] != "http://": - uri = "http://" + uri if self.test_online_status(uri): # Host is online, so lets connect client.set_core_uri(uri) self.hide() elif self.gtkui_config["autostart_localhost"]: # Check to see if we are trying to connect to a localhost - if uri[7:].split(":")[0] == "localhost" or \ - uri[7:].split(":")[0] == "127.0.0.1": + u = urlparse.urlsplit(uri) + if u.hostname == "localhost" or u.hostname == "127.0.0.1": # This is a localhost, so lets try to start it - port = uri[7:].split(":")[1] # First add it to the list - self.add_host("localhost", port) - self.start_localhost(port) - # We need to wait for the host to start before connecting - while not self.test_online_status(uri): + self.add_host("localhost", u.port) + self.start_localhost(u.port) + # Get the localhost uri with authentication details + auth_uri = None + while not auth_uri: + # We need to keep trying because the daemon may not have been started yet + # and the 'auth' file may not have been created + auth_uri = self.get_localhost_auth_uri(uri) time.sleep(0.01) - client.set_core_uri(uri) + + # We need to wait for the host to start before connecting + while not self.test_online_status(auth_uri): + time.sleep(0.01) + client.set_core_uri(auth_uri) self.hide() + def get_localhost_auth_uri(self, uri): + """ + Grabs the localclient auth line from the 'auth' file and creates a localhost uri + + :param uri: the uri to add the authentication info to + :returns: a localhost uri containing authentication information or None if the information is not available + """ + auth_file = deluge.configmanager.get_config_dir("auth") + if os.path.exists(auth_file): + u = urlparse.urlsplit(uri) + for line in open(auth_file): + username, password = line.split(":") + if username == "localclient": + # We use '127.0.0.1' in place of 'localhost' just incase this isn't defined properly + hostname = u.hostname.replace("localhost", "127.0.0.1") + return u.scheme + "://" + username + ":" + password + "@" + hostname + ":" + str(u.port) + return None + def start(self): if self.gtkui_config["autoconnect"]: # We need to update the autoconnect_host_uri on connection to host @@ -195,30 +259,24 @@ class ConnectionManager(component.Component): """Updates the host status""" def update_row(model=None, path=None, row=None, columns=None): uri = model.get_value(row, HOSTLIST_COL_URI) - uri = "http://" + uri threading.Thread(target=self.test_online_status, args=(uri,)).start() try: online = self.online_status[uri] except: online = False + # Update hosts status if online: - image = gtk.STOCK_YES online = HOSTLIST_STATUS.index("Online") else: - image = gtk.STOCK_NO online = HOSTLIST_STATUS.index("Offline") + if urlparse.urlsplit(uri).hostname == "localhost" or urlparse.urlsplit(uri).hostname == "127.0.0.1": + uri = self.get_localhost_auth_uri(uri) + if uri == current_uri: - # We are connected to this host, so lets display the connected - # icon. - image = gtk.STOCK_CONNECT online = HOSTLIST_STATUS.index("Connected") - pixbuf = self.connection_manager.render_icon( - image, gtk.ICON_SIZE_MENU) - - model.set_value(row, HOSTLIST_COL_PIXBUF, pixbuf) model.set_value(row, HOSTLIST_COL_STATUS, online) current_uri = client.get_core_uri() @@ -260,12 +318,10 @@ class ConnectionManager(component.Component): # Check to see if a localhost is selected localhost = False - if uri.split(":")[0] == "localhost" or uri.split(":")[0] == "127.0.0.1": + u = urlparse.urlsplit(uri) + if u.hostname == "localhost" or u.hostname == "127.0.0.1": localhost = True - # Make actual URI string - uri = "http://" + uri - # Make sure buttons are sensitive at start self.glade.get_widget("button_startdaemon").set_sensitive(True) self.glade.get_widget("button_connect").set_sensitive(True) @@ -321,7 +377,11 @@ class ConnectionManager(component.Component): online = True host = None try: - host = xmlrpclib.ServerProxy(uri.replace("localhost", "127.0.0.1")) + u = urlparse.urlsplit(uri) + if u.hostname == "localhost" or u.hostname == "127.0.0.1": + host = xmlrpclib.ServerProxy(self.get_localhost_auth_uri(uri)) + else: + host = xmlrpclib.ServerProxy(uri) host.ping() except socket.error: online = False @@ -341,18 +401,32 @@ class ConnectionManager(component.Component): dialog.set_icon(common.get_logo(16)) hostname_entry = self.glade.get_widget("entry_hostname") port_spinbutton = self.glade.get_widget("spinbutton_port") + username_entry = self.glade.get_widget("entry_username") + password_entry = self.glade.get_widget("entry_password") response = dialog.run() if response == 1: + username = username_entry.get_text() + password = password_entry.get_text() + hostname = hostname_entry.get_text() + if not urlparse.urlsplit(hostname).hostname: + # We need to add a http:// + hostname = "http://" + hostname + u = urlparse.urlsplit(hostname) + if username and password: + host = u.scheme + "://" + username + ":" + password + "@" + u.hostname + else: + host = hostname + # We add the host - self.add_host(hostname_entry.get_text(), - port_spinbutton.get_value_as_int()) + self.add_host(host, port_spinbutton.get_value_as_int()) dialog.hide() def add_host(self, hostname, port): """Adds the host to the list""" - if hostname.startswith("http://"): - hostname = hostname[7:] + if not urlparse.urlsplit(hostname).scheme: + # We need to add http:// to this + hostname = "http://" + hostname # Check to make sure the hostname is at least 1 character long if len(hostname) < 1: @@ -407,18 +481,17 @@ class ConnectionManager(component.Component): row = self.liststore.get_iter(paths[0]) status = self.liststore.get_value(row, HOSTLIST_COL_STATUS) uri = self.liststore.get_value(row, HOSTLIST_COL_URI) - port = uri.split(":")[1] + u = urlparse.urlsplit(uri) if HOSTLIST_STATUS[status] == "Online" or\ HOSTLIST_STATUS[status] == "Connected": # We need to stop this daemon - uri = "http://" + uri # Call the shutdown method on the daemon core = xmlrpclib.ServerProxy(uri) core.shutdown() # Update display to show change self.update() elif HOSTLIST_STATUS[status] == "Offline": - self.start_localhost(port) + self.start_localhost(u.port) def start_localhost(self, port): """Starts a localhost daemon""" @@ -444,11 +517,11 @@ class ConnectionManager(component.Component): uri = self.liststore.get_value(row, HOSTLIST_COL_URI) # Determine if this is a localhost localhost = False - port = uri.split(":")[1] - if uri.split(":")[0] == "localhost": + u = urlparse.urlsplit(uri) + if u.hostname == "localhost" or u.hostname == "127.0.0.1": localhost = True - uri = "http://" + uri + if status == HOSTLIST_STATUS.index("Connected"): # Stop all the components first. component.stop() @@ -465,9 +538,14 @@ class ConnectionManager(component.Component): if localhost: self.start_localhost(port) # We need to wait for the host to start before connecting - while not self.test_online_status(uri): + auth_uri = None + while not auth_uri: + auth_uri = self.get_localhost_auth_uri(uri) time.sleep(0.01) - client.set_core_uri(uri) + + while not self.test_online_status(auth_uri): + time.sleep(0.01) + client.set_core_uri(auth_uri) self._update_list() self.hide() @@ -477,7 +555,11 @@ class ConnectionManager(component.Component): return # Status is OK, so lets change to this host - client.set_core_uri(uri) + if localhost: + client.set_core_uri(self.get_localhost_auth_uri(uri)) + else: + client.set_core_uri(uri) + self.hide() def on_chk_autoconnect_toggled(self, widget): diff --git a/deluge/ui/gtkui/glade/connection_manager.glade b/deluge/ui/gtkui/glade/connection_manager.glade index dbfe1b290..66b1f79ba 100644 --- a/deluge/ui/gtkui/glade/connection_manager.glade +++ b/deluge/ui/gtkui/glade/connection_manager.glade @@ -1,6 +1,6 @@ - + True @@ -378,6 +378,64 @@ 1 + + + True + 5 + + + True + Username: + + + False + False + + + + + True + True + + + 1 + + + + + False + 2 + + + + + True + 5 + + + True + Password: + + + False + False + + + + + True + True + + + 1 + + + + + False + 3 + + True