Patch: MLDonkey und LDAP Authentifizierung

Aus NOBAQ
Zur Navigation springenZur Suche springen
Openldap-logo.png
Mldonkey.PNG

Seit kurzem hat MLDonkey ein User- und Gruppensystem, wobei man Downloads Gruppen und Benutzer zuordnen kann die dann auch nur von diesen gesehen werden. Da in unserem WLAN (stiftingtal.net) bereits ein LDAP-Server mit allen Usern existiert wäre es nett, wenn auch MLDonkey gegen diesen LDAP Server authentifizieren könnte. Nachdem ich bis jetzt keine Lösung dafür gefunden habe, habe ich vor einiger Zeit einen Patch geschrieben, der es über einen Umweg erlaubt, MLdonkey gegen LDAP zu authentifizieren: Ein Script das bei der Authentifizierung aufgerufen wird. Leider bin ich mir mit dem MLDonkey Entwickler nicht über die Implementierungsdetails einig, sodass der Patch wahrscheinlich nicht Einzug in den offiziellen CVS-Branch finden wird. Wer dennoch eine Möglichkeit sucht, MLDonkey gegen LDAP (oder auch mySQL, SQL, PAM, RADIUS, …) zu authentifizieren, dem hilft vielleicht der Patch, den ich hier beschreibe.


Ich spreche leider OCaml nicht besonders gut, weshalb mir eine ordentliche Codeänderung sehr schwer fiel. Ausserdem würde man mit einer C-Anbindung arbeiten müssen, denn OCaml selbst kann keinen LDAP Zugriff.

Deswegen gliedert sich meine Lösung in zwei Teile, die leider nicht sehr elegant ist: Ein Teil ist ein Script das vor dem Starten von MLdonkey aufgerufen wird, alle User und Gruppen aus dem LDAP Server abfrägt und daraus eine users2.ini bzw. eine users.ini erzeugt. Der zweite Teil ist ein Patch für MLdonkey, der es erlaubt, für die Authentifizierung ein externes Programm aufzurufen. Dieses ruft dann ein Shellscript auf das wiederum ldapwhoami aufruft und dementsprechend Erfolgreich/Nicht-erfolgreich zurückliefert.

Vorbereitung: der LDAP Server, das Schema

Natürlich kann auch eine vorhandene Infrastruktur genommen werden, dann braucht nur das Script verändert werden, ich habe jedoch meinen LDAP-Server um Schemas für MLDonkey erweitert. Am besten erstellt man eine /etc/ldap/schema/mldonkey.schema Datei:

##########################
##
## MLdonkey
##
##########################

attributetype ( 1.1.2.1.1 NAME 'donkeyCommit'
        DESC 'Commit-Directory fuer MLdonkey'
        EQUALITY caseExactIA5Match
        SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )

attributetype ( 1.1.2.1.2 NAME 'donkeyPrimaryGroup'
        DESC 'Standardgruppe fuer MLdonkey'
        EQUALITY caseExactIA5Match
        SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )

attributetype ( 1.1.2.1.3 NAME 'donkeyOtherGroups'
        DESC 'Benutzergruppen fuer MLdonkey'
        EQUALITY caseExactIA5Match
        SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{16} )

attributetype ( 1.1.2.1.4 NAME 'donkeyGroupAdmin'
        DESC 'Ist diese Gruppe eine Administratorgruppe'
        EQUALITY caseExactIA5Match
        SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )

objectclass ( 1.1.2.2.2 NAME 'stiftingtalDonkey' SUP top AUXILIARY
        DESC 'Benutzer die MLdonkey verwenden duerfen'
        MUST ( uid $ donkeyPrimaryGroup $ userPassword )
        MAY ( donkeyOtherGroups $ donkeyCommit ) )

objectClass ( 1.1.2.2.5 NAME 'stiftingtalDonkeyGroup' SUP top STRUCTURAL
        DESC 'Gruppen fuer MLdonkey'
        MUST ( donkeyGroupAdmin ) )

Die ganze Datei kann hier heruntergeladen werden: Media:mldonkeyschema.txt

Das Synchronisationsscript

Es kann hier heruntergeladen werden: Media:donkey_ldappl.txt (Ich veröffentliche es hier unter der GNU GPL!)

#!/usr/bin/perl

# (c) 2007 by Nikolaus Hammler <niki.hammler@nobaq.net>
# Please note that this piece of code is licensed under the GNU GPL!!

use Net::LDAPS;
use strict;

my $USERS_INI = "/home/donkey/root/.mldonkey/users.ini";

my $ADMIN_PASS = '(MD4-crypted secret for admin user)';

my $LDAP_SERVER = "ianus.intern.stiftingtal.net";
my $BINDDN = "uid=reader,dc=intern,dc=stiftingtal,dc=net";
my $BINDPW = "secret";

my $ldap = Net::LDAPS->new($LDAP_SERVER) or die "$@";

my $mesg = $ldap->bind($BINDDN, password => $BINDPW, version => 3) or die "$@";

$mesg->code && die $mesg->error;

$mesg = $ldap->search(base => "ou=users,dc=intern,dc=stiftingtal,dc=net", scope => "one", filter => "(objectClass=stiftingtalDonkey)", attrs => ['uid', 'mail', 'donkeyPrimaryGroup', 'donkeyOtherGroups', 'donkeyCommit', 'donkeyCommit']);

$mesg->code && die $mesg->error;


my @entries = $mesg->entries;

my $users = "  (admin, \"$ADMIN_PASS\");";

my $users2 = "";

my @groups = ();


foreach my $entr (@entries)
{
	my $uid = $entr->get_value('uid');
	my $mail = $entr->get_value('mail');
	my $dgroup = $entr->get_value('donkeyPrimaryGroup');
	my $commit = $entr->get_value('donkeyCommit');
	
	my $othergroups = "";
	foreach ($entr->get_value('donkeyOtherGroups'))
	{
		$othergroups .= "\n      $_;";
		push(@groups, $_);
	}
	
	if(!$commit)
	{
		$commit = $uid;
	}

	push(@groups, $dgroup);	

	$users .= "\n  ($uid, \"11111111111111111111111111111111\");";
	
	$users2 .= "\n";
	$users2 .= "  {     user_name = $uid\n";
	#$users2 .= "     user_pass = \"00000000000000000000000000000000\"\n";
	$users2 .= "     user_pass = \"11111111111111111111111111111111\"\n";
	$users2 .= "     user_groups = [\n";
	$users2 .= "      $dgroup;";
	$users2 .= $othergroups;
	$users2 .= "]\n";
	$users2 .= "     user_default_group = $dgroup\n";
	$users2 .= "     user_mail = \"$mail\"\n";
	$users2 .= "     user_commit_dir = $commit\n";
	$users2 .= "     user_max_concurrent_downloads = 0\n";
	$users2 .= "};";
}

system("savelog -m 600 -c 30 $USERS_INI");
open(USERS, ">$USERS_INI") || die("Could not write to $USERS_INI");

print USERS "\n groups = [\n";
print USERS "  {     group_name = mldonkey\n";
print USERS "     group_admin = true\n";
print USERS "};";
my %temp;
@temp{@groups} = ();
foreach my $group (keys %temp)
{
	print USERS "\n";
	print USERS "  {     group_name = $group\n";
	
	## URGS!
	if($group eq "admins")
	{
		print USERS "     group_admin = true\n";
	}
	else
	{
		print USERS "     group_admin = false\n";
	}
	print USERS "};";
}
print USERS "]\n\n\n";


print USERS " users2 = [\n";
print USERS "  {     user_name = admin\n";
print USERS "     user_pass = \"$ADMIN_PASS\"\n";
print USERS "     user_groups = [\n";
print USERS "      mldonkey;]\n";
print USERS "     user_default_group = mldonkey\n";
print USERS "     user_mail = \"\"\n";
print USERS "     user_commit_dir = \"\"\n";
print USERS "     user_max_concurrent_downloads = 0\n";
print USERS "};";
print USERS $users2;
print USERS "]\n\n\n";

print USERS " users = [\n$users]\n\n\n";
close(USERS);

system("chown donkey.donkey $USERS_INI");
system("chmod 600 $USERS_INI");

Das Perl-Script verbindet sich zum LDAP Server und schreibt die users.ini aus den Daten des LDAP Servers. Das Script muss vermutlich an die jeweiligen Gegebenheiten angepasst werden. ACHTUNG! MLDonkey ist ist sehr störrisch wenn Kleinigkeiten in der ini-Datei nicht passen!

Nun fügt man den Aufruf nur mehr in das init-Script von mldonkey ein, sodass das Script jedesmal gestartet wird, wenn MLDonkey gestartet wird.

Der MLDonkey Patch

Der Patch für MLDonkey kann hier bezogen werden: Media:auth_cmdpatch.txt. Achtung, ich stelle ihn ebenfalls unter die GNU GPL!!

diff -urN mldonkey/src/daemon/common/commonOptions.ml mldonkey_with_authpatch/src/daemon/common/commonOptions.ml
--- mldonkey/src/daemon/common/commonOptions.ml	2007-04-23 00:31:53.000000000 +0200
+++ mldonkey_with_authpatch/src/daemon/common/commonOptions.ml	2007-04-26 18:56:37.000000000 +0200
@@ -1363,7 +1358,26 @@
   "Regexp of comments to filter out, example: string1|string2|string3"
     string_option "http://|https://|www\\."
 
+let auth_cmd = define_option current_section ["auth_cmd"]
+  "A command that is called when authorizing a user (i.e. on startup and
+  on auth command. The following environment variables are set:
+    $USERNAME - the username
+    $PASSWORD - the users password
+  
+  The command should return a value indicating if the authentication
+  is successful:
+    0 - The authentication is immidiately accepted IF the user exists
+    1 - The authentication is immidiately rejected
+    2 - An addition check against the MD4 has is performed.
+  
+  If the user does not exist in the internal database, the authentication
+  will fail, also when the program returns 0.
+  
+  If this value is not set, a normal authentication against the internal
+  database is performed.
+  If this option is set and there is an error running the command the
+  authentication will fail."
+    string_option ""
 
 
 (*************************************************************************)
diff -urN mldonkey/src/daemon/common/commonUserDb.ml mldonkey_with_authpatch/src/daemon/common/commonUserDb.ml
--- mldonkey/src/daemon/common/commonUserDb.ml	2006-11-09 22:32:26.000000000 +0100
+++ mldonkey_with_authpatch/src/daemon/common/commonUserDb.ml	2007-04-26 19:04:41.177275304 +0200
@@ -313,9 +313,32 @@
 let user2_user_set_password user pass_string =
   user.user_pass <- Md4.string pass_string
 
-let valid_password user pass =
+let valid_password user pass = 
   try
-    user2_user_password user = Md4.string pass
+    if !!auth_cmd <> "" && user2_user_exists user
+    then
+      try
+        let pid = Unix.create_process_env !!auth_cmd
+          [|""|]
+          (Array.of_list (Printf.sprintf "USERNAME=%s" user :: Printf.sprintf "PASSWORD=%s" pass :: []))
+          Unix.stdin Unix.stdout Unix.stderr in
+
+        let _pid, status = Unix.waitpid [] pid in
+        match status with
+          | Unix.WEXITED exitcode -> 
+            (match exitcode with
+	      | 0 -> true
+	      | 1 -> false
+	      | _ -> user2_user_password user = Md4.string pass
+	    )
+          | Unix.WSIGNALED signal -> false
+          | Unix.WSTOPPED signal -> false
+      with Unix.Unix_error (code, f, arg) -> false
+      (*
+      Printf.sprintf "%s failed%s: %s" f (if arg = "" then "" else " on " ^ arg) (Unix.error_message code)
+      *)
+    else
+      user2_user_password user = Md4.string pass
   with Not_found -> false
 
 let has_empty_password user =

Der Patch fügt eine neue Konfigurationsdirektive "auth_cmd" an. Setzt man diese auf ein externes Script, so wird Usernamen und Passwort als Environmentvariable übergeben (Parameter wären usicher). Das Script kann nun die Berechtigung überprüfungen und gibt den Status anhand des return-Codes zurück. 0 bedeutet: Die Authentifikation ist sofort erfolgreich, aber nur wenn der Benutzer in MLdonkey existiert. 1: Die Authentifikation schlägt sofort fehl. 2: MLDonkey soll zusätzlich wie gewöhnlich das Passwort überprüfen. Nähere Details im Sourcecode.

Nun fehlt nur mehr das Authentifikationsscript (bei mir: auth_ldap.sh). Da ich den Esel in eine chroot-Umgebung eingesperrt habe, habe ich ein statisches busybox erstellt:

#!/bin/busybox sh
LDAP_HOST="ianus.intern.stiftingtal.net"
echo -n "Authenticating user $USERNAME..."
if [ "$USERNAME" = "admin" ]
then
	echo "passing to MLdonkey"
	exit 2
fi
echo /bin/ldapwhoami -ZZ -x -h $LDAP_HOST -D "uid=$USERNAME,ou=users,dc=intern,dc=stiftingtal,dc=net" -w "$PASSWORD"
if /bin/ldapwhoami -ZZ -x -h $LDAP_HOST -D "uid=$USERNAME,ou=users,dc=intern,dc=stiftingtal,dc=net" -w "$PASSWORD"
then
	echo "successful!"
	exit 0
fi
echo "failed"
exit 1


Man sieht, dass der Administrator immer über MLDonkey authentifiziert wird (aus Sicherheitsgründen).

Nun muss nur noch ldapwhoami in das chroot kopiert werden und mit ihm alle benötigten Bibliotheken. Diese findet man mit dem Programm "ldd".

Nun sollte MLDonkey (nicht elegant aber doch) gegen den LDAP Server authentifizieren. Diese Konstellation läuft bei uns nun seit ein paar Monaten problemlos.

Natürlich kann man den Patch wie oben gesagt auch verwenden um gegen RADIUS, mySQL, PAM, Kerberos und Co zu authentifizieren!

Feedback ist willkommen!