Add basic authentication -- your connectionmanager config will be cleared and this will likely break other UIs

This commit is contained in:
Andrew Resch 2008-12-10 07:56:40 +00:00
parent cc7a685397
commit 37c3b4334c
7 changed files with 291 additions and 53 deletions

View File

@ -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):

View File

@ -0,0 +1,85 @@
#
# authmanager.py
#
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
#
# 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

View File

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

View File

@ -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()

View File

@ -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

View File

@ -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):

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
<!--Generated with glade3 3.4.5 on Fri Nov 7 23:39:07 2008 -->
<!--Generated with glade3 3.4.5 on Tue Dec 9 21:46:39 2008 -->
<glade-interface>
<widget class="GtkDialog" id="connection_manager">
<property name="has_focus">True</property>
@ -378,6 +378,64 @@
<property name="position">1</property>
</packing>
</child>
<child>
<widget class="GtkHBox" id="hbox5">
<property name="visible">True</property>
<property name="spacing">5</property>
<child>
<widget class="GtkLabel" id="label5">
<property name="visible">True</property>
<property name="label" translatable="yes">Username:</property>
</widget>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
</packing>
</child>
<child>
<widget class="GtkEntry" id="entry_username">
<property name="visible">True</property>
<property name="can_focus">True</property>
</widget>
<packing>
<property name="position">1</property>
</packing>
</child>
</widget>
<packing>
<property name="expand">False</property>
<property name="position">2</property>
</packing>
</child>
<child>
<widget class="GtkHBox" id="hbox6">
<property name="visible">True</property>
<property name="spacing">5</property>
<child>
<widget class="GtkLabel" id="label6">
<property name="visible">True</property>
<property name="label" translatable="yes">Password:</property>
</widget>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
</packing>
</child>
<child>
<widget class="GtkEntry" id="entry_password">
<property name="visible">True</property>
<property name="can_focus">True</property>
</widget>
<packing>
<property name="position">1</property>
</packing>
</child>
</widget>
<packing>
<property name="expand">False</property>
<property name="position">3</property>
</packing>
</child>
<child internal-child="action_area">
<widget class="GtkHButtonBox" id="dialog-action_area3">
<property name="visible">True</property>