Initial import
authorjgg <>
Wed, 22 Sep 1999 03:19:03 +0000 (03:19 +0000)
committerjgg <>
Wed, 22 Sep 1999 03:19:03 +0000 (03:19 +0000)
46 files changed:
doc/apache-config.txt [new file with mode: 0644]
doc/makefile [new file with mode: 0644]
doc/samples/ud-generate [new file with mode: 0644]
doc/samples/ud-replicate [new file with mode: 0644]
doc/slapd-config.txt [new file with mode: 0644]
doc/ud-generate.8.yo [new file with mode: 0644]
doc/ud-gpgimport.8.yo [new file with mode: 0644]
doc/ud-info.1.yo [new file with mode: 0644]
doc/ud-useradd.8.yo [new file with mode: 0644]
doc/ud-userimport.8.yo [new file with mode: 0644]
doc/ud-xearth.1.yo [new file with mode: 0644]
templates/list-subscribe [new file with mode: 0644]
templates/passwd-changed [new file with mode: 0644]
templates/ping-reply [new file with mode: 0644]
templates/welcome-message-60000 [new file with mode: 0644]
templates/welcome-message-800 [new file with mode: 0644]
ud-arbimport [new file with mode: 0755]
ud-emailmatcher [new file with mode: 0755]
ud-forwardlist [new file with mode: 0755]
ud-generate [new file with mode: 0755]
ud-gpgimport [new file with mode: 0755]
ud-homecheck [new file with mode: 0755]
ud-info [new file with mode: 0755]
ud-ldapshow [new file with mode: 0755]
ud-mailgate [new file with mode: 0755]
ud-passchk [new file with mode: 0755]
ud-replicate [new file with mode: 0755]
ud-useradd [new file with mode: 0755]
ud-userimport [new file with mode: 0755]
ud-xearth [new file with mode: 0755]
userdir-ldap.conf [new file with mode: 0644]
userdir_gpg.py [new file with mode: 0644]
userdir_ldap.py [new file with mode: 0644]
web/Util.pm [new file with mode: 0644]
web/domains.tab [new file with mode: 0644]
web/fetchkey.cgi [new file with mode: 0755]
web/login.cgi [new file with mode: 0755]
web/login.html [new file with mode: 0644]
web/logout.cgi [new file with mode: 0755]
web/search.cgi [new file with mode: 0755]
web/searchform.html [new file with mode: 0644]
web/searchhelp.html [new file with mode: 0644]
web/searchresults.html [new file with mode: 0644]
web/settings.cfg [new file with mode: 0644]
web/update.cgi [new file with mode: 0755]
web/update.html [new file with mode: 0644]

diff --git a/doc/apache-config.txt b/doc/apache-config.txt
new file mode 100644 (file)
index 0000000..461eb5b
--- /dev/null
@@ -0,0 +1,15 @@
+To setup apache for use with the web database access scripts use:
+
+<VirtualHost myip>
+  ServerAdmin webmaster@mydomain.com
+  DocumentRoot /var/www/userdir-ldap
+  ServerName db.mydomain.com
+  DirectoryIndex /search.cgi
+</VirtualHost>
+       
+<directory /var/www/userdir-ldap>
+  Options +ExecCGI
+  AllowOverride All
+  AddHandler cgi-script .cgi     
+</directory>
+             
diff --git a/doc/makefile b/doc/makefile
new file mode 100644 (file)
index 0000000..4e314b3
--- /dev/null
@@ -0,0 +1,11 @@
+.SILENT:
+
+MANPAGES = ud-generate.8 ud-gpgimport.8 ud-info.1 ud-xearth.1 ud-useradd.8 \
+           ud-userimport.8
+
+all: $(MANPAGES)
+
+$(MANPAGES) :: % : %.yo
+       echo Creating man page $@
+       yodl2man -o $@ $<
+               
diff --git a/doc/samples/ud-generate b/doc/samples/ud-generate
new file mode 100644 (file)
index 0000000..24c003c
--- /dev/null
@@ -0,0 +1,2 @@
+# Cron tab for the generate operation, 15 min cycle. Put it in /etc/cron.d/
+08,23,38,53 *     * * *     sshdist  if [ -x /usr/bin/ud-generate ]; then /usr/bin/ud-generate; fi
diff --git a/doc/samples/ud-replicate b/doc/samples/ud-replicate
new file mode 100644 (file)
index 0000000..7f41347
--- /dev/null
@@ -0,0 +1,3 @@
+# Cron tab for the replicate operation, 15 min cycle, offset from generate.
+# Put it in /etc/cron.d/
+10,25,40,55 *     * * *     root  if [ -x /usr/bin/ud-replicate ]; then /usr/bin/ud-replicate; fi
diff --git a/doc/slapd-config.txt b/doc/slapd-config.txt
new file mode 100644 (file)
index 0000000..0cc7546
--- /dev/null
@@ -0,0 +1,45 @@
+Most of the configuration of the ldap server has to do with getting correct
+access controls to keep the data safe. Here is a sample:
+
+# Turn on automatic last modification time 
+lastmod on
+
+# Index some things
+index uid eq
+index keyfingerprint eq
+index cn,sn approx,sub,eq
+
+# Administrate 
+#rootdn "uid=admin,ou=users,dc=debian,dc=org"
+#rootpw 
+
+# Restrict reading/modification of the password to administration and self
+access to attrs=userpassword
+        by self write
+        by dn="uid=admin,ou=users,dc=debian,dc=org" write
+        by * compare   
+
+# Reading of eamil forward is restricted by machine
+access to attrs=emailforward
+        by dn="uid=admin,ou=users,dc=debian,dc=org" write
+        by self write
+        by addr=127.0.0.1 read
+       by domain=.*\.debian\.org read
+       by * none
+       
+# Public self modifyable attributes
+access to attrs=c,l,loginShell,ircNick,labeledURL
+        by dn="uid=admin,ou=users,dc=debian,dc=org" write
+        by self write
+       
+# Private self modifyable fields that are still viewable by other users
+# in the directory.
+access to attrs=facsimileTelephoneNumber,telephoneNumber,postalAddress,postalCode,loginShell,onvacation
+        by dn="uid=admin,ou=users,dc=debian,dc=org" write
+        by self write
+       by dn="uid=.*,ou=users,dc=debian,dc=org" read
+       by * none
+       
+# Remainder    
+access to * 
+        by dn="uid=admin,ou=users,dc=debian,dc=org" write
diff --git a/doc/ud-generate.8.yo b/doc/ud-generate.8.yo
new file mode 100644 (file)
index 0000000..f3f9e38
--- /dev/null
@@ -0,0 +1,48 @@
+mailto(admin@db.debian.org)
+manpage(ud-generate)(1)(17 Sep 1999)(userdir-ldap)()
+manpagename(ud-generate)(Produce machine specific formatted version of the
+directory)
+
+manpagesynopsis()
+  ud-generate
+
+manpagedescription()
+
+ud-generate prouces machine specific versions of the directory in the
+following formats:
+
+itemize(
+  it() passwd file [in normal and DB form]
+  it() shadow file [in normal and DB form]
+  it() group file [in normal and DB form]
+  it() Exim forwarding file
+)
+
+Generation of the files is controlled by the configuration file
+bf(/etc/userdir-ldap/ud-generate.conf). The output is placed in
+bf(/var/cache/userdir-ldap/hosts/<hostname>/). Each host listed in the
+configuration file has its own home dir path and its own list of groups that
+are allowed to login to the machine.
+
+The format of the configuration file is a one line per host with these fields:
+verb(host homedirpath group1 group2 ...)
+Only users who are a member of the named groups are emitted to the output
+files.
+
+Authorization to read protected entries from the directory is achieved by
+reading a username and password from the pass- file in the userdir-ldap
+directory.
+
+manpagefiles()
+itemize(
+  it() /etc/userdir-ldap/userdir-ldap.conf
+  Configuration variables to select what server and what base DN to use.
+  it() /etc/userdir-ldap/ud-generate.conf
+  Configuration variables to determine how hosts are generated.
+  it() /etc/userdir-ldap/pass-<uid>
+  Directory authentication credentials
+)
+
+manpageauthor()
+userdir-ldap was written by Jason Gunthorpe <jgg@debian.org>.
+
diff --git a/doc/ud-gpgimport.8.yo b/doc/ud-gpgimport.8.yo
new file mode 100644 (file)
index 0000000..c9f4976
--- /dev/null
@@ -0,0 +1,67 @@
+mailto(admin@db.debian.org)
+manpage(ud-gpgimport)(8)(17 Sep 1999)(userdir-ldap)()
+manpagename(ud-gpgimport)(Key Ring Syncronization utility)
+
+manpagesynopsis()
+  ud-gpgimport [options] [keyrings]
+   
+manpagedescription()
+ud-gpgimport maintains the key fingerprint to user ID mapping in the
+directory. It takes as input a set of keyrings that represent all keys
+belonging to all users in the directory. It then reads each key and attempts
+to match it up to a user already in the directory. This matching process has
+several steps:
+
+1) If the key fingerprint already exists in the directory then the key is
+assumed to be already assigned so it is ignored
+
+2) If the key email address is in the override table then the key is
+assigned to the user in the override table
+
+3) An exact match of first name + last name from the key's primary UID is 
+performed against the directory. If a single hit is found then the key is
+assigned to that user
+
+4) If the email address in the key is within the debian.org domain then the
+key is assigned to the to the mentioned user if the last name from the
+directory appears some place in the key UID. This is called an bf(EmailAppend)
+hit.
+
+5) Nothing is done, but a soundex matcher is invoked to give some suggestions 
+on who the key may belong to.
+
+An override table is used to deal with keys that do not exactly match any
+user in the directory. The override table takes the email address that
+appears on a key and maps it to a uid in the directory.
+
+By default the matcher only generates a report on what it would do but makes
+no changes. The -a option must be given and an password entered to allow
+modification.
+
+GnuPG must be properly installed in the system to extract the key
+information from the key rings.
+
+manpageoptions()
+startdit()
+dit(bf(-a))
+Enable modification of the directory.
+
+dit(bf(-u))
+Set the authentication user. This is the user who's authority is used when 
+accessing the LDAP directory. The default is to use the current system user
+name.
+
+dit(bf(-m))
+Set the override file to use. The format of the override file is a map of key
+email address to uid, eg verb(foo@bar.com: baz)
+enddit()
+
+manpagefiles()
+itemize(
+  it() /etc/userdir-ldap/userdir-ldap.conf
+  Configuration variables to select what server and what base DN to use.
+)
+       
+manpageauthor()
+userdir-ldap was written by Jason Gunthorpe <jgg@debian.org>.
+
diff --git a/doc/ud-info.1.yo b/doc/ud-info.1.yo
new file mode 100644 (file)
index 0000000..119d95d
--- /dev/null
@@ -0,0 +1,151 @@
+mailto(admin@db.debian.org)
+manpage(ud-info)(1)(17 Sep 1999)(userdir-ldap)()
+manpagename(ud-info)(Command line LDAP user record manipulator)
+
+manpagesynopsis()
+  ud-info [options]
+   
+manpagedescription()
+
+ud-info is the command-line tool for end users to manipulate their own
+database information and to view other users information. It also provides
+root functions which when combined with sufficient LDAP privilages allow
+an administrator to completely manipulate a users record.
+
+The defined fields are:
+itemize(
+  it() cn - Common (first) name. [root]
+  it() mn - Middle name or initial. [root]
+  it() sn - Surname (last name). [root]
+  it() cn - ISO 3166 country code, see file(/usr/share/zoneinfo/iso3166.tab)
+            Should be upper case.
+  it() ircnick - IRC nickname.
+  it() l - City name, state/province. The part of a mailing address that is
+           not the street address. e.g.: Dallas, Texas
+  it() postalcode - Postal Code or ZIP Code 
+  it() postaladdress - Complete mailing address including postal codes and
+           country designations. Newlines are seperated by a $ character. The
+          address should be formed exactly as it would appear on a parcel.
+  it() latitude/longitude - The physical latitude and longitude. This 
+           information is typically used to generate an xearth marker file. 
+          See the discussion below on position formats.
+  it() facsimiletelephonenumber - FAX phone number, do not forget to specify a
+           country code [North Armerica is +1].
+  it() telephonenumber - Voice phone number. 
+  it() loginshell - Full path to the prefered Unix login shell. e.g. file(/bin/bash)
+  it() emailforward - Destination email address.
+  it() userpassword - Encrypted version of the password. [root]
+  it() supplementarygid - A list of group names that the user belongs.
+           This field emulates the functionality of the traditional Unix group
+          file. [root]
+  it() onvacation - A message indicating that the user is on vacation. The
+           time of departure and expected return date should be included as
+           well as any special instructions.
+  it() comment - Administrative comment about the account. [root]
+  it() labeledurl - User's web site.
+)  
+
+When prompted for a password it is possible to enter a blank password and
+access the database anonymously. This is useful to check PGP key
+fingerprints, for instance.
+
+manpagesection(SECURITY AND PRIVACY)
+Three levels of information security are provided by the database. The first
+is completely public information that anyone can see either by issuing an
+LDAP query or by visiting the web site. The next level is "maintainer-only"
+information that requires authentication to the directory before it can be
+accessed. The final level is admin-only or user-only information; this
+information can only be viewed by the user or an administrator. 
+
+Maintainer-only information includes precise location information
+[postalcode, postal address, lat/long] telephone numbers, and the vacation
+message.
+
+Admin-only/maintainer-only information includes email forwarding and the 
+encrypted password. Note that email forwarding is necessarily publicly viewable 
+from accounts on the actual machines.
+
+manpagesection(LAT/LONG POSITION)
+There are three possible formats for giving position information and several
+online sites that can give an accurate position fix based on mailing address.
+
+startdit()
+dit(Decimal Degrees)
+The format is +-DDD.DDDDDDDDDDDDDDD. This is the format programs like
+bf(xearth)
+use and the format that many positioning web sites use. However typically
+the precision is limited to 4 or 5 decimals.
+
+dit(Degrees Minutes (DGM))
+The format is +-DDDMM.MMMMMMMMMMMMM. It is not an arithmetic type, but a
+packed representation of two seperate units, degrees and minutes. This
+output is common from some types of hand held GPS units and from NMEA format
+GPS messages.
+
+dit(Degrees Minutes Seconds (DGMS))
+The format is +-DDDMMSS.SSSSSSSSSSS. Like DGM, it is not an arithmetic type but
+a packed representation of three seperate units, degrees minutes and
+seconds. This output is typically derived from web sites that give 3 values
+for each position. For instance 34:50:12.24523 North might be the position
+given, in DGMS it would be +0345012.24523.
+enddit()
+
+For Latitude + is North, for Longitude + is East. It is important to specify
+enough leading zeros to dis-ambiguate the format that is being used if your
+position is less than 2 degrees from a zero point.
+
+So locations to find positioning information are:
+
+itemize(
+ it() Good starting point - http://www.ckdhr.com/dns-loc/finding.html
+ it() AirNav - GPS locations for airports around the world http://www.airnav.com/
+ it() GeoCode - US index by ZIP Code http://www.geocode.com/eagle.html-ssi
+ it() Map Blast! Canadian, US and some European maps - http://www.mapblast.com/
+ it() Australian Database http://www.environment.gov.au/database/MAN200R.html
+ it() Canadian Database http://GeoNames.NRCan.gc.ca/
+ it() GNU Timezone database, organized partially by country /usr/share/zoneinfo/zone.tab
+)
+
+Remember that we are after reasonable coordinates for drawing an xearth
+graph and looking for people to sign keys, not for coordinates accurate
+enough to land an ICBM on your doorstop!
+
+manpagesection(Editing Supplemental GIDs)
+When the root function is activated then the supplemental GIDs can be
+manipulated as a list of items. It is possible to add and remove items from
+the list by name. Proper prompts are given.
+
+manpageoptions()
+startdit()
+dit(bf(-a))
+Set the authentication user. This is the user whose authority is used when 
+accessing the LDAP directory. The default is to use the current system user
+name.
+
+dit(bf(-u))
+Select the user whose fields will be displayed/edited. The default is to use
+the current system user name.
+
+dit(bf(-c))
+Set both the authentication user and the target user. This option is useful
+if the login name does not match the user who is operating the program.
+
+dit(bf(-r))
+Enable root functions. This enables more options to allow changing
+any entry in the directory. This function only has meaning if the
+authentication user has the necessary permissions at the LDAP server.
+
+dit(bf(-n))
+No actions. Anonymously bind and show the information for the user and then
+exit.
+enddit()
+
+manpagefiles()
+itemize(
+  it() /etc/userdir-ldap/userdir-ldap.conf
+  Configuration variables to select what server and what base DN to use.
+)
+       
+manpageauthor()
+userdir-ldap was written by Jason Gunthorpe <jgg@debian.org>.
+
diff --git a/doc/ud-useradd.8.yo b/doc/ud-useradd.8.yo
new file mode 100644 (file)
index 0000000..96df739
--- /dev/null
@@ -0,0 +1,107 @@
+mailto(admin@db.debian.org)
+manpage(ud-useradd)(8)(17 Sep 1999)(userdir-ldap)()
+manpagename(ud-useradd)(Interactive user addition program)
+
+manpagesynopsis()
+  ud-useradd [options]
+   
+manpagedescription()
+ud-uaseradd is an interactive program for adding new users to the directory. 
+It takes care of all steps of user addition including generating a random
+new password and sending a greeting form letter.
+
+The operator is taken through a set of prompts to determine the data to be
+loaded into the directory:
+
+startdit()
+dit(PGP Key Fingerprint)
+The first prompt is to determine the user's PGP key. For this to be
+successfull the key must have already been loaded into a keyring referenced
+by the GPG configuration file. The search specification is passed directly
+to GPG and then the results are presented, when a single match is found then
+it is taken as the correct key.
+
+dit(Account Name)
+This is the UID of the user, their login name and email local part. If the
+name already exists then it is possible to update the account directly. This
+feature should probably be used very infrequently as ud-info can adjust
+all of the values.
+
+dit(First, Last and Middle Name)
+The proper name of the user, split into three components. The name
+name attached to the PGP key is provided as a default. In most cases this
+should be adaquate and correct.
+
+dit(Email Forwarding Address)
+The address that all general email should be forwarded to. This is analogous 
+to a .forward file in the users home directory except that it applies
+globally to all machines. The email address attached to the PGP key is
+provided as a default.
+
+dit(Debian-Private Subscription)
+The address the user should be subscribed to debian-private with. Currently
+this sets the field in the DB and emails a subscription form to the
+list server.
+
+dit(Group ID Number)
+Main group the user will be part of. The group the user is assigned to
+determines which welcome form they are sent. The default is taken from
+the global configuration file
+
+dit(UID)
+The uid is selected automatically based on the first found free UID.
+
+dit(Password)
+The password can be specified if the user is not legaly able to use
+encryption (they live in France for instance) otherwise pressing enter at 
+this prompt will generate a random new password. The password to be entered
+is the plain text version, the script will crypt it automatically.
+enddit()
+
+After the information has been collected a summary is displayed and
+confirmation is required to proceed. Once confirmed the script will create a
+new entry and fill it with the given values. Then it will open the greeting
+form bf(/etc/userdir-ldap/templates/welcome-message-<GID>) and perform a
+variable substitution before sending it. Then the debian-private subscription
+form is sent.
+
+It is expected that the PGP key of the user has already been inserted into a
+local keyring known to GPG.
+
+manpagesection(Substitution Variables)
+A number of values are provided as substitution variables for the greeting
+and subscription message, they are:
+
+itemize(
+  it() __REALNAME__ The combined First/Middle/Last name
+  it() __WHOAMI__ The invoking user ID [unix ID]
+  it() __DATE__ The current date in RFC 822 form
+  it() __LOGIN__ The new users login ID
+  it() __PRIVATE__ The address to subscribe to debian-private
+  it() __EMAIL__ The normal email address of the user
+  it() __PASSWORD__ An ascii armored PGP packet containing the users 
+       password.
+  it() __LISTPASS__ The contents of the file ~/.debian-lists_passwd
+)
+
+manpageoptions()
+startdit()
+dit(bf(-u))
+Set the authentication user. This is the user who's authority is used when 
+accessing the LDAP directory. The default is to use the current system user
+name.
+enddit()
+
+manpagefiles()
+itemize(
+  it() /etc/userdir-ldap/userdir-ldap.conf
+  Configuration variables to select what server and what base DN to use.
+  it() /etc/userdir-ldap/templates/welcome-message-<GID>
+  The welcoming message to send to the user. Each primary group has its
+  own message
+  it() ~/.debian-lists_passwd
+  Authentication password for the list server
+)
+
+manpageauthor()
+userdir-ldap was written by Jason Gunthorpe <jgg@debian.org>.
diff --git a/doc/ud-userimport.8.yo b/doc/ud-userimport.8.yo
new file mode 100644 (file)
index 0000000..73dad01
--- /dev/null
@@ -0,0 +1,76 @@
+mailto(admin@db.debian.org)
+manpage(ud-userimport)(1)(17 Sep 1999)(userdir-ldap)()
+manpagename(ud-userimport)(Perform initial import of date)
+
+manpagesynopsis()
+  ud-userimport [options]
+   
+manpagedescription()
+
+ud-userimport is the utility that is used to initially load data into the
+directory. It takes as input a set of normal unix password, group and shadow
+files and loads their contents. Also it provide enough functionality to
+allow simple additions at a later date. 
+
+Before attempting to import the data the passwd file should be sanitized
+of any system entries and the GECOs fields should be cleaned of any
+strangeness users may have inserted.
+
+Next the passwd file alone should be added using the command
+verb(ud-userimport -a -p passwd)
+The passwd file will be loaded into the
+empty directory and new entries created for all the users.
+
+The shadow file does not have to be santized, importing it without the -a
+option will automatically skip any records that are not needed.
+The command to use is verb(ud-userimport -s shadow)
+
+Like the passwd file the group file needs to be cleaned of system groups and
+groups that are no longer needed. It is not necessary to remove non-existant
+users from the group lists, they will be automatically ignored. Like for
+the shadow file the command is verb(ud-userimport -a -g group)
+
+After the initial import is completed the ud-info tool can be used to
+manipulate the user records, however new groups can most easially be created 
+by giving a file containing only a single group (and its initial membership)
+to ud-userimport.
+
+The importer is optimized to get good speed on updates through the use
+of the async ldap mechanism. If errors are found in the import of the 
+passwd file or shadow file it is possible to re-run the import command 
+(without the -a option) to freshen the data set.
+
+Aside from the evident transformations, the splitter also processes the 
+unix gecos field into split first/last/middle names and it also sanitizes
+the gecos field to follow normal Debian convetions.
+
+manpageoptions()
+startdit()
+dit(bf(-u))
+Set the authentication user. This is the user who's authority is used when 
+accessing the LDAP directory. The default is to use the current system user
+name.
+
+dit(bf(-x))
+Do not write new passwords into the directory. This is usefull if other
+information is being freshened but users have changed their passwords.
+
+dit(bf(-p))
+Specify the passwd file to import.
+
+dit(bf(-g))
+Specify the group file to import.
+
+dit(bf(-s))
+Specify the shadow file to import.
+enddit()
+
+manpagefiles()
+itemize(
+  it() /etc/userdir-ldap/userdir-ldap.conf
+  Configuration variables to select what server and what base DN to use.
+)
+       
+manpageauthor()
+userdir-ldap was written by Jason Gunthorpe <jgg@debian.org>.
+
diff --git a/doc/ud-xearth.1.yo b/doc/ud-xearth.1.yo
new file mode 100644 (file)
index 0000000..666c903
--- /dev/null
@@ -0,0 +1,34 @@
+mailto(admin@db.debian.org)
+manpage(ud-xearth)(1)(17 Sep 1999)(userdir-ldap)()
+manpagename(ud-xearth)(Extracts the XEarth marker database)
+
+manpagesynopsis()
+  ud-xearth [options]
+
+manpagedescription()
+ud-xearth simply extracts the lat/long information from the directory and
+formats it in a form suitable for use by XEarth or XPlanet. The program
+takes the lat/long coords stored in the directory and converts them to a
+decimal degrees format and then outputs a file containing the UID of the
+user and their coordinates as well as their full email address in a comment.
+The output is place in a file called ./markers.dat
+
+Since lat/long information is restricted to developers only a valid login is
+required to extract the information. 
+
+manpageoptions()
+startdit()
+dit(bf(-u))
+Set the authentication user. This is the user who's authority is used when 
+accessing the LDAP directory. The default is to use the current system user
+name.
+enddit()
+
+manpagefiles()
+itemize(
+  it() /etc/userdir-ldap/userdir-ldap.conf
+  Configuration variables to select what server and what base DN to use.
+)
+       
+manpageauthor()
+userdir-ldap was written by Jason Gunthorpe <jgg@debian.org>.
diff --git a/templates/list-subscribe b/templates/list-subscribe
new file mode 100644 (file)
index 0000000..e59d65d
--- /dev/null
@@ -0,0 +1,4 @@
+To: debian-private-REQUEST@lists.debian.org
+X-We-Want-Cabal: listmaster@lists.debian.org __LISTPASS__ subscribe __PRIVATE__
+
+foo
diff --git a/templates/passwd-changed b/templates/passwd-changed
new file mode 100644 (file)
index 0000000..74b497f
--- /dev/null
@@ -0,0 +1,14 @@
+From: __FROM__
+Subject: Password Changed!
+
+Hello __EMAIL__!
+
+Your password has been updated. Enclosed below is the new password encrypted
+with your key. __CRYPTTYPE__
+
+Currently LDAP information is replicated to each machine every 15 mins, 
+va, pandora and master are not presently in the LDAP system.
+
+Please email __ADMIN__ if you have any questions.
+
+__PASSWORD__
diff --git a/templates/ping-reply b/templates/ping-reply
new file mode 100644 (file)
index 0000000..4db4b87
--- /dev/null
@@ -0,0 +1,11 @@
+From: __FROM__
+Subject: Pring Reply
+
+Hello __EMAIL__!
+
+Here is a list of all the public fields associated with your LDAP entry:
+
+__LDAPFIELDS__
+
+Please email __ADMIN__ if you have any questions.
+
diff --git a/templates/welcome-message-60000 b/templates/welcome-message-60000
new file mode 100644 (file)
index 0000000..f89fa1c
--- /dev/null
@@ -0,0 +1,37 @@
+To: "__REALNAME__" <__EMAIL__>
+Subject: Debian Guest Account for __REALNAME__
+Cc: debian-admin@lists.debian.org
+Reply-To: debian-admin@lists.@debian.org
+Date: __DATE__
+User-Agent: Script run by __WHOAMI__
+
+Dear __REALNAME__!
+
+An account has been created for you on the Debian machine cluster. You can
+use this account to help make software run properly on the Debian GNU/Linux
+distribution. The username for this account is '__LOGIN__'. The password can
+be found encrypted with your PGP key and appended to this message.
+
+The following machines are accesible:
+  faure.debian.org    Alpha running unstable
+  albert.debian.org   Alpha running stable  [slow]
+  kubrick.dbian.org   Sparc running unstable
+  debussy.debian.org  ARM running unstable
+
+Requests for Debian software to be installed should be directed at
+debian-admin@lists.debian.org. Please note that not all software is available
+on all architectures.
+
+You should use ssh to log into the machines instead of regular telnet or 
+rlogin.  We have installed ~/.ssh directories and empty authorized_keys
+files with appropriate permissions on each machine.If you want to ssh to them
+without typing the password, run ssh-keygen on your machine and add the 
+contents of ~/.ssh/identity.pub into the authorized_keys files in ~/.ssh. But 
+please be aware of the security implications of doing this.
+
+After a short while of inactivity this account will be expired.
+
+-- 
+Debian Administration
+
+__PASSWORD__
diff --git a/templates/welcome-message-800 b/templates/welcome-message-800
new file mode 100644 (file)
index 0000000..ea87686
--- /dev/null
@@ -0,0 +1,92 @@
+To: "__REALNAME__" <__EMAIL__>
+From: __WHOAMI__
+Subject: New Debian maintainer __REALNAME__
+Cc: new-maintainer@debian.org
+Reply-To: new-maintainer@debian.org
+Date: __DATE__
+User-Agent: nm-create script run by __WHOAMI__
+
+[ This is a long (automatically-generated) mail, but it contains
+   important information, please read it all carefully. ]
+
+Dear __REALNAME__!
+
+An account has been created for you on master.debian.org and
+va.debian.org with username '__LOGIN__'.  The password for this
+account can be found encrypted with your PGP key and appended to this
+message.
+
+You have been subscribed to the debian-private mailing list as
+<__PRIVATE__> (you should receive seperate confirmation of this from
+smartlist).  Please respect the privacy of that list and don't forward
+mail from it elsewhere.  E-mail to <__LOGIN__@debian.org> will be
+forwarded to <__EMAIL__>.  To change this, edit the file '.qmail' in
+your home directory on master to point to the new address (don't
+forget the '&' character at the beginning of the line).  Please
+subscribe to debian-devel-announce, if you haven't done so already.
+
+We strongly suggest that you use your __LOGIN__@debian.org address for
+the maintainer field in your packages, because that one will be valid
+as long as you are a Debian developer, even if you change jobs, leave
+university or change Internet Service providers.  If you do so, please 
+add that address to your PGP key (using `pgp -ke "YOUR USER ID"'), if
+you have one, and your GnuPG key (using `gpg --edit-key "YOUR USER ID"')
+and send it to <keyring-maint@debian.org>.
+
+You can find more information useful to developers at
+<URL:http://www.debian.org/devel/> (in particular, see the subsection
+titled "Debian Developer's reference").
+
+We suggest that you subscribe to debian-mentors@lists.debian.org.
+This list is for new maintainers who seek help with initial packaging
+and other developer-related issues.  Those who prefer one-on-one help
+can also post to the list, and an experienced developer may volunteer
+to help you.  You can get online help on IRC, too, if you join the
+channel #debian-devel on irc.debian.org.  Take a look at the support
+section on www.debian.org in order to find out more information.
+
+You should have read these documents before working on your packages.
+
+  o The Debian Social Contract
+    <URL:http://www.debian.org/social_contract.html>
+
+  o The Debian Policy Manual
+    <URL:http://www.debian.org/doc/debian-policy/>
+
+  o The Debian Packaging Manual
+    <URL:http://www.debian.org/doc/packaging-manuals/packaging.html>
+
+If you have some spare time and want to contribute it to Debian you
+may wish to take a look at the "Work-Needing and Prospective Packages
+for Debian GNU/Linux" also known as WNPP that can be found at
+<URL:ftp://ftp.debian.org/doc/package-developer/prospective-packages.html>
+
+If you plan to make a Debian package from a not yet packaged piece of
+software you *must* announce your intention on the debian-devel mailing
+list to make sure nobody else is working on them.
+
+The machine master.debian.org is our main archive server.  Every
+uploaded package finds it's way there (except for Packages covered by
+US crypto laws which go to non-us.debian.org) eventually.  That
+machine is also the home of our web pages and our bug tracking system.
+The second machine va.debian.org is supposed to take over some of the
+work master does.
+
+Both machines were sponsored by companies (Novare Inc. for master and
+VA Linux Systems for va).  Please don't over-stress them and use your
+accounts carefully.
+
+You should use ssh to log into the machines instead of regular telnet
+or rlogin.  We have installed ~/.ssh directories and authorized_keys
+files with appropriate permissions on both machines.  If you want to
+ssh to them without typing the password, run ssh-keygen on your
+machine and add the contents of ~/.ssh/identity.pub into the
+authorized_keys files in ~/.ssh on master and va.  But please be aware
+of the security implications of doing this.
+
+Welcome to the project!
+
+-- 
+The Debian New Maintainer Team
+
+__PASSWORD__
diff --git a/ud-arbimport b/ud-arbimport
new file mode 100755 (executable)
index 0000000..cf57cbc
--- /dev/null
@@ -0,0 +1,46 @@
+#!/usr/bin/env python
+# -*- mode: python -*-
+# This script imports arbitary lists of data. The input is a file with 
+# the form of:
+#  uid: <data>
+
+import string, re, time, ldap, getopt, sys;
+from userdir_ldap import *;
+
+# Process options
+(options, arguments) = getopt.getopt(sys.argv[1:], "u:m:n")
+for (switch, val) in options:
+   if (switch == '-u'):
+      AdminUser = val
+   elif (switch == '-m'):
+       LoadOverride(val);
+   elif (switch == '-n'):
+       NoAct = 1;
+if len(arguments) == 0:
+   print "Give the key to assignt to then the file to import";
+   os.exit(0);
+
+# Main program starts here
+print "Accessing LDAP directory as '" + AdminUser + "'";
+Password = getpass(AdminUser + "'s password: ");
+
+# Connect to the ldap server
+l = ldap.open(LDAPServer);
+UserDn = "uid=" + AdminUser + "," + BaseDn;
+l.simple_bind_s(UserDn,Password);
+
+# Read the override file into the unknown map. The override file is a list
+# of colon delimited entires mapping PGP email addresess to local users
+List = open(arguments[1],"r");
+while(1):
+   Line = List.readline();
+   if Line == "":
+      break;
+   Split = re.split("[:\n]",Line);
+   
+   Rec = [(ldap.MOD_REPLACE,arguments[0],string.strip(Split[1]))];
+   Dn = "uid=" + Split[0] + "," + BaseDn;
+   try:
+      l.modify_s(Dn,Rec);
+   except:
+      print "Failed",Dn;
diff --git a/ud-emailmatcher b/ud-emailmatcher
new file mode 100755 (executable)
index 0000000..daf76cd
--- /dev/null
@@ -0,0 +1,172 @@
+#!/usr/bin/env python
+# -*- mode: python -*-
+# This script tries to match a list of email addresses to the ldap database
+# uids. It makes use of the PGP key ring to determine matches
+
+import string, re, time, ldap, getopt, sys;
+from userdir_ldap import *;
+from userdir_gpg import *;
+
+AddressSplit = re.compile("(.*).*<([^@]*)@([^>]*)>");
+
+# Import an an forward file
+def ImportForward(File,EmailMap):
+   F = open(File,"r");
+   while(1):
+      Line = string.strip(F.readline());
+      if Line == "":
+         break;
+      Split = string.split(Line,":");
+      if len(Split) != 2:
+         continue;
+   
+      Addr = string.strip(Split[1]);
+      if EmailMap.has_key(Addr) and  EmailMap[Addr] != Split[0]:
+         print "Dup Over Emap",Line,Split
+      else:
+         EmailMap[Addr] = Split[0];
+   F.close();
+
+# Import an override file
+def ImportOverride(File,OverMap):
+   F = open(File,"r");
+   while(1):
+      Line = F.readline();
+      if Line == "":
+         break;
+      Line = string.strip(Line);
+
+      Split = string.split(Line,":");
+      if len(Split) != 2:
+         continue;
+      OverMap[Split[0]] = string.strip(Split[1]);
+   F.close();
+
+(options, arguments) = getopt.getopt(sys.argv[1:], "o:f:")
+
+# Popen GPG with the correct magic special options
+Args = [GPGPath] + GPGBasicOptions + GPGKeyRings;
+for x in arguments:
+   Args.append("--keyring");
+   Args.append(x);
+Args = Args + GPGSearchOptions + [" 2> /dev/null"]
+Keys = os.popen(string.join(Args," "),"r");
+
+l = ldap.open(LDAPServer);
+l.simple_bind_s("","");
+
+# Fetch the key list and map to email address
+PasswdAttrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"keyfingerprint=*",\
+                ["uid","keyfingerprint"]);
+KFMap = {}
+for x in PasswdAttrs:
+   if x[1].has_key("keyfingerprint") == 0 or x[1].has_key("uid") == 0:
+      continue;
+   for I in x[1]["keyfingerprint"]:
+      KFMap[I] = x[1]["uid"][0];
+   
+# Loop over the GPG key file mapping addresses to uids
+Outstanding = 0;
+Ignored = 0;
+Emails = [];
+EmailMap = {};
+UIDMap = {};
+UID = None;
+FingerPrint = None;
+print "Reading keyrings",
+sys.stdout.flush();
+while(1):
+   Line = Keys.readline();
+   if Line == "":
+      break;
+   
+   Split = string.split(Line,":");
+   if len(Split) >= 8 and Split[0] == "pub":
+      if FingerPrint != None and UID != None:
+         for x in Emails:
+            Match = AddressSplit.match(x);
+            if Match == None:
+              continue;
+            Groups = Match.groups();
+           Email = Groups[1]+'@'+Groups[2];
+           if UIDMap.has_key(Groups[1]):
+              UIDMap[Groups[1]].append(Email);
+            else:
+              UIDMap[Groups[1]] = [Email];
+           if EmailMap.has_key(Email) and EmailMap[Email] != UID:
+              print "Dup Emap",Email
+            else:
+              EmailMap[Email] = UID;
+      Emails = [Split[9]];
+      continue;
+   if len(Split) >= 11 and Split[0] == "fpr":
+      FingerPrint = Split[9];
+      if KFMap.has_key(FingerPrint) == 0:
+         print "Failed",FingerPrint;
+        UID = None;
+         continue;
+      UID = KFMap[FingerPrint];
+   if len(Split) >= 9 and Split[0] == "uid":
+      Emails.append(Split[9]);
+print;
+
+# Process the override files
+for (switch, val) in options:
+   if (switch == '-f'):
+      ImportForward(val,EmailMap);
+      BindUser = val;
+   elif (switch == '-o'):
+      ImportOverride(val,EmailMap);
+
+# Map the input
+FinalMap = {};
+while(1):
+   Line = sys.stdin.readline();
+   if Line == "":
+      break;
+   Line = string.strip(Line);
+
+   Split = string.split(Line,"@");
+   if len(Split) != 2:
+      continue;
+
+   # The address is in our domain, go directly
+   if Split[1] == EmailAppend:
+      if FinalMap.has_key(Line):
+        print "Dup",Line
+      Split2 = string.split(Split[0],"-");
+      FinalMap[Line] = Split2[0];
+      continue;
+
+   # Exists in the email map..
+   if EmailMap.has_key(Line):
+      if FinalMap.has_key(Line):
+        print "Dup",Line
+      FinalMap[Line] = EmailMap[Line];
+      continue;
+
+   # Try again splitting off common address appendage modes
+   Split2 = string.split(Split[0],"-");
+   Addr = Split2[0]+'@'+Split[1];
+   if EmailMap.has_key(Addr):
+      if FinalMap.has_key(Addr):
+        print "Dup",Addr
+      FinalMap[Line] = EmailMap[Addr];
+      continue;
+
+    # Failed 
+   if UIDMap.has_key(Split[0]):
+      print Line,UIDMap[Split[0]];
+   print Line;
+print "-----";
+
+# Generate a reverse map and check for duplicates
+Back = {};
+for x in FinalMap.keys():
+   if Back.has_key(FinalMap[x]):
+      print "Dup",x,FinalMap[x],Back[FinalMap[x]];
+   Back[FinalMap[x]] = x;
+   
+# Print the forward map
+for x in Back.keys():
+   print "%s: %s" % (x,Back[x]);
diff --git a/ud-forwardlist b/ud-forwardlist
new file mode 100755 (executable)
index 0000000..9e5e28a
--- /dev/null
@@ -0,0 +1,68 @@
+#!/usr/bin/env python
+# -*- mode: python -*-
+# This script takes a list of .forward files and generates a list of colon
+# delimited fields for import into a ldap directory. The fields represent
+# the user and their email forwarding.
+#
+# A sample invokation..
+#   cd /home
+#   find -name ".foward" -maxdepth 2 | mkforwardlist | sort | less
+# Then correct any invalid forward files if possible. After that stash the
+# output in a file, remove the invalid lines and import it.
+#
+# It also understand .qmail type files
+
+import string, re, time, getopt, os, sys, pwd, stat;
+
+AddressSplit = re.compile("<(.*)>");
+
+while (1):
+   File = string.strip(sys.stdin.readline());
+   if File == "":
+      break;
+
+   # Attempt to determine the UID   
+   try:
+      User = pwd.getpwuid(os.stat(File)[stat.ST_UID])[0];
+   except KeyError:
+      print "Invalid0", File;
+      continue;
+
+   # Read the first two non comment non empty lines
+   Forward = open(File,"r");
+   Line = None;
+   while (1):
+      Line2 = string.strip(Forward.readline());
+      if Line2 == "":
+         break;
+      if Line2[0] == '#' or Line2[0] == '\n':
+         continue;
+      if Line == None:
+         Line = Line2;
+      else:
+         break;
+
+   # If we got more than one line or no lines at all it is invalid
+   if Line == None or Line == "" or Line2 != "":
+      print "Invalid1", File;
+      continue;
+
+   # Abort for funky things like pipes or directions to mailboxes
+   if Line[0] == '/' or Line[0] == '|' or Line[0] == '.' or Line[-1] == '/' or \
+      string.find(Line,'@') == -1:
+      print "Invalid2", File;
+      continue;
+
+   # Split off the address part
+   Address = AddressSplit.match(Line);
+   if Address == None: 
+      # Or parse a qmail adddress..
+      Address = Line;
+      if Address[0] == '&':
+         Address = Address[1:];
+
+   if Address == "":
+      print "Invalid3", File;
+      continue;
+
+   print User + ":",Address;
diff --git a/ud-generate b/ud-generate
new file mode 100755 (executable)
index 0000000..b627cae
--- /dev/null
@@ -0,0 +1,253 @@
+#!/usr/bin/env python
+# -*- mode: python -*-
+# Generates passwd, shadow and group files from the ldap directory.
+
+import string, re, time, ldap, getopt, sys, os, posix, pwd;
+from userdir_ldap import *;
+
+PasswdAttrs = None;
+GroupIDMap = {};
+
+# See if this user is in the group list
+def IsInGroup(DnRecord,Allowed):
+  # See if the primary group is in the list
+  if Allowed.has_key(GetAttr(DnRecord,"gidnumber")) != 0:
+     return 1;
+
+  # See if there are supplementary groups
+  if DnRecord[1].has_key("supplementarygid") == 0:
+     return 0;
+
+  # Check the supplementary groups
+  for I in DnRecord[1]["supplementarygid"]:
+     if Allowed.has_key(I):
+        return 1;
+  return 0;
+
+def Die(F,Fdb):
+   if F != None:
+      F.close();
+   if Fdb != None:
+      Fdb.close();
+   try: os.remove(File + ".tmp");
+   except: pass;
+   try: os.remove(File + ".tdb.tmp");
+   except: pass;
+
+def Done(File,F,Fdb):
+  if F != None:
+    F.close();
+    os.rename(File + ".tmp",File);
+  if Fdb != None:
+    Fdb.close();
+    os.rename(File + ".tdb.tmp",File+".tdb");
+  
+# Generate the password list
+def GenPasswd(l,File,HomePrefix,Allowed):
+  F = None;
+  Fdb = None;
+  try:
+   F = open(File + ".tmp","w");
+   Fdb = open(File + ".tdb.tmp","w");
+
+   # Fetch all the users
+   global PasswdAttrs;
+   if PasswdAttrs == None:
+      raise "No Users";
+
+   I = 0;
+   for x in PasswdAttrs:
+      if x[1].has_key("uidnumber") == 0 or IsInGroup(x,Allowed) == 0:
+         continue;
+           
+      Line = "%s:x:%s:%s:%s:%s%s:%s\n" % (GetAttr(x,"uid"),\
+              GetAttr(x,"uidnumber"),GetAttr(x,"gidnumber"),\
+              GetAttr(x,"gecos"),HomePrefix,GetAttr(x,"uid"),\
+              GetAttr(x,"loginshell"));
+      F.write(Line);
+      Fdb.write("0%u %s" % (I,Line));
+      Fdb.write(".%s %s" % (GetAttr(x,"uid"),Line));
+      Fdb.write("=%s %s" % (GetAttr(x,"uidnumber"),Line));
+      I = I + 1;
+
+  # Oops, something unspeakable happened.
+  except:
+   Die(F,Fdb);
+   raise;
+  Done(File,F,Fdb);
+
+# Generate the shadow list
+def GenShadow(l,File,Allowed):
+  F = None;
+  Fdb = None;
+  try:
+   OldMask = os.umask(0077);
+   F = open(File + ".tmp","w",0600);
+   Fdb = open(File + ".tdb.tmp","w",0600);
+   os.umask(OldMask);
+
+   # Fetch all the users
+   global PasswdAttrs;
+   if PasswdAttrs == None:
+      raise "No Users";
+
+   I = 0;
+   for x in PasswdAttrs:
+      if x[1].has_key("uidnumber") == 0 or IsInGroup(x,Allowed) == 0:
+         continue;
+        
+      Pass = GetAttr(x,"userpassword");
+      if Pass[0:7] != "{crypt}":
+         Pass = '*';
+      else:
+         Pass = Pass[7:];
+      Line = "%s:%s:%s:%s:%s:%s:%s:%s:\n" % (GetAttr(x,"uid"),\
+              Pass,GetAttr(x,"shadowlastchange"),\
+              GetAttr(x,"shadowmin"),GetAttr(x,"shadowmax"),\
+              GetAttr(x,"shadowwarning"),GetAttr(x,"shadowinactive"),\
+              GetAttr(x,"shadowexpire"));
+      F.write(Line);
+      Fdb.write("0%u %s" % (I,Line));
+      Fdb.write(".%s %s" % (GetAttr(x,"uid"),Line));
+      I = I + 1;
+
+  # Oops, something unspeakable happened.
+  except:
+   Die(F,Fdb);
+   raise;
+  Done(File,F,Fdb);
+
+# Generate the group list
+def GenGroup(l,File,Allowed):
+  F = None;
+  Fdb = None;
+  try:
+   F = open(File + ".tmp","w");
+   Fdb = open(File + ".tdb.tmp","w");
+
+   # Generate the GroupMap
+   GroupMap = {};
+   for x in GroupIDMap.keys():
+      GroupMap[x] = [];
+      
+   # Fetch all the users
+   global PasswdAttrs;
+   if PasswdAttrs == None:
+      raise "No Users";
+
+   # Sort them into a list of groups having a set of users
+   for x in PasswdAttrs:
+      if x[1].has_key("uidnumber") == 0 or IsInGroup(x,Allowed) == 0:
+         continue;
+      if x[1].has_key("supplementarygid") == 0:
+         continue;
+        
+      for I in x[1]["supplementarygid"]:
+         if GroupMap.has_key(I):
+           GroupMap[I].append(GetAttr(x,"uid"));
+        else:
+           GroupMap[I] = [GetAttr(x,"uid")];
+           
+   # Output the group file.
+   Counter = 0; 
+   for x in GroupMap.keys():
+      Line = "%s:x:%u:" % (x,GroupIDMap[x]);
+      Comma = '';
+      for I in GroupMap[x]:
+        Line = Line + ("%s%s" % (Comma,I));
+        Comma = ',';
+      Line = Line + '\n';
+      F.write(Line);
+      Fdb.write("0%u %s" % (Counter,Line));
+      Fdb.write(".%s %s" % (x,Line));
+      Fdb.write("=%u %s" % (GroupIDMap[x],Line));
+      Counter = Counter + 1;
+      
+  # Oops, something unspeakable happened.
+  except:
+   Die(F,Fdb);
+   raise;
+  Done(File,F,Fdb);
+
+# Generate the email forwarding list
+def GenForward(l,File,Allowed):
+  F = None;
+  Fdb = None;
+  try:
+   F = open(File + ".tmp","w");
+   Fdb = None;
+
+   # Fetch all the users
+   global PasswdAttrs;
+   if PasswdAttrs == None:
+      raise "No Users";
+
+   # Write out the email address for each user
+   for x in PasswdAttrs:
+      if x[1].has_key("emailforward") == 0 or IsInGroup(x,Allowed) == 0:
+         continue;
+      Line = "%s: %s\n" % (GetAttr(x,"uid"),GetAttr(x,"emailforward"));
+      F.write(Line);
+      
+  # Oops, something unspeakable happened.
+  except:
+   Die(F,Fdb);
+   raise;
+  Done(File,F,Fdb);
+
+# Connect to the ldap server
+l = ldap.open(LDAPServer);
+F = open(PassDir+"/pass-"+pwd.getpwuid(posix.getuid())[0],"r");
+Pass = string.split(string.strip(F.readline())," ");
+F.close();
+l.simple_bind_s("uid="+Pass[0]+","+BaseDn,Pass[1]);
+
+# Fetch all the groups
+GroupIDMap = {};
+Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"gid=*",\
+                  ["gid","gidnumber"]);
+
+# Generate the GroupMap and GroupIDMap
+for x in Attrs:
+   if x[1].has_key("gidnumber") == 0:
+      continue;
+   GroupIDMap[x[1]["gid"][0]] = int(x[1]["gidnumber"][0]);
+
+# Fetch all the users
+PasswdAttrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"uid=*",\
+                ["uid","uidnumber","gidnumber","supplementarygid",\
+                 "gecos","loginshell","userpassword","shadowlastchange",\
+                 "shadowmin","shadowmax","shadowwarning","shadowinactive",
+                "shadowexpire","emailforward"]);
+
+# Open the control file
+if len(sys.argv) == 1:
+   F = open(GenerateConf,"r");
+else:
+   F = open(sys.argv[1],"r")
+while(1):
+   Line = F.readline();
+   if Line == "":
+      break;
+   Line = string.strip(Line);
+   if Line == "":
+      continue;
+   if Line[0] == '#':
+      continue;
+
+   Split = string.split(Line," ");
+   OutDir = GenerateDir + '/' + Split[0] + '/';
+   try: os.mkdir(OutDir);
+   except: pass;
+
+   # Get the group list and convert any named groups to numerics
+   GroupList = {};
+   for I in Split[2:]:
+      GroupList[I] = None;
+      if GroupIDMap.has_key(I):
+         GroupList[str(GroupIDMap[I])] = None;
+
+   GenPasswd(l,OutDir+"passwd",Split[1],GroupList);
+   GenGroup(l,OutDir+"group",GroupList);
+   GenShadow(l,OutDir+"shadow",GroupList);
+   GenForward(l,OutDir+"forward-alias",GroupList);
diff --git a/ud-gpgimport b/ud-gpgimport
new file mode 100755 (executable)
index 0000000..73e2a03
--- /dev/null
@@ -0,0 +1,234 @@
+#!/usr/bin/env python
+# -*- mode: python -*-
+# This script tries to match key fingerprints from a keyring with user
+# name in a directory. When an unassigned key is found a heuristic match
+# against the keys given cn/sn and the directory is performed to try to get
+# a matching. Generally this works about 90% of the time, matching is fairly
+# strict. In the event a non-match a fuzzy sounds-alike search is performed
+# and the results printed to aide the user.
+#
+# GPG is automatically invoked with the correct magic special options,
+# pass the names of all the valid key rings on the command line.
+#
+# The output report will list what actions were taken. Keys that are present
+# in the directory but not in the key ring will be removed from the 
+# directory. 
+
+import string, re, time, ldap, getopt, sys, pwd, posix;
+from userdir_ldap import *;
+from userdir_gpg import *;
+
+# This map deals with people who put the wrong sort of stuff in their pgp
+# key entries
+UnknownMap = {};
+NoAct = 1;
+
+AddressSplit = re.compile("(.*).*<([^@]*)@([^>]*)>");
+
+# Read the override file into the unknown map. The override file is a list
+# of colon delimited entires mapping PGP email addresess to local users
+def LoadOverride(File):
+   List = open(File,"r");
+   while(1):
+      Line = List.readline();
+      if Line == "":
+         break;
+      Split = re.split("[:\n]",Line);
+      UnknownMap[Split[0]] = string.strip(Split[1]);
+
+# Convert the PGP name string to a uid value
+def GetUID(l,Name):
+   # Crack up the email address into a best guess first/middle/last name
+   (cn,mn,sn) = NameSplit(re.sub('["]','',Name[0]))
+   
+   # Brackets anger the ldap searcher
+   cn = re.sub('[(")]','?',cn);
+   sn = re.sub('[(")]','?',sn);
+
+   # First check the unknown map for the email address
+   if UnknownMap.has_key(Name[1] + '@' + Name[2]):
+      print "unknown map hit for",Name;
+      return UnknownMap[Name[1] + '@' + Name[2]];
+
+   # Then the cruft component (ie there was no email address to match)
+   if UnknownMap.has_key(Name[2]):
+      print "unknown map hit for",Name;
+      return UnknownMap[Name[2]];
+
+   # Search for a possible first/last name hit
+   try:
+      Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"(&(cn=%s)(sn=%s))"%(cn,sn),["uid"]);
+   except ldap.FILTER_ERROR:
+      print "Filter failure:","(&(cn=%s)(sn=%s))"%(cn,sn);
+      return None;
+
+   # Hmm, more than one/no return
+   if (len(Attrs) != 1):
+      # Key claims a local address
+      if Name[2] == EmailAppend:
+
+         # Pull out the record for the claimed user
+         Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"(uid=%s)"%(Name[1]),["uid","sn","cn"]);
+
+         # We require the UID surname to be someplace in the key name, this
+         # deals with special purpose keys like 'James Troup (Alternate Debian key)'
+        # Some people put their names backwards on their key too.. check that as well
+         if len(Attrs) == 1 and \
+            (string.find(string.lower(sn),string.lower(Attrs[0][1]["sn"][0])) != -1 or \
+            string.find(string.lower(cn),string.lower(Attrs[0][1]["sn"][0])) != -1):
+            print EmailAppend,"hit for",Name;
+            return Name[1];
+
+      # Attempt to give some best guess suggestions for use in editing the
+      # override file.
+      print "None for",Name;
+      Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"(sn~=%s)"%(sn),["uid","sn","cn"]);
+      for x in Attrs:
+         print "  But might be:",x[1]["cn"][0],x[1]["sn"][0],"<" + x[1]["uid"][0] + "@debian.org>";
+   else:
+      return Attrs[0][1]["uid"][0];
+
+   return None;
+
+# Process options
+AdminUser = pwd.getpwuid(posix.getuid())[0];
+(options, arguments) = getopt.getopt(sys.argv[1:], "au:m:n")
+for (switch, val) in options:
+   if (switch == '-u'):
+      AdminUser = val
+   elif (switch == '-m'):
+       LoadOverride(val);
+   elif (switch == '-a'):
+       NoAct = 0;
+if len(arguments) == 0:
+   print "Give some keyrings to probe";
+   os.exit(0);
+
+# Main program starts here
+
+# Connect to the ldap server
+l = ldap.open(LDAPServer);
+if NoAct == 0:
+   print "Accessing LDAP directory as '" + AdminUser + "'";
+   Password = getpass(AdminUser + "'s password: ");
+   UserDn = "uid=" + AdminUser + "," + BaseDn;
+   l.simple_bind_s(UserDn,Password);
+else:
+   l.simple_bind_s("","");
+
+# Download the existing key list and put it into a map
+print "Fetching key list..",
+sys.stdout.flush();
+Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"keyfingerprint=*",["keyfingerprint","uid"]);
+KeyMap = {};
+KeyCount = {};
+for x in Attrs:
+  try:
+     # Sense a bad fingerprint.. Slapd has problems, it will store a null
+     # value that ldapsearch doesn't show up.. detect and remove
+     if len(x[1]["keyfingerprint"]) == 0 or x[1]["keyfingerprint"][0] == "":
+       print;
+       print "Fixing bad fingerprint for",x[1]["uid"][0],
+       sys.stdout.flush();
+       if NoAct == 0:
+         l.modify_s("uid="+x[1]["uid"][0]+","+BaseDn,\
+                     [(ldap.MOD_DELETE,"keyfingerprint",None)]);
+     else:
+       for I in x[1]["keyfingerprint"]:
+         KeyMap[I] = [x[1]["uid"][0],0];
+         if KeyCount.has_key(x[1]["uid"][0]):
+            KeyCount[x[1]["uid"][0]] = KeyCount[x[1]["uid"][0]] + 1;
+         else:
+            KeyCount[x[1]["uid"][0]] = 1;
+  except:
+     continue;
+Attrs = None;
+print;
+
+# Popen GPG with the correct magic special options
+Args = [GPGPath] + GPGBasicOptions;
+for x in arguments:
+   Args.append("--keyring");
+   if string.find(x,"/") == -1:
+      Args.append("./"+x);
+   else:
+      Args.append(x);
+Args = Args + GPGSearchOptions + [" 2> /dev/null"]
+Keys = os.popen(string.join(Args," "),"r");
+
+# Loop over the GPG key file
+Outstanding = 0;
+Ignored = 0;
+while(1):
+   Line = Keys.readline();
+   if Line == "":
+      break;
+   
+   Split = string.split(Line,":");
+   if len(Split) < 8 or Split[0] != "pub":
+      continue;
+
+   while (1):
+       Line2 = Keys.readline();
+       if Line2 == "":
+          break;
+       Split2 = string.split(Line2,":");
+       if len(Split2) < 11 or Split2[0] != "fpr":
+          continue;
+       break;
+   if Line2 == "":
+      break;
+
+   if KeyMap.has_key(Split2[9]):
+      Ignored = Ignored + 1;
+      # print "Ignoring keyID",Split2[9],"belonging to",KeyMap[Split2[9]][0];
+      KeyMap[Split2[9]][1] = 1;
+      continue;
+
+   Match = AddressSplit.match(Split[9]);
+   if Match == None:
+      UID = GetUID(l,("","",Split[9]));
+   else:
+      UID = GetUID(l,Match.groups());
+
+   if UID == None:
+      print "MISSING 0x" + Split2[9];
+      continue;
+
+   Rec = [(ldap.MOD_ADD,"keyfingerprint",Split2[9])];
+   Dn = "uid=" + UID + "," + BaseDn;
+   print "Adding keyID",Split2[9],"to",UID;
+   if KeyCount.has_key(UID):
+      KeyCount[UID] = KeyCount[UID] + 1;
+   else:
+      KeyCount[UID] = 1;
+   
+   if NoAct == 1:
+      continue;
+
+   # Send the modify request
+   l.modify(Dn,Rec);
+   Outstanding = Outstanding + 1;
+   Outstanding = FlushOutstanding(l,Outstanding,1);
+   sys.stdout.flush();
+
+if NoAct == 0:
+   FlushOutstanding(l,Outstanding);
+
+if Keys.close() != None:
+   raise "Error","GPG failed"
+
+print Ignored,"keys already in the directory (ignored)";
+
+# Look for unmatched keys
+for x in KeyMap.keys():
+   if KeyMap[x][1] == 0:
+      print "keyID",x,"belonging to",KeyMap[x][0],"removed";
+      if KeyCount.has_key(KeyMap[x][0]) :
+         KeyCount[KeyMap[x][0]] = KeyCount[KeyMap[x][0]] - 1
+         if KeyCount[KeyMap[x][0]] <= 0:
+            print "**",KeyMap[x][0],"no longer has any keys";
+      if NoAct == 0:
+         l.modify_s("uid="+KeyMap[x][0]+","+BaseDn,\
+                     [(ldap.MOD_DELETE,"keyfingerprint",x)]);
+      
diff --git a/ud-homecheck b/ud-homecheck
new file mode 100755 (executable)
index 0000000..dda3410
--- /dev/null
@@ -0,0 +1,16 @@
+#!/usr/bin/env python
+# -*- mode: python -*-
+# Checks a directory against the passwd file assuming it is the home
+# directory directory
+
+import string, ldap, getopt, sys, os, pwd;
+
+for x in os.listdir(sys.argv[1]):
+   try:
+      User = pwd.getpwnam(x);
+      st = os.stat(sys.argv[1]+x);
+      if User[2] != st[4] or User[3] != st[5]:
+         print "Bad ownership",x;
+   except:
+      print "Failed",x,"==> %s: %s" %(sys.exc_type,sys.exc_value);
+      
diff --git a/ud-info b/ud-info
new file mode 100755 (executable)
index 0000000..5e9603e
--- /dev/null
+++ b/ud-info
@@ -0,0 +1,359 @@
+#!/usr/bin/env python
+# -*- mode: python -*-
+# This script is an interactive way to manipulate fields in the LDAP directory.
+# When run it connects to the directory using the current users ID and fetches
+# all the attributes for that user. It then formats them nicely and allows
+# the user to change them.
+# It is possible to authenticate as someone differnt than you are viewing/changing
+# this allows administrative functions and also allows users to view 
+# restricted information about others, such as phone numbers and addresses.
+#
+#  Usage: userinfo -a <user> -u <user> -c <user> -r
+#    -a    Set the authentication user (the user whose password you are 
+#          going to enter)
+#    -u    Set the user to display
+#    -c    Set both -a and -u, use this if your login uid is not in the 
+#          database
+#    -r    Enable 'root' functions, do this if your uid has access to
+#          restricted variables.
+#
+# http://www.geocode.com/eagle.html-ssi
+
+import string, time, posix, pwd, sys, getopt, ldap, crypt, whrandom, readline, copy;
+from userdir_ldap import *;
+
+RootMode = 0;
+AttrInfo = {"cn": ["First Name", 101],
+            "mn": ["Middle Name", 102],
+            "sn": ["Surname", 103],
+           "c": ["Country Code",1],
+           "l": ["Locality",2],
+           "ou": ["Membership",0],
+           "facsimiletelephonenumber": ["Fax Phone Number",3],
+           "telephonenumber": ["Phone Number",4],
+           "postaladdress": ["Mailing Address",5],
+           "postalcode": ["Postal Code",6],
+           "uid": ["Unix User ID",0],
+           "loginshell": ["Unix Shell",7],
+           "supplementarygid": ["Unix Groups",0],
+           "emailforward": ["Email Forwarding",8],
+           "ircnick": ["IRC Nickname",9],
+           "onvacation": ["Vacation Message",10],
+           "labeledurl": ["Home Page",11],
+           "latitude": ["Latitude",12],
+           "longitude": ["Longitude",13],
+           "comment": ["Comment",114],
+           "userpassword": ["Crypted Password",115]};
+
+AttrPrompt = {"cn": ["Common name or first name"],
+              "mn": ["Middle name (or initial if it ends in a dot)"],
+              "sn": ["Surname or last name"],
+              "c": ["ISO 2 letter country code, such as US, DE, etc"],
+              "l": ["City name, State/Provice (Locality)\n e.g. Dallas, Texas"],
+              "facsimiletelephonenumber": ["Fax phone number, with area code and country code"],
+              "telephonenumber": ["Voice phone number"],
+             "postaladdress": ["Complete mailing address including postal codes and country designations\nSeperate lines using a $ character"],
+             "postalcode": ["Postal Code or Zip Code"],
+              "loginshell": ["Login shell with full path (no check is done for validity)"],
+             "emailforward": ["EMail address to send all mail to or blank to disable"],
+             "ircnick": ["IRC nickname if you use IRC"],
+             "onvacation": ["A message if on vaction, indicating the time of departure and return"],
+              "userpassword": ["The users Crypt'd password"],
+              "comment": ["Admin Comment about the account"],
+              "supplementarygid": ["Groups the user is in"],
+             "latitude": ["XEarth latitude in ISO 6709 format - see /usr/share/zoneinfo/zone.tab or etak.com"],
+             "longitude": ["XEarth latitude in ISO 6709 format - see /usr/share/zoneinfo/zone.tab or etak.com"],
+              "labeledurl": ["Web home page"]};
+
+# Create a map of IDs to desc,value,attr
+OrderedIndex = {};
+for at in AttrInfo.keys():
+   if (AttrInfo[at][1] != 0):
+      OrderedIndex[AttrInfo[at][1]] = [AttrInfo[at][0], "", at];
+OrigOrderedIndex = copy.deepcopy(OrderedIndex);
+
+# Show shadow information
+def PrintShadow(Attrs):
+   Changed = int(GetAttr(Attrs,"shadowlastchange","0"));
+   MinDays = int(GetAttr(Attrs,"shadowmin","0"));
+   MaxDays = int(GetAttr(Attrs,"shadowmax","0"));
+   WarnDays = int(GetAttr(Attrs,"shadowwarning","0"));
+   InactDays = int(GetAttr(Attrs,"shadowinactive","0"));
+   Expire = int(GetAttr(Attrs,"shadowexpire","0"));
+
+   print "%-24s:" % ("Password last changed"),
+   print time.strftime("%a %d/%m/%Y %Z",time.localtime(Changed*24*60*60));
+   if (Expire > 0):
+      print "%-24s:" % ("Account expires on"),
+      print time.strftime("%a %d/%m/%Y %Z",time.localtime(Expire*24*60*60));
+   if (InactDays >= 0 and MaxDays < 99999):
+      print "Account aging is active, you must change your password every", MaxDays, "days."
+
+# Print out the automatic time stamp information
+def PrintModTime(Attrs):
+   Stamp = GetAttr(Attrs,"modifytimestamp","");
+   if len(Stamp) >= 13:
+      Time = (int(Stamp[0:4]),int(Stamp[4:6]),int(Stamp[6:8]),
+              int(Stamp[8:10]),int(Stamp[10:12]),int(Stamp[12:14]),0,0,-1);
+      print "%-24s:" % ("Record last modified on"), time.strftime("%a %d/%m/%Y %X UTC",Time),
+      print "by",ldap.explode_dn(GetAttr(Attrs,"modifiersname"),1)[0];
+
+   Stamp = GetAttr(Attrs,"createtimestamp","");
+   if len(Stamp) >= 13:
+      Time = (int(Stamp[0:4]),int(Stamp[4:6]),int(Stamp[6:8]),
+              int(Stamp[8:10]),int(Stamp[10:12]),int(Stamp[12:14]),0,0,-1);
+      print "%-24s:" % ("Record created on"), time.strftime("%a %d/%m/%Y %X UTC",Time);
+
+# Print the PGP key for a user
+def PrintKeys(Attrs):
+   if Attrs[1].has_key("keyfingerprint") == 0:
+      return;
+   First = 0;
+   for x in Attrs[1]["keyfingerprint"]:
+      if First == 0:
+         print "%-24s:" % ("PGP/GPG Key Fingerprints"),
+         First = 1;
+      else:
+         print "%-24s:" % (""),
+
+      # PGP Print
+      if (len(x) == 32):
+         I = 0;
+         while (I < len(x)):
+            print x[I]+x[I+1],
+            I = I + 2;
+            if I == 32/2:
+               print "",
+      elif (len(x) == 40):
+         # GPG Print
+         I = 0;
+         while (I < len(x)):
+            print x[I]+x[I+1]+x[I+2]+x[I+3],
+            I = I + 4;
+            if I == 40/2:
+               print "",
+      else:
+         print x,
+      print;
+
+# Display all of the attributes in a numbered list
+def ShowAttrs(Attrs):
+   print;
+   print EmailAddress(Attrs);   
+   PrintModTime(Attrs);
+   PrintShadow(Attrs);
+   PrintKeys(Attrs);
+
+   for at in Attrs[1].keys():
+      if AttrInfo.has_key(at):
+         if AttrInfo[at][1] == 0:
+            print "      %-18s:" % (AttrInfo[at][0]),
+           for x in Attrs[1][at]:
+              print "'%s'" % (x),
+           if at == "uid":
+              print "(id=%s, gid=%s)" % (GetAttr(Attrs,"uidnumber","-1"),GetAttr(Attrs,"gidnumber","-1")),
+            print;
+         else:
+            OrderedIndex[AttrInfo[at][1]][1] = Attrs[1][at];
+                                      
+   Keys = OrderedIndex.keys();
+   Keys.sort();
+   for at in Keys:
+      if at < 100 or RootMode != 0:
+         print " %3u) %-18s: " % (at,OrderedIndex[at][0]),
+         for x in OrderedIndex[at][1]:
+            print "'%s'" % (re.sub('[\n\r]','?',x)),
+         print;
+
+# Change a single attribute
+def ChangeAttr(Attrs,Attr):
+   if (Attr == "supplementarygid"):
+      return MultiChangeAttr(Attrs,Attr);
+
+   print "Old value: '%s'" % (GetAttr(Attrs,Attr,""));
+   print "Press enter to leave unchanged and a single space to set to empty";
+   NewValue = raw_input("New? ");
+  
+   # Empty string
+   if (NewValue == ""):
+      print "Leaving unchanged.";
+      return;
+
+   # Single space designates delete, trap the delete error
+   if (NewValue == " "):
+      print "Deleting.",;
+      try:
+         l.modify_s(UserDn,[(ldap.MOD_DELETE,Attr,None)]);
+      except ldap.NO_SUCH_ATTRIBUTE:
+         pass;
+
+      print;
+      Attrs[1][Attr] = [""];
+      return;
+
+   # Set a new value
+   print "Setting.",;
+   l.modify_s(UserDn,[(ldap.MOD_REPLACE,Attr,NewValue)]);
+   Attrs[1][Attr] = [NewValue];
+   print;
+
+def MultiChangeAttr(Attrs,Attr):
+   # Make sure that we have an entry
+   if not Attrs[1].has_key(Attr):
+      Attrs[1][Attr] = [];
+
+   Attrs[1][Attr].sort();
+   print "Old values: ",Attrs[1][Attr];
+
+   Mode = string.upper(raw_input("[D]elete or [A]dd? "));
+   if (Mode != 'D' and Mode != 'A'):
+      return;
+
+   NewValue = raw_input("Value? ");
+   # Empty string
+   if (NewValue == ""):
+      print "Leaving unchanged.";
+      return;
+   
+   # Delete   
+   if (Mode == "D"):
+      print "Deleting.",;
+      try:
+         l.modify_s(UserDn,[(ldap.MOD_DELETE,Attr,NewValue)]);
+      except ldap.NO_SUCH_ATTRIBUTE:
+         print "Failed";
+
+      print;
+      Attrs[1][Attr].remove(NewValue);
+      return;
+
+   # Set a new value
+   print "Setting.",;
+   l.modify_s(UserDn,[(ldap.MOD_ADD,Attr,NewValue)]);
+   Attrs[1][Attr].append(NewValue);
+   print;
+
+# Main program starts here
+User = pwd.getpwuid(posix.getuid())[0];
+BindUser = User;
+# Process options
+(options, arguments) = getopt.getopt(sys.argv[1:], "nu:c:a:r")
+for (switch, val) in options:
+   if (switch == '-u'):
+      User = val;
+   elif (switch == '-a'):
+      BindUser = val;
+   elif (switch == '-c'):
+      BindUser = val;
+      User = val;
+   elif (switch == '-r'):
+      RootMode = 1;
+   elif (switch == '-n'):
+      BindUser = "";
+
+if (BindUser != ""):
+   print "Accessing LDAP entry for '" + User + "'",
+if (BindUser != User):
+   if (BindUser != ""):
+      print "as '" + BindUser + "'";
+else:
+   print;
+if (BindUser != ""):
+   Password = getpass(BindUser + "'s password: ");
+
+# Connect to the ldap server
+l = ldap.open(LDAPServer);
+UserDn = "uid=" + BindUser + "," + BaseDn;
+if (BindUser != ""):
+   l.simple_bind_s(UserDn,Password);
+else:
+   l.simple_bind_s("","");
+UserDn = "uid=" + User + "," + BaseDn;
+
+# Enable changing of supplementary gid's
+if (RootMode == 1):
+   AttrInfo["supplementarygid"][1] = 100;
+   OrderedIndex[AttrInfo["supplementarygid"][1]] = [AttrInfo["supplementarygid"][0], "","supplementarygid"];
+   OrigOrderedIndex[AttrInfo["supplementarygid"][1]] = [AttrInfo["supplementarygid"][0], "","supplementarygid"];
+
+# Query the server for all of the attributes
+Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"uid=" + User);
+
+# repeatedly show the account configuration
+while(1):
+   ShowAttrs(Attrs[0]);
+   if (BindUser == ""):
+      sys.exit(0);
+
+   if RootMode == 1:
+      print "   a) Arbitary Change";
+   print "   p) Change Password";
+   print "   u) Switch Users";
+   print "   x) Exit";
+   
+   # Prompt
+   Response = raw_input("Change? ");
+   if (Response == "x" or Response == "X" or Response == "q" or 
+       Response == "quit" or Response == "exit"):
+      break;
+
+   # Change who we are looking at
+   if (Response == 'u' or Response == 'U'):
+      NewUser = raw_input("User? ");
+      if NewUser == "":
+         continue;
+      User = NewUser;
+      UserDn = "uid=" + User + "," + BaseDn;
+      Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"uid=" + User);
+      OrderedIndex = copy.deepcopy(OrigOrderedIndex);
+      continue;
+
+   # Handle changing the password
+   if (Response == "p"):
+      print "Please enter a new password. Your password can be of unlimited length,";
+      print "contain spaces and other special characters. No checking is done on the";
+      print "strength of the passwords so pick good ones please!";
+
+      Pass1 = getpass(User + "'s new password: ");
+      Pass2 = getpass(User + "'s new password again: ");
+      if Pass1 != Pass2:
+         print "Passwords did not match";
+         raw_input("Press a key");
+         continue;
+
+      # Hash it telling glibc to use the MD5 algorithm - if you dont have
+      # glibc then just change Salt = "$1$" to Salt = "";
+      SaltVals = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/.";
+      Salt  = "$1$";
+      for x in range(0,10):
+         Salt = Salt + SaltVals[whrandom.randint(0,len(SaltVals)-1)];
+      Pass = crypt.crypt(Pass1,Salt);
+      if len(Pass) < 14:
+         print "Caution! MD5 Password hashing failed, not changing password!";
+         raw_input("Press a key");
+         continue;
+
+      print "Setting password..";
+      Pass = "{crypt}" + Pass;
+      l.modify_s(UserDn,[(ldap.MOD_REPLACE,"userpassword",Pass)]);
+      continue;
+
+   # Handle changing an arbitary value
+   if (Response == "a"):
+      Attr = raw_input("Attr? ");
+      ChangeAttr(Attrs[0],Attr);
+      continue;
+
+   # Convert the integer response
+   try:
+      ID = int(Response);
+      if (not OrderedIndex.has_key(ID) or (ID > 100 and RootMode == 0)):
+         raise ValueError;
+   except ValueError:
+      print "Invalid";
+      continue;
+
+   # Print the what to do prompt
+   print "Changing LDAP entry '%s' (%s)" % (OrderedIndex[ID][0],OrderedIndex[ID][2]);
+   print AttrPrompt[OrderedIndex[ID][2]][0];
+   ChangeAttr(Attrs[0],OrderedIndex[ID][2]);
diff --git a/ud-ldapshow b/ud-ldapshow
new file mode 100755 (executable)
index 0000000..5faef77
--- /dev/null
@@ -0,0 +1,96 @@
+#!/usr/bin/env python
+# -*- mode: python -*-
+# Show some reports from the ldap database
+# Call with nokey to generate a missing key report
+# Call with noforward to generate a missing .forward report
+
+import string, re, time, ldap, getopt, sys;
+from userdir_ldap import *;
+
+def ShowDups(Attrs,Len):
+   for x in Attrs:
+      if x[1].has_key("keyfingerprint") == 0:
+         continue;
+        
+      Count = 0;
+      for I in x[1]["keyfingerprint"]:
+         if len(I) == Len:
+            Count = Count + 1;
+      if Count > 1:
+         for I in x[1]["keyfingerprint"]:
+           if len(I) == Len:
+              print "%s: %s" % (EmailAddress(x),I);
+
+# Main program starts here
+# Process options
+(options, arguments) = getopt.getopt(sys.argv[1:], "")
+for (switch, val) in options:
+   if (switch == '-a'):
+      DoAdd = 1;
+
+print "Connecting to LDAP directory";
+
+# Connect to the ldap server
+l = ldap.open(LDAPServer);
+l.simple_bind_s("","");
+
+if arguments[0] == "nokey":
+   Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"(!(keyfingerprint=*))",\
+           ["uid","cn","sn","emailforward","comment"]);
+   Attrs.sort();
+   for x in Attrs:
+      print "Key Missing:",EmailAddress(x);
+      if GetAttr(x,"emailforward") != "":
+         print "  ->",GetAttr(x,"emailforward");
+      if GetAttr(x,"comment") != "":
+         print "  :",GetAttr(x,"comment");
+
+if arguments[0] == "noforward":
+   Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"(!(emailforward=*))",\
+           ["uid","cn","sn","emailforward","comment"]);
+   Attrs.sort();
+   for x in Attrs:
+      print "No Forward:",EmailAddress(x);
+
+if arguments[0] == "badpriv":
+   Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"(&(!(keyfingerprint=*))(privatesub=*))",\
+           ["uid","cn","sn","privatesub"]);
+   Attrs.sort();
+   for x in Attrs:
+      print EmailAddress(x)+": "+GetAttr(x,"privatesub");
+
+if arguments[0] == "nopriv":
+   Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"(&(keyfingerprint=*)(!(privatesub=*)))",\
+           ["uid","cn","sn","privatesub"]);
+   Attrs.sort();
+   for x in Attrs:
+      print "  ",EmailAddress(x)+": "+GetAttr(x,"privatesub");
+
+if arguments[0] == "keymap":
+   Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"uid=*",\
+           ["uid","cn","sn","keyfingerprint"]);
+   Attrs.sort();
+   for x in Attrs:
+      if x[1].has_key("keyfingerprint"):
+         for I in x[1]["keyfingerprint"]:
+           print "%s: %s" % (EmailAddress(x),I);
+
+if arguments[0] == "devcount":
+   Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"(&(keyfingerprint=*)(gidnumber=800))",\
+           ["uid"]);
+   Count = 0;
+   for x in Attrs:
+      Count = Count + 1;
+   print "There are",Count,"developers as of",time.strftime("%a, %d %b %Y %H:%M:%S +0000",time.gmtime(time.time()));
+
+if arguments[0] == "multikeys":
+   Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"uid=*",\
+           ["uid","cn","sn","keyfingerprint"]);
+   Attrs.sort();
+   
+   
+   print "--- PGP Keys ---"
+   ShowDups(Attrs,32);
+   print "--- GPG Keys ---"
+   ShowDups(Attrs,40);
+          
diff --git a/ud-mailgate b/ud-mailgate
new file mode 100755 (executable)
index 0000000..922a1bb
--- /dev/null
@@ -0,0 +1,170 @@
+#!/usr/bin/env python
+# -*- mode: python -*-
+import userdir_gpg, userdir_ldap, sys, traceback, time, ldap, posix;
+import string, pwd
+from userdir_gpg import *;
+from userdir_ldap import *;
+
+# Error codes from /usr/include/sysexits.h
+ReplyTo = ConfModule.replyto;
+PingFrom = ConfModule.pingfrom;
+ChPassFrom = ConfModule.chpassfrom;
+ReplayCacheFile = ConfModule.replaycachefile;
+
+EX_TEMPFAIL = 75;
+EX_PERMFAIL = 65;      # EX_DATAERR
+Error = 'Message Error';
+
+# Handle ping handles an email sent to the 'ping' address (ie this program
+# called with a ping argument) It replies with a dump of the public records.
+def HandlePing(Reply,DnRecord,Key):
+   Subst = {};
+   Subst["__FROM__"] = PingFrom;
+   Subst["__EMAIL__"] = EmailAddress(DnRecord);
+   Subst["__LDAPFIELDS__"] = PrettyShow(Attrs[0]);
+   Subst["__ADMIN__"] = ReplyTo;
+
+   return Reply + TemplateSubst(Subst,open(TemplatesDir+"ping-reply","r").read());
+
+# Handle a change password email sent to the change password address
+# (this program called with the chpass argument)
+def HandleChPass(Reply,DnRecord,Key):
+   # Generate a random password
+   Password = GenPass();
+   Pass = HashPass(Password);
+      
+   # Use GPG to encrypt it      
+   Message = GPGEncrypt("Your new password is '" + Password + "'\n",\
+                        "0x"+Key[1],Key[4]);
+   Password = None;
+
+   if Message == None:
+      raise Error, "Unable to generate the encrypted reply, gpg failed.";
+
+   if (Key[4] == 1):
+      Type = "Your message was encrypted using PGP 2.x\ncompatibility mode.";
+   else:
+      Type = "Your message was encrypted using GPG (OpenPGP)\ncompatibility "\
+             "mode, without IDEA. This message cannot be decoded using PGP 2.x";
+   
+   Subst = {};
+   Subst["__FROM__"] = ChPassFrom;
+   Subst["__EMAIL__"] = EmailAddress(DnRecord);
+   Subst["__CRYPTTYPE__"] = Type;
+   Subst["__PASSWORD__"] = Message;
+   Subst["__ADMIN__"] = ReplyTo;
+   Reply = Reply + TemplateSubst(Subst,open(TemplatesDir+"passwd-changed","r").read());
+   
+   # Connect to the ldap server
+   l = ldap.open(LDAPServer);
+   F = open(PassDir+"/pass-"+pwd.getpwuid(posix.getuid())[0],"r");
+   AccessPass = string.split(string.strip(F.readline())," ");
+   F.close();
+
+   # Modify the password
+   l.simple_bind_s("uid="+AccessPass[0]+","+BaseDn,AccessPass[1]);
+   Rec = [(ldap.MOD_REPLACE,"userPassword","{crypt}"+Pass)];
+   Dn = "uid=" + GetAttr(DnRecord,"uid") + "," + BaseDn;
+   l.modify_s(Dn,Rec);
+   
+   return Reply;
+      
+# Start of main program
+ErrMsg = "Indeterminate Error";
+ErrType = EX_TEMPFAIL;
+try:
+   # Startup the replay cache
+   ErrType = EX_TEMPFAIL;
+   ErrMsg = "Failed to initialize the replay cache:";
+   RC = ReplayCache(ReplayCacheFile);
+   RC.Clean();
+
+   # Get the email 
+   ErrType = EX_PERMFAIL;
+   ErrMsg = "Failed to understand the email or find a signature:";
+   Email = mimetools.Message(sys.stdin,0);
+   Msg = GetClearSig(Email);
+
+   # Check the signature   
+   ErrMsg = "Unable to check the signature or the signature was invalid:";
+   Res = GPGCheckSig(Msg[0]);
+
+   if Res[0] != None:
+      raise Error, Res[0];
+      
+   if Res[3] == None:
+      raise Error, "Null signature text";
+
+   # Extract the plain message text in the event of mime encoding
+   ErrMsg = "Problem stripping MIME headers from the decoded message"
+   if Msg[1] == 1:
+      try:
+         Index = string.index(Res[3],"\n\n") + 2;
+      except ValueError:
+         Index = string.index(Res[3],"\n\r\n") + 3;
+      PlainText = Res[3][Index:];
+   else:
+      PlainText = Res[3];   
+
+   # Check the signature against the replay cache
+   ErrMsg = "The replay cache rejected your message. Check your clock!";
+   Rply = RC.Check(Res[1]);
+   if Rply != None:
+      raise Error, Rply;
+   RC.Add(Res[1]);
+
+   # Connect to the ldap server
+   ErrType = EX_TEMPFAIL;
+   ErrMsg = "An error occured while performing the LDAP lookup";
+   l = ldap.open(LDAPServer);
+   l.simple_bind_s("","");
+
+   # Search for the matching key fingerprint
+   Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"keyfingerprint=" + Res[2][1]);
+   if len(Attrs) == 0:
+      raise Error, "Key not found"
+   if len(Attrs) != 1:
+      raise Error, "Oddly your key fingerprint is assigned to more than one account.."
+
+   # Determine the sender address
+   ErrType = EX_PERMFAIL;
+   ErrMsg = "A problem occured while trying to formulate the reply";
+   Sender = Email.getheader("Reply-To");
+   if Sender == None:
+      Sender = Email.getheader("From");
+   if Sender == None:
+      raise Error, "Unable to determine the sender's address";
+
+   # Formulate a reply
+   Date = time.strftime("%a, %d %b %Y %H:%M:%S +0000",time.gmtime(time.time()));
+   Reply = "To: %s\nReply-To: %s\nDate: %s\n" % (Sender,ReplyTo,Date);
+
+   # Dispatch
+   if sys.argv[1] == "ping":
+      Reply = HandlePing(Reply,Attrs[0],Res[2]);
+   elif sys.argv[1] == "chpass":
+      if string.find(string.strip(PlainText),"Please change my Debian password") != 0:
+         raise Error,"Please send a signed message where the first line of text is the string 'Please change my Debian password'";
+      Reply = HandleChPass(Reply,Attrs[0],Res[2]);
+   else:
+      print sys.argv;
+      raise Error, "Incorrect Invokation";
+
+   # Send the message through sendmail      
+   ErrMsg = "A problem occured while trying to send the reply";
+   Child = posix.popen("/usr/sbin/sendmail -t","w");
+#   Child = posix.popen("cat","w");
+   Child.write(Reply);
+   if Child.close() != None:
+      raise Error, "Sendmail gave a non-zero return code";
+
+except:
+   print ErrMsg;
+   print "==> %s: %s" %(sys.exc_type,sys.exc_value);
+   List = traceback.extract_tb(sys.exc_traceback);
+   if len(List) > 1:
+      print "Trace: ";
+      for x in List:
+         print "   %s %s:%u: %s" %(x[2],x[0],x[1],x[3]);
+   sys.exit(ErrType);
+   
diff --git a/ud-passchk b/ud-passchk
new file mode 100755 (executable)
index 0000000..6929f4f
--- /dev/null
@@ -0,0 +1,47 @@
+#!/usr/bin/env python
+# -*- mode: python -*-
+# Checks the passwd file to make sure all entries are in the directory
+
+import string, ldap, getopt, sys, os;
+from userdir_ldap import *;
+
+def PassCheck(l,File,HomePrefix):
+   F = open(File,"r");
+   
+   # Fetch all the users and generate a map out of them
+   Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"uid=*",\
+           ["uid","uidnumber","gidnumber","loginshell"]);
+   UIDMap = {};
+   for x in Attrs:
+      if x[1].has_key("uid") == 0:
+         continue;
+      UIDMap[x[1]["uid"][0]] = x[1];
+   
+   # Iterate over every user in the passwd file
+   while(1):
+      Line = F.readline();
+      if Line == "":
+         break;
+      
+      Split = string.split(Line,":");
+      if UIDMap.has_key(Split[0]) == 0:
+         print Line,
+        continue;
+
+      Ats = UIDMap[Split[0]];
+      Miss = [];
+      if Ats.has_key("uidnumber") and Ats["uidnumber"][0] != Split[2]: 
+         Miss.append("UID");
+      if Ats.has_key("uidnumber") and Ats["gidnumber"][0] != Split[3]: 
+         Miss.append("GID");
+      if Ats.has_key("homedirectory") and \
+         split[5] != HomePrefix + Split[0]:
+         Miss.append("Home");
+      if len(Miss) != 0:
+         print "mismatch",Split[0],Miss;
+
+# Connect to the ldap server
+l = ldap.open(LDAPServer);
+l.simple_bind_s("","");
+
+PassCheck(l,sys.argv[1],sys.argv[2]);
diff --git a/ud-replicate b/ud-replicate
new file mode 100755 (executable)
index 0000000..c69c6b8
--- /dev/null
@@ -0,0 +1,12 @@
+#! /bin/sh
+# The rsync source host needs to be customized..
+set -e
+
+HOST=`hostname -f`
+cd /tmp/
+cd /var/lib/misc > /dev/null 2>&1 || cd /var/state/glibc/ > /dev/null 2>&1 || cd /var/db/ > /dev/null 2>&1
+lockfile -r 1 -l 3600 lock
+rsync -e ssh -rp sshdist@samosa:/var/cache/userdir-ldap/hosts/$HOST . > /dev/null 2>&1
+makedb $HOST/passwd.tdb -o passwd.db > /dev/null 2>&1
+makedb $HOST/shadow.tdb -o shadow.db > /dev/null 2>&1
+makedb $HOST/group.tdb -o group.db > /dev/null 2>&1
diff --git a/ud-useradd b/ud-useradd
new file mode 100755 (executable)
index 0000000..b875470
--- /dev/null
@@ -0,0 +1,248 @@
+#!/usr/bin/env python
+# -*- mode: python -*-
+
+import string, re, time, ldap, getopt, sys, posix, pwd;
+from userdir_ldap import *;
+from userdir_gpg import *;
+
+AddressSplit = re.compile("(.*).*<([^@]*)@([^>]*)>");
+
+# This tries to search for a free UID. There are two possible ways to do
+# this, one is to fetch all the entires and pick the highest, the other
+# is to randomly guess uids until one is free. This uses the formar.
+# Regrettably ldap doesn't have an integer attribute comparision function
+# so we can only cut the search down slightly 
+def GetFreeID(l):
+   HighestUID = 1400;
+   Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"uidnumber>="+str(HighestUID),["uidnumber"]);
+   HighestUID = 0;
+   for I in Attrs:
+      ID = int(GetAttr(I,"uidnumber","0"));
+      if ID > HighestUID: 
+         HighestUID = ID;
+   return HighestUID + 1;
+
+# Main starts here
+
+# Process options
+(options, arguments) = getopt.getopt(sys.argv[1:], "u:")
+for (switch, val) in options:
+   if (switch == '-u'):
+      AdminUser = val
+
+print "Accessing LDAP directory as '" + AdminUser + "'";
+Password = getpass(AdminUser + "'s password: ");
+
+# Connect to the ldap server
+l = ldap.open(LDAPServer);
+UserDn = "uid=" + AdminUser + "," + BaseDn;
+l.simple_bind_s(UserDn,Password);
+
+# Locate the key of the user we are adding
+GPGBasicOptions[0] = "--batch"           # Permit loading of the config file
+while (1):
+   Foo = raw_input("Who are you going to add (for a GPG search)? ");
+   if Foo == "":
+      continue;
+
+   Keys = GPGKeySearch(Foo);
+
+   if len(Keys) == 0:
+      print "Sorry, that search did not turn up any keys";
+      continue;
+   if len(Keys) > 1:
+      print "Sorry, more than one key was found, please specify the key to use by\nfingerprint:";
+      for i in Keys:
+         GPGPrintKeyInfo(i);
+      continue;
+
+   print
+   print "A matching key was found:"
+   GPGPrintKeyInfo(Keys[0]);
+   break;
+   
+# Crack up the email address from the key into a best guess 
+# first/middle/last name
+Match = AddressSplit.match(Keys[0][2]);
+if Match == None:
+   (cn,mn,sn,email,account) = ('','','','','');
+else:
+   (cn,mn,sn) = NameSplit(re.sub('["]','',Match.groups()[0]))
+   email = Match.groups()[1] + '@' + Match.groups()[2];
+   account = Match.groups()[1];
+
+privsub = email;
+gidnumber = str(DefaultGID);
+uidnumber = 0;
+
+# Decide if we should use IDEA encryption
+UsePGP2 = 0;
+while len(Keys[0][1]) < 40:
+   Res = raw_input("Use PGP2.x compatibility [no]? ");
+   if Res == "yes":
+      UsePGP2 = 1;
+      break;
+   if Res == "":
+      break;
+
+# Try to get a uniq account name
+Update=0
+while 1:
+   Res = raw_input("Login account [" + account + "]? ");
+   if Res != "":
+      account = Res;
+   Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"uid=" + account);
+   if len(Attrs) == 0:
+      break;
+   Res = raw_input("That account already exists, update [no]? ");
+   if Res == "yes":
+      # Update mode, fetch the default values from the directory
+      Update = 1;
+      privsub = GetAttr(Attrs[0],"privatesub");
+      gidnumber = GetAttr(Attrs[0],"gidnumber");
+      uidnumber = GetAttr(Attrs[0],"uidnumber");
+      email = GetAttr(Attrs[0],"emailforward");
+      cn = GetAttr(Attrs[0],"cn");
+      sn = GetAttr(Attrs[0],"sn");
+      mn = GetAttr(Attrs[0],"mn");
+      if privsub == None or privsub == "":
+         privsub = " ";
+      break;
+
+# Prompt for the first/last name and email address
+Res = raw_input("First name [" + cn + "]? ");
+if Res != "":
+   cn = Res;
+Res = raw_input("Middle name [" + mn + "]? ");
+if Res != "":
+   mn = Res;
+Res = raw_input("Last name [" + sn + "]? ");
+if Res != "":
+   sn = Res;
+Res = raw_input("Email forwarding address [" + email + "]? ");
+if Res != "":
+   email = Res;
+
+# Debian-Private subscription
+Res = raw_input("Subscribe to debian-private (space is none) [" + privsub + "]? ");
+if Res != "":
+   privsub = Res;
+
+# GID
+Res = raw_input("Group ID Number [" + gidnumber + "]? ");
+if Res != "":
+   gidnumber = Res;
+
+# UID
+if uidnumber == 0:
+   uidnumber = GetFreeID(l);
+
+# Generate a random password
+if Update == 0:
+   Password = raw_input("User's Password (Enter for random)? ");
+
+   if Password == "":
+      print "Randomizing and encrypting password"
+      Password = GenPass();
+      Pass = HashPass(Password);
+      print "PASS: ", Password;
+
+      # Use GPG to encrypt it, pass the fingerprint to ID it
+      CryptedPass = GPGEncrypt("Your new password is '" + Password + "'\n",\
+                               "0x"+Keys[0][1],UsePGP2);
+      Password = None;
+      if CryptedPass == None:
+        raise "Error","Password Encryption failed"
+   else:
+      Pass = HashPass(Password);
+      CryptedPass = "Your password has been set to the previously agreed value.";
+else:
+   CryptedPass = "";
+   Pass = None;
+
+# Now we have all the bits of information.
+if mn != "": 
+   FullName = "%s %s %s" % (cn,mn,sn);
+else:
+   FullName = "%s %s" % (cn,sn);
+print "------------";
+print "Final information collected:"
+print " %s <%s@%s>:" % (FullName,account,EmailAppend);
+print "   Assigned UID:",uidnumber," GID:", gidnumber;
+print "   Email forwarded to:",email;
+print "   Private Subscription:",privsub;
+print "   GECOS Field: \"%s,,,,\"" % (FullName);
+print "   Login Shell: /bin/bash";
+print "   Key Fingerprint:",Keys[0][1];
+Res = raw_input("Continue [no]? ");
+if Res != "yes":
+   sys.exit(1);
+
+# Initialize the substitution Map
+Subst = {}
+Subst["__REALNAME__"] = FullName;
+Subst["__WHOAMI__"] = pwd.getpwuid(posix.getuid())[0];
+Subst["__DATE__"] = time.strftime("%a, %d %b %Y %H:%M:%S +0000",time.gmtime(time.time()));
+Subst["__LOGIN__"] = account;
+Subst["__PRIVATE__"] = privsub;
+Subst["__EMAIL__"] = email;
+Subst["__PASSWORD__"] = CryptedPass;
+Subst["__LISTPASS__"] = string.strip(open(pwd.getpwuid(posix.getuid())[5]+"/.debian-lists_passwd","r").read());
+
+# Generate the LDAP request
+Rec = [(ldap.MOD_REPLACE,"uid",account),
+       (ldap.MOD_REPLACE,"uidNumber",str(uidnumber)),
+       (ldap.MOD_REPLACE,"gidNumber",str(gidnumber)),
+       (ldap.MOD_REPLACE,"gecos",FullName+",,,,"),
+       (ldap.MOD_REPLACE,"loginShell","/bin/bash"),
+       (ldap.MOD_REPLACE,"keyfingerprint",Keys[0][1]),
+       (ldap.MOD_REPLACE,"cn",cn),
+       (ldap.MOD_REPLACE,"mn",mn),
+       (ldap.MOD_REPLACE,"sn",sn),
+       (ldap.MOD_REPLACE,"emailforward",email),
+       (ldap.MOD_REPLACE,"shadowLastChange",str(int(time.time()/24/60/60))),
+       (ldap.MOD_REPLACE,"shadowMin","0"),
+       (ldap.MOD_REPLACE,"shadowMax","99999"),
+       (ldap.MOD_REPLACE,"shadowWarning","7"),
+       (ldap.MOD_REPLACE,"shadowInactive",""),
+       (ldap.MOD_REPLACE,"shadowExpire","")];
+if privsub != " ":
+   Rec.append((ldap.MOD_REPLACE,"privatesub",privsub));
+if Pass != None:
+   Rec.append((ldap.MOD_REPLACE,"userPassword","{crypt}"+Pass));
+
+# Submit the modification request   
+Dn = "uid=" + account + "," + BaseDn;
+print "Updating LDAP directory..",
+sys.stdout.flush();
+try:
+   l.add_s(Dn,[("uid",account),
+               ("objectclass","top"),
+               ("objectclass","account"),
+               ("objectclass","posixAccount"),
+               ("objectclass","shadowAccount"),
+               ("objectclass","debiandeveloper")]);
+except ldap.ALREADY_EXISTS:
+   pass;
+
+# Send the modify request
+l.modify_s(Dn,Rec);
+print;
+
+# Abort email sends for an update operation
+if Update == 1:
+   print "Account is not new, Not sending mails"
+   sys.exit(0);
+   
+# Do the subscription/welcome message
+if privsub != " ":
+   print TemplateSubst(Subst,open("templates/list-subscribe","r").read());
+
+# Send the Welcome message
+print "Sending Welcome Email"
+Reply = TemplateSubst(Subst,open("templates/welcome-message-"+gidnumber,"r").read());
+Child = posix.popen("/usr/sbin/sendmail -t","w");
+#Child = posix.popen("cat","w");
+Child.write(Reply);
+if Child.close() != None:
+   raise Error, "Sendmail gave a non-zero return code";
diff --git a/ud-userimport b/ud-userimport
new file mode 100755 (executable)
index 0000000..2e6f903
--- /dev/null
@@ -0,0 +1,238 @@
+#!/usr/bin/env python
+# -*- mode: python -*-
+# Imports passwd, shadow and group files into the directory.
+# You should cleanse the files of anything you do not want to add to the
+# directory.
+#
+# The first step is to call this script to import the passwd file and
+# create all the new entries. This should be done on an empty freshly 
+# initialized directory with the rootdn/password set in the server.
+# The command to execute is
+#   ldapimport -a -p ~/passwd
+# The -a tells the script to add all the entries it finds, it should be
+# used only once.
+#
+# The next step is to import the shadow file and group, no clensing need be 
+# done for 
+# this as any entries that do not exist will be ignored (silently)
+#  ldapimport -s /etc/shadow -g /etc/group
+# 
+
+import string, re, time, ldap, getopt, sys;
+from userdir_ldap import *;
+
+DoAdd = 0;
+WritePasses = 1;
+Passwd = "";
+Shadow = "";
+Group = "";
+
+# This parses a gecos field and returns a tuple containing the new normalized
+# field and the first, middle and last name of the user. Gecos is formed
+# in the standard debian manner with 5 feilds seperated by commas
+def ParseGecos(Field):
+   Gecos = re.split("[,:]",Field);
+   cn = "";
+   mn = "";
+   sn = "";
+   if (len(Gecos) >= 1):
+      (cn,mn,sn) = NameSplit(Gecos[0]);
+
+      # Normalize the gecos field
+      if (len(Gecos) > 5):
+         Gecos = Gecos[0:4];
+      else:
+         while (len(Gecos) < 5):
+            Gecos.append("");
+   else:
+      Gecos = ["","","","",""];
+
+   # Reconstruct the gecos after mauling it
+   Field = Gecos[0] + "," + Gecos[1] + "," + Gecos[2] + "," + \
+           Gecos[3] + "," + Gecos[4];
+   return (Field,cn,mn,sn);
+
+# Check if a number string is really a number
+def CheckNumber(Num):
+   for x in Num:
+      string.index(string.digits,x);
+
+# Read the passwd file into the database
+def DoPasswd(l,Passwd):
+   # Read the passwd file and import it
+   Passwd = open(Passwd,"r");
+   Outstanding = 0;
+   while(1):
+      Line = Passwd.readline();
+      if Line == "":
+         break;
+
+      Split = re.split("[:\n]",Line);
+      (Split[4],cn,mn,sn) = ParseGecos(Split[4]);
+      CheckNumber(Split[2]);
+      CheckNumber(Split[3]);
+      Rec = [(ldap.MOD_REPLACE,"uid",Split[0]),
+             (ldap.MOD_REPLACE,"uidNumber",Split[2]),
+             (ldap.MOD_REPLACE,"gidNumber",Split[3]),
+             (ldap.MOD_REPLACE,"gecos",Split[4]),
+             (ldap.MOD_REPLACE,"homeDirectory",Split[5]),
+             (ldap.MOD_REPLACE,"loginShell",Split[6]),
+            (ldap.MOD_REPLACE,"cn",cn),
+            (ldap.MOD_REPLACE,"mn",mn),
+            (ldap.MOD_REPLACE,"sn",sn)];
+
+      Dn = "uid=" + Split[0] + "," + BaseDn;
+      print "Importing",Dn,
+      sys.stdout.flush();
+
+      # Unfortunately add_s does not take the same args as modify :|
+      if (DoAdd == 1):
+         try:
+            l.add_s(Dn,[("uid",Split[0]),
+                        ("objectclass","top"),
+                        ("objectclass","account"),
+                        ("objectclass","posixAccount"),
+                        ("objectclass","shadowAccount"),
+                        ("objectclass","debiandeveloper")]);
+         except ldap.ALREADY_EXISTS:
+            print "exists",;
+
+      # Send the modify request
+      l.modify(Dn,Rec);
+      Outstanding = Outstanding + 1;
+      Outstanding = FlushOutstanding(l,Outstanding,1);
+      print "done";
+   FlushOutstanding(l,Outstanding);
+
+# Read the shadow file into the database
+def DoShadow(l,Shadow):
+   # Read the passwd file and import it
+   Shadow = open(Shadow,"r");
+   Outstanding = 0;
+   while(1):
+      Line = Shadow.readline();
+      if Line == "":
+         break;
+
+      Split = re.split("[:\n]",Line);
+      
+      # Ignore system accounts with no password, they do not belong in the
+      # directory.
+      if (Split[1] == 'x' or Split[1] == '*'):
+         print "Ignoring system account,",Split[0];
+         continue;
+
+      for x in range(2,8):
+         CheckNumber(Split[x]);
+
+      Rec = [(ldap.MOD_REPLACE,"shadowLastChange",Split[2]),
+             (ldap.MOD_REPLACE,"shadowMin",Split[3]),
+             (ldap.MOD_REPLACE,"shadowMax",Split[4]),
+             (ldap.MOD_REPLACE,"shadowWarning",Split[5]),
+             (ldap.MOD_REPLACE,"shadowInactive",Split[6]),
+             (ldap.MOD_REPLACE,"shadowExpire",Split[7])];
+      if (WritePasses == 1):
+         Rec.append((ldap.MOD_REPLACE,"userPassword","{crypt}"+Split[1]));
+
+      Dn = "uid=" + Split[0] + "," + BaseDn;
+      print "Importing",Dn,
+      sys.stdout.flush();
+
+      # Send the modify request
+      l.modify(Dn,Rec);
+      Outstanding = Outstanding + 1;
+      print "done";
+      Outstanding = FlushOutstanding(l,Outstanding,1);
+   FlushOutstanding(l,Outstanding);
+
+# Read the group file into the database
+def DoGroup(l,Group):
+   # Read the passwd file and import it
+   Group = open(Group,"r");
+   Outstanding = 0;
+   while(1):
+      Line = Group.readline();
+      if Line == "":
+         break;
+
+      # Split up the group information
+      Split = re.split("[:\n]",Line);
+      Members = re.split("[, ]*",Split[3]);
+      CheckNumber(Split[2]);
+
+      # Iterate over the membership list and add the membership information
+      # To the directory
+      Rec = [(ldap.MOD_ADD,"supplementarygid",Split[0])];
+      Counter = 0;
+      for x in Members:
+        if x == "":
+            continue;
+           
+         Dn = "uid=" + x + "," + BaseDn;
+         print "Adding",Dn,"to group",Split[0];
+        Counter = Counter+1;
+
+         # Send the modify request
+         l.modify(Dn,Rec);
+         Outstanding = Outstanding + 1;
+         Outstanding = FlushOutstanding(l,Outstanding,1);
+        
+      if Counter == 0:
+         continue;
+
+      Rec = [(ldap.MOD_REPLACE,"gid",Split[0]),
+             (ldap.MOD_REPLACE,"gidNumber",Split[2])];
+
+      Dn = "gid=" + Split[0] + "," + BaseDn;
+      print "Importing",Dn,
+      sys.stdout.flush();
+
+      # Unfortunately add_s does not take the same args as modify :|
+      if (DoAdd == 1):
+         try:
+            l.add_s(Dn,[("gid",Split[0]),
+                        ("objectclass","top"),
+                        ("objectclass","posixGroup")]);
+         except ldap.ALREADY_EXISTS:
+            print "exists",;
+
+      # Send the modify request
+      l.modify(Dn,Rec);
+      Outstanding = Outstanding + 1;
+      print ".";
+
+   FlushOutstanding(l,Outstanding);
+
+# Process options
+(options, arguments) = getopt.getopt(sys.argv[1:], "ap:s:g:xu:")
+for (switch, val) in options:
+   if (switch == '-a'):
+      DoAdd = 1;
+   if (switch == '-x'):
+      WritePasses = 0;
+   elif (switch == '-p'):
+      Passwd = val
+   elif (switch == '-s'):
+      Shadow = val
+   elif (switch == '-g'):
+      Group = val
+   elif (switch == '-u'):
+      AdminUser = val
+
+# Main program starts here
+print "Accessing LDAP directory as '" + AdminUser + "'";
+Password = getpass(AdminUser + "'s password: ");
+
+# Connect to the ldap server
+l = ldap.open(LDAPServer);
+UserDn = "uid=" + AdminUser + "," + BaseDn;
+l.simple_bind_s(UserDn,Password);
+
+if (Passwd != ""):
+   DoPasswd(l,Passwd);
+
+if (Shadow != ""):
+   DoShadow(l,Shadow);
+
+if (Group != ""):
+   DoGroup(l,Group);
diff --git a/ud-xearth b/ud-xearth
new file mode 100755 (executable)
index 0000000..b1c1fee
--- /dev/null
+++ b/ud-xearth
@@ -0,0 +1,80 @@
+#!/usr/bin/env python
+# -*- mode: python -*-
+# Generate an xearth database from the LDAP entries
+# LDAP entires for lat/long can be in one of 3 different formats
+#    1) Decimal Degrees
+#        +-DDD.DDDDDDDDDDDDDDD
+#    2) Degrees Minutes (DGM), common output from GPS units
+#        +-DDDMM.MMMMMMMMMMMMM
+#    3) Degrees Minutes Seconds (DGMS)
+#        +-DDDMMSS.SSSSSSSSSSS
+# Decimal Degrees is the most basic format, but to have good accuracy it
+# needs a large number of decimals. The other formats are all derived from it:
+#  DGM -> DD   DDD + (MM.MMMMMMMM)/60
+#  DGMS -> DD  DDD + (MM + (SS.SSSSSS)/60)/60
+# For Latitude + is North, for Longitude + is East
+
+import string, re, time, ldap, getopt, sys, pwd, posix;
+from userdir_ldap import *;
+
+# This needs to check for leading 0 to disambiguate some things
+def DecDegree(Attr,Type):
+  Parts = re.match('[+-]?(\d*)\\.?(\d*)?',GetAttr(Attr,Type)).groups();
+  Val = string.atof(GetAttr(Attr,Type));
+
+  if (abs(Val) >= 1806060.0):
+     raise ValueError,"Too Big";
+
+  # Val is in DGMS
+  if abs(Val) >= 18060.0 or len(Parts[0]) > 5:
+     Val = Val/100.0;
+     Secs = Val - long(Val);
+     Val = long(Val)/100.0;
+     Min = Val - long(Val);
+     Val = long(Val) + (Min*100.0 + Secs*100.0/60.0)/60.0;
+
+  # Val is in DGM
+  elif abs(Val) >= 180 or len(Parts[0]) > 3:
+     Val = Val/100.0;
+     Min = Val - long(Val);
+     Val = long(Val) + Min*100.0/60.0;
+     
+  if Val >= 0:
+     return "+" + str(Val);
+  return str(Val);
+
+# Main program starts here
+User = pwd.getpwuid(posix.getuid())[0];
+BindUser = User;
+(options, arguments) = getopt.getopt(sys.argv[1:], "u:")
+for (switch, val) in options:
+   if (switch == '-u'):
+      User = val
+
+# Connect to the ldap server
+l = ldap.open(LDAPServer);
+print "Accessing LDAP directory as '" + User + "'";
+Password = getpass(User + "'s password: ");
+UserDn = "uid=" + User + "," + BaseDn;
+l.simple_bind_s(UserDn,Password);
+
+Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"latitude=*",\
+         ["uid","cn","mn","sn","latitude","longitude"]);
+
+#ttrs = [('uid=bma,ou=users,dc=debian,dc=org', {'longitude': ['-0771426.059'], 'sn': ['Almeida'], 'cn': ['Brian'], 'latitude': ['0384514.263'], 'uid': ['bma']}), ('uid=jgg,ou=users,dc=debian,dc=org', {'longitude': ['-11328'], 'sn': ['Gunthorpe'], 'cn': ['Jason'], 'latitude': ['+5333'], 'uid': ['jgg']})]
+Attrs.sort();
+
+print "Markers file will be written to markers.dat,",
+sys.stdout.flush();
+F = open("markers.dat","w");
+Count = 0;
+for x in Attrs:
+   if x[1].has_key("latitude") == 0 or x[1].has_key("longitude") == 0:
+      continue;
+   Count = Count + 1;
+   try:
+      F.write("%16s %16s \"%s\" \t# %s\n"%(DecDegree(x,"latitude"),DecDegree(x,"longitude"),GetAttr(x,"uid"),EmailAddress(x)));
+   except:
+      F.write("# Failed %s => %s: %s\n" %(x[0],sys.exc_type,sys.exc_value));
+F.close();
+print Count,"entries.";
diff --git a/userdir-ldap.conf b/userdir-ldap.conf
new file mode 100644 (file)
index 0000000..70344a4
--- /dev/null
@@ -0,0 +1,51 @@
+# Config file for ldap scripts
+
+# Basic LDAP configuration
+ldaphost = "db.debian.org";
+basedn   = "ou=users,dc=debian,dc=org";
+adminuser = "admin";
+
+# Printable email addresses are shown as: 'cn mn sn <uid@emailappend>'
+emailappend = "debian.org";
+
+# For the mail interface
+maildomain = "db.debian.org";
+replyto = "admin@" + maildomain;
+pingfrom = "ping@" + maildomain;
+chpassfrom = "chpasswd@" + maildomain;
+templatesdir = "/etc/userdir-ldap/templates/";
+replaycachefile = "/var/cache/userdir-ldap/replay";
+#replaycachefile = "/tmp/replay";
+
+# User properties
+defaultgid = 800;
+
+# For the output generator
+generateconf = "/etc/userdir-ldap/generate.conf"
+generatedir = "/var/cache/userdir-ldap/hosts/";
+#generatedir = "/tmp/hosts";
+passdir = "/etc/userdir-ldap/";
+
+# GPG Things
+gpg = "/usr/bin/gpg";
+keyrings = "/usr/share/keyrings/debian-keyring.gpg:/usr/share/keyrings/debian-keyring.pgp";
+
+# For the WEB interface
+webloginhtml = "login.html";
+websearchhtml = "searchform.html";
+websearchresulthtml = "searchresults.html";
+webupdatehtml = "update.html";
+
+webloginurl = "login.cgi";
+websearchurl = "search.cgi";
+webupdateurl = "update.cgi";
+
+# When should authentication tokens expire?
+authexpires = 600;
+
+# How many bytes to use for the blowfish key (max = 56 (448 bits))
+blowfishkeylen = 10;
+
+# Change this!
+authtokenpath = "/var/cache/userdir-ldap/web-cookies";
+countrylist = "/var/www/userdir-ldap/domains.tab";
diff --git a/userdir_gpg.py b/userdir_gpg.py
new file mode 100644 (file)
index 0000000..82e9ed9
--- /dev/null
@@ -0,0 +1,432 @@
+ #!/usr/bin/env python
+# -*- mode: python -*-
+
+# GPG issues - 
+#  - gpgm with a status FD being fed keymaterial and other interesting
+#    things does nothing.. If it could ID the keys and stuff over the
+#    status-fd I could decide what to do with them. I would also like it
+#    to report which key it selected for encryption (also if there 
+#    were multi-matches..) Being able to detect a key-revoke cert would be
+#    good too.
+#  - I would like to be able to fetch the comment and version fields from the 
+#    packets so I can tell if a signature is made by pgp2 to enable the
+#    pgp2 encrypting mode.
+
+import string, mimetools, multifile, sys, StringIO, os, tempfile, re;
+import rfc822, time, fcntl, FCNTL, anydbm
+
+# General GPG options
+GPGPath = "gpg"
+GPGBasicOptions = ["--no-options","--batch","--load-extension","rsa",\
+          "--no-default-keyring","--always-trust"];
+GPGKeyRings = ["--keyring","/usr/share/keyrings/debian-keyring.pgp",\
+               "--keyring","/usr/share/keyrings/debian-keyring.gpg"];
+GPGSigOptions = ["--output","-"];
+GPGSearchOptions = ["--dry-run","--with-colons","--fingerprint"];
+GPGEncryptOptions = ["--output","-","--quiet","--always-trust",\
+                     "--armor","--encrypt"];
+GPGEncryptPGP2Options = ["--set-filename","","--rfc1991",\
+                        "--load-extension","idea",\
+                        "--cipher-algo","idea"] + GPGEncryptOptions;
+
+# Replay cutoff times in seconds
+CleanCutOff = 7*24*60*60;
+AgeCutOff = 4*24*60*60;
+FutureCutOff = 3*24*60*60;
+
+# GetClearSig takes an un-seekable email message stream (mimetools.Message) 
+# and returns a standard PGP '---BEGIN PGP SIGNED MESSAGE---' bounded 
+# clear signed text.
+# If this is fed to gpg/pgp it will verify the signature and spit out the
+# signed text component. Email headers and PGP mime (RFC 2015) is understood
+# but no effort is made to cull any information outside the PGP boundaries
+# Please note that in the event of a mime decode the mime headers will be
+# present in the signature text! The return result is a tuple, the first
+# element is the text itself the second is a mime flag indicating if the
+# result should be mime processed after sig checking.
+def GetClearSig(Msg):
+   Error = 'MIME Error';
+   # See if this is a MIME encoded multipart signed message
+   if Msg.gettype() == "multipart/signed":
+      Boundary = Msg.getparam("boundary");
+      if not Boundary:
+         raise Error, "multipart/* without a boundary parameter";
+
+      # Create the multipart handler. Regrettably their implementation 
+      # Needs seeking..
+      SkMessage = StringIO.StringIO();
+      SkMessage.write(Msg.fp.read());
+      SkMessage.seek(0);
+      mf = multifile.MultiFile(SkMessage)
+      mf.push(Msg.getparam("boundary"));
+
+      # Get the first part of the multipart message
+      if not mf.next():
+         raise Error, "Invalid pgp/mime encoding [no section]";
+
+      # Get the part as a safe seekable stream
+      Signed = StringIO.StringIO();
+      Signed.write(mf.read());
+      InnerMsg = mimetools.Message(Signed);
+
+      # Make sure it is the right type
+      if InnerMsg.gettype() != "text/plain":
+         raise Error, "Invalid pgp/mime encoding [wrong plaintext type]";
+   
+      # Get the next part of the multipart message
+      if not mf.next():
+         raise Error, "Invalid pgp/mime encoding [no section]";
+      InnerMsg = mimetools.Message(mf);
+      if InnerMsg.gettype() != "application/pgp-signature":
+         raise Error, "Invalid pgp/mime encoding [wrong signature type]";
+      Signature = string.joinfields(mf.readlines(),'');
+
+      # Append the PGP boundary header and the signature text to re-form the
+      # original signed block [needs to convert to \r\n]
+      Output = "-----BEGIN PGP SIGNED MESSAGE-----\r\n\r\n" + Signed.getvalue() + Signature;
+      return (Output,1);
+   else:
+      # Just return the message body
+      return (string.joinfields(Msg.fp.readlines(),''),0);
+
+# This opens GPG in 'write filter' mode. It takes Message and sends it
+# to GPGs standard input, pipes the standard output to a temp file along
+# with the status FD. The two tempfiles are passed to GPG by fd and are
+# accessible from the filesystem for only a short period. Message may be
+# None in which case GPGs stdin is closed directly after forking. This
+# is best used for sig checking and encryption.
+# The return result is a tuple (Exit,StatusFD,OutputFD), both fds are
+# fully rewound and readable.
+def GPGWriteFilter(Program,Options,Message):
+   # Make sure the tmp files we open are unreadable, there is a short race
+   # between when the temp file is opened and unlinked that some one else
+   # could open it or hard link it. This is not important however as no 
+   # Secure data is fed through the temp files.
+   OldMask = os.umask(0777);
+   try:
+      Output = tempfile.TemporaryFile("w+b");
+      GPGText = tempfile.TemporaryFile("w+b");
+      InPipe = os.pipe();
+      InPipe = [InPipe[0],InPipe[1]];
+   finally:
+      os.umask(OldMask);
+      
+   try:
+      # Fork off GPG in a horrible way, we redirect most of its FDs
+      # Input comes from a pipe and its two outputs are spooled to unlinked
+      # temp files (ie private)
+      Child = os.fork();
+      if Child == 0:
+         try:
+           os.dup2(InPipe[0],0);
+            os.close(InPipe[1]);
+           os.dup2(Output.fileno(),1);
+           os.dup2(os.open("/dev/null",os.O_WRONLY),2);
+           os.dup2(GPGText.fileno(),3);
+           
+           Args = [Program,"--status-fd","3"] + GPGBasicOptions + GPGKeyRings + Options
+           os.execvp(Program,Args);
+        finally:
+           os._exit(100);
+      
+      # Get rid of the other end of the pipe
+      os.close(InPipe[0])
+      InPipe[0] = -1;
+
+      # Send the message
+      if Message != None:
+         try:
+            os.write(InPipe[1],Message);
+         except:
+           pass;
+      os.close(InPipe[1]);
+      InPipe[1] = -1;
+
+      # Wait for GPG to finish
+      Exit = os.waitpid(Child,0);
+
+      # Create the result including the new readable file descriptors
+      Result = (Exit,os.fdopen(os.dup(GPGText.fileno()),"r"), \
+                os.fdopen(os.dup(Output.fileno()),"r"));
+      Result[1].seek(0);
+      Result[2].seek(0);
+
+      Output.close();
+      GPGText.close();
+      return Result;
+   finally:
+      if InPipe[0] != -1:
+         os.close(InPipe[0]);
+      if InPipe[1] != -1:
+         os.close(InPipe[1]);
+      Output.close();
+      GPGText.close();
+
+# This takes a text passage, a destination and a flag indicating the 
+# compatibility to use and returns an encrypted message to the recipient.
+# It is best if the recipient is specified using the hex key fingerprint
+# of the target, ie 0x64BE1319CCF6D393BF87FF9358A6D4EE
+def GPGEncrypt(Message,To,PGP2):
+   # Encrypt using the PGP5 block encoding and with the PGP5 option set.
+   # This will handle either RSA or DSA/DH asymetric keys.
+   # In PGP2 compatible mode IDEA and rfc1991 encoding are used so that
+   # PGP2 can read the result. RSA keys do not need PGP2 to be set, as GPG
+   # can read a message encrypted with blowfish and RSA.
+   if PGP2 == 0:
+      try:
+         Res = None;
+         Res = GPGWriteFilter(GPGPath,["-r",To]+GPGEncryptOptions,Message);
+         if Res[0][1] != 0:
+            return None;
+         Text = Res[2].read();
+         return Text;
+      finally:
+         if Res != None:
+            Res[1].close();
+            Res[2].close();
+   else:
+      # We have to call gpg with a filename or it will create a packet that
+      # PGP2 cannot understand.
+      TmpName = tempfile.mktemp();
+      try:
+         Res = None;
+         MsgFile = open(TmpName,"wc");
+         MsgFile.write(Message);
+         MsgFile.close();
+         Res = GPGWriteFilter(GPGPath,["-r",To]+GPGEncryptPGP2Options+[TmpName],None);
+         if Res[0][1] != 0:
+            return None;
+         Text = Res[2].read();
+         return Text;
+      finally:
+         try:
+            os.unlink(TmpName);
+         except:
+            pass;
+         if Res != None:
+            Res[1].close();
+            Res[2].close();
+
+# Checks the signature of a standard PGP message, like that returned by
+# GetClearSig. It returns a large tuple of the form:
+#   (Why,(SigId,Date,KeyFinger),(KeyID,KeyFinger,Owner,Length,PGP2),Text);
+# Where,
+#  Why = None if checking was OK otherwise an error string. 
+#  SigID+Date represent something suitable for use in a replay cache. The
+#             date is returned as the number of seconds since the UTC epoch.
+#             The keyID is also in this tuple for easy use of the replay 
+#             cache
+#  KeyID, KeyFinger and Owner represent the Key used to sign this message
+#         PGP2 indicates if the message was created using PGP 2.x 
+#  Text is the full byte-for-byte signed text in a string
+def GPGCheckSig(Message):
+   Res = None;
+   try:
+      Res = GPGWriteFilter(GPGPath,GPGSigOptions,Message);
+      Exit = Res[0];
+
+      # Parse the GPG answer
+      Strm = Res[1];
+      GoodSig = 0;
+      SigId = None;
+      KeyFinger = None;
+      KeyID = None;
+      Owner = None;
+      Date = None;
+      Why = None;
+      TagMap = {};
+      while(1):
+         # Grab and split up line
+         Line = Strm.readline();
+         if Line == "":
+            break;
+         Split = re.split("[ \n]",Line);
+        if Split[0] != "[GNUPG:]":
+           continue;
+
+         # We only process the first occurance of any tag.
+         if TagMap.has_key(Split[1]):
+            continue;
+         TagMap[Split[1]] = None;
+
+        # Good signature response
+         if Split[1] == "GOODSIG":
+            # Just in case GPG returned a bad signal before this (bug?)
+           if Why == None:
+              GoodSig = 1;
+           KeyID = Split[2];
+           Owner = string.join(Split[3:],' ');
+           
+        # Bad signature response
+        if Split[1] == "BADSIG":
+           GoodSig = 0;
+           KeyID = Split[2];
+            Why = "Verification of signature failed";
+
+        # Bad signature response
+        if Split[1] == "ERRSIG" or Split[1] == "NO_PUBKEY":
+           GoodSig = 0;
+           KeyID = Split[2];
+           if Split[7] == '9':
+               Why = "Unable to verify signature, signing key missing.";
+            elif Split[7] == '4':
+               Why = "Unable to verify signature, unknown packet format/key type";
+           else:   
+               Why = "Unable to verify signature, unknown reason";
+
+        # Expired signature
+        if Split[1] == "SIGEXPIRED":
+           GoodSig = 0;
+            Why = "Signature has expired";
+           
+        # Revoked key
+        if Split[1] == "KEYREVOKED":
+           GoodSig = 0;
+            Why = "Signing key has been revoked";
+
+        # Corrupted packet
+        if Split[1] == "NODATA" or Split[1] == "BADARMOR":
+           GoodSig = 0;
+            Why = "The packet was corrupted or contained no data";
+           
+         # Signature ID
+        if Split[1] == "SIG_ID":
+           SigId = Split[2];
+           Date = long(Split[4]);
+
+         # ValidSig has the key finger print
+        if Split[1] == "VALIDSIG":
+           KeyFinger = Split[2];
+
+      # Reopen the stream as a readable stream
+      Text = Res[2].read();
+
+      # A gpg failure is an automatic bad signature
+      if Exit[1] != 0 and Why == None:
+         GoodSig = 0;
+         Why = "GPG execution failed " + str(Exit[0]);
+
+      if GoodSig == 0 and (Why == None or len(Why) == 0):
+         Why = "Checking Failed";
+
+      # Try to decide if this message was sent using PGP2
+      PGP2Message = 0;
+      if (re.search("-----[\n\r][\n\r]?Version: 2\\.",Message) != None):
+         PGP2Message = 1;
+
+      return (Why,(SigId,Date,KeyFinger),(KeyID,KeyFinger,Owner,0,PGP2Message),Text);
+   finally:
+      if Res != None:
+         Res[1].close();
+         Res[2].close();
+
+# Search for keys given a search pattern. The pattern is passed directly
+# to GPG for processing. The result is a list of tuples of the form:
+#   (KeyID,KeyFinger,Owner,Length)
+# Which is similar to the key identification tuple output by GPGChecksig
+def GPGKeySearch(SearchCriteria):
+   Args = [GPGPath] + GPGBasicOptions + GPGKeyRings + GPGSearchOptions + \
+          [SearchCriteria," 2> /dev/null"]
+   Strm = None;
+   Result = [];
+   Owner = "";
+   KeyID = "";
+   try:
+      Strm = os.popen(string.join(Args," "),"r");
+      
+      while(1):
+         # Grab and split up line
+         Line = Strm.readline();
+         if Line == "":
+            break;
+        Split = string.split(Line,":");
+        
+        # Store some of the key fields
+         if Split[0] == 'pub':
+            KeyID = Split[4];
+            Owner = Split[9];
+           Length = int(Split[2]);
+
+         # Output the key
+         if Split[0] == 'fpr':
+            Result.append( (KeyID,Split[9],Owner,Length) );
+   finally:
+      if Strm != None:
+         Strm.close();
+   return Result;
+
+# Print the available key information in a format similar to GPG's output
+# We do not know the values of all the feilds so they are just replaced
+# with ?'s
+def GPGPrintKeyInfo(Ident):
+   print "pub  %u?/%s ??-??-?? %s" % (Ident[3],Ident[0][-8:],Ident[2]);
+   print "     key fingerprint = 0x%s" % (Ident[1]);
+
+# Perform a substition of template 
+def TemplateSubst(Map,Template):
+   for x in Map.keys():
+      Template = string.replace(Template,x,Map[x]);
+   return Template;
+
+# The replay class uses a python DB (BSD db if avail) to implement
+# protection against replay. Replay is an attacker capturing the
+# plain text signed message and sending it back to the victim at some
+# later date. Each signature has a unique signature ID (and signing 
+# Key Fingerprint) as well as a timestamp. The first stage of replay
+# protection is to ensure that the timestamp is reasonable, in particular
+# not to far ahead or too far behind the current system time. The next
+# step is to look up the signature + key fingerprint in the replay database
+# and determine if it has been recived. The database is cleaned out 
+# periodically and old signatures are discarded. By using a timestamp the
+# database size is bounded to being within the range of the allowed times
+# plus a little fuzz. The cache is serialized with a flocked lock file
+class ReplayCache:
+   def __init__(self,Database):
+      self.Lock = open(Database + ".lock","w",0600);
+      fcntl.flock(self.Lock.fileno(),FCNTL.LOCK_EX);
+      self.DB = anydbm.open(Database,"c",0600);
+      self.CleanCutOff = CleanCutOff;
+      self.AgeCutOff = AgeCutOff;
+      self.FutureCutOff = FutureCutOff;
+      
+   # Close the cache and lock
+   def __del__(self):
+      self.close();
+   def close(self):
+      self.DB.close();
+      self.Lock.close();
+      
+   # Clean out any old signatures
+   def Clean(self):
+      CutOff = time.time() - self.CleanCutOff;
+      for x in self.DB.keys():
+         if int(self.DB[x]) <= CutOff:
+           del self.DB[x];
+    
+   # Check a signature. 'sig' is a 3 tuple that has the sigId, date and
+   # key ID
+   def Check(self,Sig):
+      if Sig[0] == None or Sig[1] == None or Sig[2] == None:
+         return "Invalid signature";
+      if int(Sig[1]) > time.time() + self.FutureCutOff:
+         return "Signature has a time too far in the future";
+      if self.DB.has_key(Sig[0] + '-' + Sig[2]):
+         return "Signature has already been received";
+      if int(Sig[1]) < time.time() - self.AgeCutOff:
+         return "Signature has passed the age cut off ";
+      # + str(int(Sig[1])) + ',' + str(time.time()) + "," + str(Sig);
+      return None;
+           
+   # Add a signature, the sig is the same as is given to Check
+   def Add(self,Sig):
+      if Sig[0] == None or Sig[1] == None:
+         raise RuntimeError,"Invalid signature";
+      if Sig[1] < time.time() - self.CleanCutOff:
+         return;
+      Key = Sig[0] + '-' + Sig[2]
+      if self.DB.has_key(Key):
+        if int(self.DB[Key]) < Sig[1]:
+           self.DB[Key] = str(int(Sig[1]));
+      else:
+         self.DB[Key] = str(int(Sig[1]));
+        
diff --git a/userdir_ldap.py b/userdir_ldap.py
new file mode 100644 (file)
index 0000000..eab3cab
--- /dev/null
@@ -0,0 +1,170 @@
+# Some routines and configuration that are used by the ldap progams
+import termios, TERMIOS, re, string, imp, ldap, sys, whrandom, crypt;
+
+try:
+   File = open("/etc/userdir-ldap/userdir-ldap.conf");
+except:
+   File = open("userdir-ldap.conf");
+ConfModule = imp.load_source("userdir_config","/etc/userdir-ldap.conf",File);
+File.close();
+
+BaseDn = ConfModule.basedn;
+BaseDn = ConfModule.basedn;
+LDAPServer = ConfModule.ldaphost;
+EmailAppend = ConfModule.emailappend;
+AdminUser = ConfModule.adminuser;
+GenerateDir = ConfModule.generatedir;
+GenerateConf = ConfModule.generateconf;
+DefaultGID = ConfModule.defaultgid;
+TemplatesDir = ConfModule.templatesdir;
+PassDir = ConfModule.passdir;
+
+# This is a list of common last-name prefixes
+LastNamesPre = {"van": None, "le": None, "de": None, "di": None};
+   
+# Safely get an attribute from a tuple representing a dn and an attribute
+# list. It returns the first attribute if there are multi.
+def GetAttr(DnRecord,Attribute,Default = ""):
+   try:
+      return DnRecord[1][Attribute][0];
+   except IndexError:
+      return Default;
+   except KeyError:
+      return Default;
+   return Default;
+
+# Return a printable email address from the attributes.
+def EmailAddress(DnRecord):
+   cn = GetAttr(DnRecord,"cn");
+   sn = GetAttr(DnRecord,"sn");
+   uid = GetAttr(DnRecord,"uid");
+   if cn == "" and sn == "":
+      return "<" + uid + "@" + EmailAppend + ">";
+   return cn + " " + sn + " <" + uid + "@" + EmailAppend + ">"
+
+# Show a dump like ldapsearch
+def PrettyShow(DnRecord):
+   Result = "";
+   List = DnRecord[1].keys();
+   List.sort();
+   for x in List:
+      Rec = DnRecord[1][x];
+      for i in Rec:
+         Result = Result + "%s: %s\n" % (x,i);
+   return Result[:-1];
+
+# Function to prompt for a password 
+def getpass(prompt = "Password: "):
+   import termios, TERMIOS, sys;
+   fd = sys.stdin.fileno();
+   old = termios.tcgetattr(fd);
+   new = termios.tcgetattr(fd);
+   new[3] = new[3] & ~TERMIOS.ECHO;          # lflags
+   try:
+      termios.tcsetattr(fd, TERMIOS.TCSADRAIN, new);
+      passwd = raw_input(prompt);
+   finally:
+      termios.tcsetattr(fd, TERMIOS.TCSADRAIN, old);
+   print;
+   return passwd;
+
+# Split up a name into multiple components. This tries to best guess how
+# to split up a name
+def NameSplit(Name):
+   Words = re.split(" ",string.strip(Name));
+
+   # Insert an empty middle name
+   if (len(Words) == 2):
+      Words.insert(1,"");
+   if (len(Words) < 2):
+      Words.append("");
+
+   # Put a dot after any 1 letter words, must be an initial
+   for x in range(0,len(Words)):
+      if len(Words[x]) == 1:
+         Words[x] = Words[x] + '.';
+
+   # If a word starts with a -, ( or [ we assume it marks the start of some
+   # Non-name information and remove the remainder of the string
+   for x in range(0,len(Words)):
+      if len(Words[x]) != 0 and (Words[x][0] == '-' or \
+          Words[x][0] == '(' or Words[x][0] == '['):
+         Words = Words[0:x];
+         break;
+        
+   # Merge any of the middle initials
+   if len(Words) > 2:
+      while len(Words[2]) == 2 and Words[2][1] == '.':
+         Words[1] = Words[1] +  Words[2];
+         del Words[2];
+
+   while len(Words) < 2:
+      Words.append('');
+   
+   # Merge any of the last name prefixes into one big last name
+   while LastNamesPre.has_key(string.lower(Words[-2])):
+      Words[-1] = Words[-2] + " " + Words[-1];
+      del Words[-2];
+
+   # Fix up a missing middle name after lastname globbing
+   if (len(Words) == 2):
+      Words.insert(1,"");
+
+   # If the name is multi-word then we glob them all into the last name and
+   # do not worry about a middle name
+   if (len(Words) > 3):
+      Words[2] = string.join(Words[1:]);
+      Words[1] = "";
+
+   return (string.strip(Words[0]),string.strip(Words[1]),string.strip(Words[2]));
+
+# Compute a random password using /dev/urandom
+def GenPass():   
+   # Generate a 10 character random string
+   SaltVals = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/.";
+   Rand = open("/dev/urandom");
+   Password = "";
+   for i in range(0,10):
+      Password = Password + SaltVals[ord(Rand.read(1)[0]) % len(SaltVals)];
+   return Password;
+
+# Compute the MD5 crypted version of the given password
+def HashPass(Password):
+   # Hash it telling glibc to use the MD5 algorithm - if you dont have
+   # glibc then just change Salt = "$1$" to Salt = "";
+   SaltVals = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/.";
+   Salt  = "$1$";
+   for x in range(0,10):
+      Salt = Salt + SaltVals[whrandom.randint(0,len(SaltVals)-1)];
+   Pass = crypt.crypt(Password,Salt);
+   if len(Pass) < 14:
+      raise "Password Error", "MD5 password hashing failed, not changing the password!";
+   return Pass;
+
+# Sync with the server, we count the number of async requests that are pending
+# and make sure result has been called that number of times
+def FlushOutstanding(l,Outstanding,Fast=0):
+   # Sync with the remote end
+   if Fast == 0:
+      print "Waiting for",Outstanding,"requests:",
+   while (Outstanding > 0):
+      try:
+         if Fast == 0 or Outstanding > 50:
+            sys.stdout.write(".",);
+            sys.stdout.flush();
+            if (l.result(ldap.RES_ANY,1) != (None,None)):
+               Outstanding = Outstanding - 1;
+         else:
+            if (l.result(ldap.RES_ANY,1,0) != (None,None)):
+               Outstanding = Outstanding - 1;
+           else:
+               break;
+      except ldap.TYPE_OR_VALUE_EXISTS:
+         Outstanding = Outstanding - 1;
+      except ldap.NO_SUCH_ATTRIBUTE:
+         Outstanding = Outstanding - 1;
+      except ldap.NO_SUCH_OBJECT:
+         Outstanding = Outstanding - 1;
+   if Fast == 0:
+      print;
+   return Outstanding;
diff --git a/web/Util.pm b/web/Util.pm
new file mode 100644 (file)
index 0000000..4cbec32
--- /dev/null
@@ -0,0 +1,271 @@
+# -*- perl -*-x
+package Util;
+
+use strict;
+use Crypt::Blowfish;
+
+my $blocksize = 8; # A blowfish block is 8 bytes
+my $configfile = "/etc/userdir-ldap/userdir-ldap.conf";
+
+my %config = &ReadConfigFile;
+
+sub CreateKey {
+  my $keysize = shift;
+  my $input;
+  open (F, "</dev/urandom") || die &HTMLError("No /dev/urandom found!");
+  read(F, $input, $keysize); # key length is 8 bytes
+  close F;
+  
+  return $input;
+}
+
+sub CreateCryptSalt {
+  # this can create either a DES type salt or a MD5 salt
+  my $md5 = shift; # do we want a MD5 salt?
+  my $validstr = './0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+  my @valid = split(//,$validstr);
+  my ($in, $out);
+  
+  my $cryptsaltlen = ($md5 ? 8 : 2);
+  
+  open (F, "</dev/urandom") || die &HTMLError("No /dev/urandom found!");
+  foreach (1..$cryptsaltlen) {
+    read(F, $in, 1);
+    $out .= $valid[ord($in) % ($#valid + 1)];
+  }
+  close F;
+  return ($md5 ? "\$1\$$out\$" : $out);
+}
+
+sub Encrypt { 
+  # blowfish only encrypts things in blocks of 8 bytes, so we
+  # need a custom routine that handles longer strings....
+  my $cipher = shift;
+  my $input = shift;
+  my ($pos, $output);
+
+  $input .= " " x ($blocksize - (length($input) % $blocksize)) if (length($input % $blocksize));
+
+  for ($pos = 0; $pos < length($input); $pos += $blocksize) {
+    $output .= unpack("H16", $cipher->encrypt(substr($input, $pos, $blocksize)));
+  }
+  return $output;
+}
+
+sub Decrypt {
+  # like encrypt, needs to deal with big blocks. Note that we assume
+  # trailing spaces are unimportant.
+  my $cipher = shift;
+  my $input = shift;
+  my ($pos, $portion, $output);
+  
+  ((length($input) % $blocksize) == 0) || &HTMLError("Password corrupted"); # should always be true...
+
+  for ($pos = 0; $pos < length($input); $pos += $blocksize*2) {
+    $portion = pack("H16", substr($input, $pos, $blocksize*2));
+    $output .= $cipher->decrypt($portion);
+  }
+    
+  $output =~ s/ +$//;
+  return $output;
+}
+
+sub SavePasswordToFile {
+  my $userid = shift;
+  my $password = shift;
+  my $cipher = shift;
+
+  my $cryptuser = crypt($userid, &CreateCryptSalt);
+  my $secret = Encrypt($cipher, $password);
+  $cryptuser =~ y/\//_/; # translate slashes to underscores...
+  
+  my $fn = "$config{authtokenpath}/$cryptuser";
+  open (F, ">$fn") || &HTMLError("$fn: $!");
+  print F "$secret\n";
+  print F time."\n";
+  close F;
+  chmod 0600, $fn;
+  return $cryptuser;
+}
+
+sub ReadPasswordFromFile {
+  my $userid = shift;
+  my $cipher = shift;
+  my $passwd;
+  my $time;
+  
+  $userid =~ y/\//_/; # translate slashes to underscores...
+
+  # if we couldn't read the password file, assume user is unauthenticated. is this ok?
+  open (F, "<$config{authtokenpath}/$userid") || return undef; 
+  chomp($passwd = <F>);
+  chomp($time = <F>);
+  close F; 
+
+  # check to make sure we read something
+  return undef if (!$passwd || !$time);
+  
+  # check to make sure the time is positive, and that the auth token
+  # has not expired
+  my $tdiff = (time - $time);
+  &HTMLError("Your authentication token has expired. Please <a href=\"$config{webloginhtml}\">relogin</a>") if (($tdiff < 0) || ($tdiff > $config{authexpires}));
+  
+  return Decrypt($cipher, $passwd);
+}
+
+sub CheckAuthToken {
+  my ($id, $hrkey) = split(/:/, shift, 2);
+  return undef if (!$id || !$hrkey);
+  my $key = pack("H".(length($hrkey)), $hrkey);
+  my $cipher = new Crypt::Blowfish $key;
+  my $r = ReadPasswordFromFile($id, $cipher);
+  if ($r) {
+    UpdateAuthToken("$id:$hrkey", $r);
+  } else {    
+    ClearAuthToken("$id:$hrkey")
+  }
+  return $r;
+}
+
+sub ClearAuthToken {
+  my ($id, $hrkey) = split(/:/, shift, 2);
+  $id =~ y/\//_/; # switch / to _
+  unlink "$config{authtokenpath}/$id" || &HTMLError("Error removing authtoken: $!");
+}
+
+sub UpdateAuthToken {
+  my ($id, $hrkey) = split(/:/, shift, 2);
+  my $password = shift;
+  my $key = pack("H".(length($hrkey)), $hrkey);
+  $id =~ y/\//_/; # switch / to _
+  my $cipher = new Crypt::Blowfish $key;
+  my $secret = Encrypt($cipher, $password);
+  
+  my $fn = "$config{authtokenpath}/$id";
+  open (F, ">$fn") || &HTMLError("$fn: $!");
+  print F "$secret\n";
+  print F time."\n";
+  close F;
+  chmod 0600, "$fn" || &HTMLError("$fn: $!");
+  return 1;
+}
+
+sub FormatFingerPrint {
+  my $in = shift;
+  my $out;
+  
+  if (length($in) == 32) {
+    foreach (0..15) {
+      $out .= substr($in, $_*2, 2)." ";
+      $out .= "&nbsp;" if ($_ == 7);
+    }      
+  } else {
+    foreach (0..int(length($in)/2)) {
+      $out .= substr($in, $_*4, 4)." ";
+    }      
+  }
+  return $out;
+}
+
+sub FetchKey {
+  my $fingerprint = shift;
+  my ($out, $keyringparam);
+  
+  foreach (split(/:/, $config{keyrings})) {
+    $keyringparam .= "--keyring $_ ";
+  }
+  
+  $fingerprint =~ s/\s//g;
+  $fingerprint = "0x".$fingerprint;
+
+  $/ = undef; # just suck it up ....
+  open(FP, "$config{gpg} $keyringparam --list-sigs --fingerprint $fingerprint|");
+  $out = <FP>;
+  close FP;
+  open(FP, "$config{gpg} $keyringparam --export -a $fingerprint|");
+  $out .= <FP>;
+  close FP;
+  $/ = "\n";
+  
+  return $out;
+}
+
+sub FormatTimestamp {
+  my $in = shift;
+  $in =~ /^(....)(..)(..)(..)(..)(..)/;
+  
+  return sprintf("%04d/%02d/%02d %02d:%02d:%02d UTC", $1,$2,$3,$4,$5,$6);
+}
+
+sub LookupCountry {
+  my $in = shift;
+  my ($abbrev, $country);
+  open (F, $config{countrylist}) || return uc($in);
+  while (<F>) {
+    chomp;
+    ($abbrev, $country) = split(/\s+/, $_, 2);
+    if ($abbrev eq $in) {
+      close F;
+      return $country;
+    }
+  }
+  close F;
+  return uc($in);
+}
+
+####################
+# Some HTML Routines
+
+my $htmlhdrsent = 0;
+
+sub HTMLSendHeader {
+  print "Content-type: text/html\n\n" if (!$htmlhdrsent);
+  $htmlhdrsent = 1;
+}
+
+sub HTMLPrint {
+  &HTMLSendHeader if (!$htmlhdrsent);
+  print shift;
+}
+
+sub HTMLError {
+  HTMLPrint(shift);
+  die "\n";
+}
+
+sub CheckLatLong {
+  my ($lat, $long) = @_;
+
+  $lat =~ s/[^-+\.\d]//g; $long =~ s/[^-+\.\d]//g;
+  
+  if (($lat =~ /^(\-|\+?)\d+(\.\d+)?/) && ($long =~ /^(\-|\+?)\d+(\.\d+)?/)) {
+    return ($lat, $long);
+  } else {
+    return ("", "");
+  }
+}
+
+###################
+# Config file stuff
+sub ReadConfigFile {
+  # reads a config file and results a hashref with the results
+  my (%config, $attr, $setting);
+  open (F, "<$configfile") || &HTMLError("Cannot open $configfile: $!");
+  while (<F>) {
+    chomp;
+    if ((!/^\s*#/) && ($_ ne "")) {
+      # Chop off any trailing comments
+      s/#.*//;
+      ($attr, $setting) = split(/=/, $_, 2);
+      $setting =~ s/"//g;
+      $setting =~ s/;$//;
+      $attr =~ s/^ +//; $attr =~ s/ +$//;
+      $setting =~ s/^ +//; $setting =~ s/ +$//;      
+      $config{$attr} = $setting;
+    }
+  }
+  close F;
+  return %config;
+}
+
+1;
diff --git a/web/domains.tab b/web/domains.tab
new file mode 100644 (file)
index 0000000..fd7d1d7
--- /dev/null
@@ -0,0 +1,255 @@
+ad   Andorra
+ae   United Arab Emirates
+af   Afghanistan
+ag   Antigua and Barbuda
+ai   Anguilla
+al   Albania
+am   Armenia
+an   Netherlands Antilles
+ao   Angola
+aq   Antarctica
+ar   Argentina
+arpa   Old style Arpanet
+as   American Samoa
+at   Austria
+au   Australia
+aw   Aruba
+az   Azerbaidjan
+ba   Bosnia-Herzegovina
+bb   Barbados
+bd   Bangladesh
+be   Belgium
+bf   Burkina Faso
+bg   Bulgaria
+bh   Bahrain
+bi   Burundi
+bj   Benin
+bm   Bermuda
+bn   Brunei Darussalam
+bo   Bolivia
+br   Brazil
+bs   Bahamas
+bt   Bhutan
+bv   Bouvet Island
+bw   Botswana
+by   Belarus
+bz   Belize
+ca   Canada
+cc   Cocos (Keeling) Islands
+cd   Democratic Republic of Congo
+cf   Central African Republic
+cg   Congo
+ch   Switzerland
+ci   Ivory Coast (Cote D'Ivoire)
+ck   Cook Islands
+cl   Chile
+cm   Cameroon
+cn   China
+co   Colombia
+com   Commercial
+cr   Costa Rica
+cs   Czech Republic and Slovakia
+cu   Cuba
+cv   Cape Verde
+cx   Christmas Island
+cy   Cyprus
+cz   Czech Republic
+de   Germany
+dj   Djibouti
+dk   Denmark
+dm   Dominica
+do   Dominican Republic
+dz   Algeria
+ec   Ecuador
+edu   USA Educational
+ee   Estonia
+eg   Egypt
+eh   Western Sahara
+er   Eritrea
+es   Spain
+et   Ethiopia
+fi   Finland
+fj   Fiji
+fk   Falkland Islands
+fm   Micronesia
+fo   Faroe Islands
+fr   France
+fx   France (European Territory)
+ga   Gabon
+gb   Great Britain
+gd   Grenada
+ge   Georgia
+gf   French Guyana
+gg   Guernsey
+gh   Ghana
+gi   Gibraltar
+gl   Greenland
+gm   Gambia
+gn   Guinea
+gov   USA Government
+gp   Guadeloupe (French)
+gq   Equatorial Guinea
+gr   Greece
+gs   S. Georgia & S. Sandwich Isls.
+gt   Guatemala
+gu   Guam (USA)
+gw   Guinea Bissau
+gy   Guyana
+hk   Hong Kong
+hm   Heard and McDonald Islands
+hn   Honduras
+hr   Croatia
+ht   Haiti
+hu   Hungary
+id   Indonesia
+ie   Ireland
+il   Israel
+im   Isle of Man
+in   India
+int   International
+io   British Indian Ocean Territory
+iq   Iraq
+ir   Iran
+is   Iceland
+it   Italy
+je   Jersey
+jm   Jamaica
+jo   Jordan
+jp   Japan
+ke   Kenya
+kg   Kyrgyzstan
+kh   Cambodia
+ki   Kiribati
+km   Comoros
+kn   Saint Kitts & Nevis Anguilla
+kp   North Korea
+kr   South Korea
+kw   Kuwait
+ky   Cayman Islands
+kz   Kazakhstan
+la   Laos
+lb   Lebanon
+lc   Saint Lucia
+li   Liechtenstein
+lk   Sri Lanka
+lr   Liberia
+ls   Lesotho
+lt   Lithuania
+lu   Luxembourg
+lv   Latvia
+ly   Libya
+ma   Morocco
+mc   Monaco
+md   Moldavia
+mg   Madagascar
+mh   Marshall Islands
+mil   USA Military
+mk   Macedonia
+ml   Mali
+mm   Myanmar
+mn   Mongolia
+mo   Macau
+mp   Northern Mariana Islands
+mq   Martinique (French)
+mr   Mauritania
+ms   Montserrat
+mt   Malta
+mu   Mauritius
+mv   Maldives
+mw   Malawi
+mx   Mexico
+my   Malaysia
+mz   Mozambique
+na   Namibia
+nato   NATO
+nc   New Caledonia (French)
+ne   Niger
+net   Network
+nf   Norfolk Island
+ng   Nigeria
+ni   Nicaragua
+nl   Netherlands
+no   Norway
+np   Nepal
+nr   Nauru
+nt   Neutral Zone
+nu   Niue
+nz   New Zealand
+om   Oman
+org   Non-Profit Making Organisations
+pa   Panama
+pe   Peru
+pf   Polynesia (French)
+pg   Papua New Guinea
+ph   Philippines
+pk   Pakistan
+pl   Poland
+pm   Saint Pierre and Miquelon
+pn   Pitcairn Island
+pr   Puerto Rico
+pt   Portugal
+pw   Palau
+py   Paraguay
+qa   Qatar
+re   Reunion (French)
+ro   Romania
+ru   Russia
+rw   Rwanda
+sa   Saudi Arabia
+sb   Solomon Islands
+sc   Seychelles
+sd   Sudan
+se   Sweden
+sg   Singapore
+sh   Saint Helena
+si   Slovenia
+sj   Svalbard and Jan Mayen Islands
+sk   Slovak Republic
+sl   Sierra Leone
+sm   San Marino
+sn   Senegal
+so   Somalia
+sr   Suriname
+st   Saint Tome and Principe
+su   Former USSR
+sv   El Salvador
+sy   Syria
+sz   Swaziland
+tc   Turks and Caicos Islands
+td   Chad
+tf   French Southern Territories
+tg   Togo
+th   Thailand
+tj   Tadjikistan
+tk   Tokelau
+tm   Turkmenistan
+tn   Tunisia
+to   Tonga
+tp   East Timor
+tr   Turkey
+tt   Trinidad and Tobago
+tv   Tuvalu
+tw   Taiwan
+tz   Tanzania
+ua   Ukraine
+ug   Uganda
+uk   United Kingdom
+um   USA Minor Outlying Islands
+us   United States
+uy   Uruguay
+uz   Uzbekistan
+va   Vatican City State
+vc   Saint Vincent & Grenadines
+ve   Venezuela
+vg   Virgin Islands (British)
+vi   Virgin Islands (USA)
+vn   Vietnam
+vu   Vanuatu
+wf   Wallis and Futuna Islands
+ws   Samoa
+ye   Yemen
+yt   Mayotte
+yu   Yugoslavia
+za   South Africa
+zm   Zambia
+zw   Zimbabwe
diff --git a/web/fetchkey.cgi b/web/fetchkey.cgi
new file mode 100755 (executable)
index 0000000..8e88502
--- /dev/null
@@ -0,0 +1,27 @@
+#!/usr/bin/perl
+
+use strict;
+use CGI;
+use Util;
+
+# Global settings...
+my %config = &Util::ReadConfigFile;
+
+my $query = new CGI;
+print "Content-type: text/plain\n\n";
+
+my $fp = $query->param('fingerprint');
+
+if ($fp) {
+  my $key = &Util::FetchKey($fp);
+  if ($key) {
+    print $key;
+  } else {
+    print "Sorry, no key found matching fingerprint $fp\n";
+  }
+} else {
+  print "No fingerprint given\n";
+}
+
+exit 0;
+
diff --git a/web/login.cgi b/web/login.cgi
new file mode 100755 (executable)
index 0000000..3514953
--- /dev/null
@@ -0,0 +1,52 @@
+#!/usr/bin/perl
+
+# (c) 1999 Debian and Randolph Chung. Licensed under the GPL. <tausq@debian.org>
+
+use lib '.';
+use strict;
+#use Apache::Registry;
+use CGI;
+use Util;
+use URI::Escape;
+use Crypt::Blowfish;
+use Net::LDAP qw(:all);
+
+my %config = &Util::ReadConfigFile;
+
+my $query = new CGI;
+my $proto = ($ENV{HTTPS} ? "https" : "http");
+
+if (!($query->param('username')) || !($query->param('password'))) {
+  print "Location: $proto://$ENV{SERVER_NAME}/$config{webloginurl}\n\n";
+  exit;
+}
+
+my $key = &Util::CreateKey($config{blowfishkeylen}); # human-readable version of the key
+my $hrkey = unpack("H".($config{blowfishkeylen}*2), $key);
+my $cipher = new Crypt::Blowfish $key;
+
+my $ldap = Net::LDAP->new($config{ldaphost}) || &Util::HTMLError($!);
+
+my $username = $query->param('username');
+my $password = $query->param('password');
+my $binddn = "uid=$username,$config{basedn}";
+
+my $mesg = $ldap->bind($binddn, password => $password);
+$mesg->sync;
+
+if ($mesg->code == LDAP_SUCCESS) {
+  my $cryptid = &Util::SavePasswordToFile($username, $password, $cipher);
+
+  if ($query->param('update')) {
+    my $url = "$proto://$ENV{SERVER_NAME}/$config{webupdateurl}?id=$username&authtoken=$cryptid:$hrkey&editdn=";
+    $url .= uri_escape("uid=$username,$config{basedn}", "\x00-\x40\x7f-\xff");
+    print "Location: $url\n\n";
+  } else {
+    print "Location: $proto://$ENV{SERVER_NAME}/$config{websearchurl}?id=$username&authtoken=$cryptid:$hrkey\n\n";
+  }
+
+  $ldap->unbind;
+} else {
+  print "Content-type: text/html\n\n";
+  print "<html><body><h1>Not authenticated</h1></body></html>\n";
+}
diff --git a/web/login.html b/web/login.html
new file mode 100644 (file)
index 0000000..300dcd5
--- /dev/null
@@ -0,0 +1,16 @@
+<html><head><title>debian.org Developers LDAP Search</title>
+<body bgcolor=#9999ff link=red alink=red vlink=red text=black>
+<br><br><center>
+<form method=post action="login.cgi">
+<table border=5 cellspacing=0 cellpadding=3 width="70%" bgcolor=#DDDDDD> 
+<tr><th colspan=2 bgcolor="#44CCCC" align=center><font size="+2">Authentication Required</font></th></tr>
+<tr><td colspan=2 align=center><br><br>If you are not a developer, please <a href="search.cgi">return to the main search page</a>, otherwise, enter your Debian user ID and password below<br><br></td></tr>
+<tr><td align=right><b>login:</b></td><td><input size=10 name=username>@debian.org</td></tr>
+<tr><td align=right><b>Password:</b></td><td><input size=20 name=password type=password></td></tr>
+<tr><td colspan=2 align="center">
+<input type=submit name=search value="Full Search">&nbsp;&nbsp;&nbsp;
+<input type=submit name=update value="Update my info">
+</td></tr></table>
+</center>
+</form>
+</body></html>
diff --git a/web/logout.cgi b/web/logout.cgi
new file mode 100755 (executable)
index 0000000..11167b4
--- /dev/null
@@ -0,0 +1,23 @@
+#!/usr/bin/perl
+
+# (c) 1999 Debian and Randolph Chung. Licensed under the GPL. <tausq@debian.org>
+
+use lib '.';
+use strict vars;
+#use Apache::Registry;
+use CGI;
+use Util;
+use Net::LDAP qw(:all);
+
+my %config = &Util::ReadConfigFile;
+my $proto = ($ENV{HTTPS} ? "https" : "http");
+
+my $query = new CGI;
+my $id = $query->param('id');
+my $authtoken = $query->param('authtoken');
+&Util::ClearAuthToken($authtoken);
+my $doneurl = $query->param('done') || "$config{websearchurl}";
+
+&Util::ClearAuthToken($authtoken);
+
+print "Location: $proto://$ENV{SERVER_NAME}/$doneurl\n\n";
diff --git a/web/search.cgi b/web/search.cgi
new file mode 100755 (executable)
index 0000000..3c21fb7
--- /dev/null
@@ -0,0 +1,255 @@
+#!/usr/bin/perl
+
+# (c) 1999 Debian and Randolph Chung. Licensed under the GPL. <tausq@debian.org>
+
+use lib '.';
+use strict vars;
+#use Apache::Registry;
+use CGI;
+use Util;
+use URI::Escape;
+use Net::LDAP qw(:all);
+
+# Global settings...
+my %config = &Util::ReadConfigFile;
+
+my $query = new CGI;
+my $id = $query->param('id');
+my $authtoken = $query->param('authtoken');
+my $password = &Util::CheckAuthToken($authtoken);
+my $dosearch = $query->param('dosearch');
+my $searchdn = $query->param('searchdn');
+my $ldap = undef;
+
+my $proto = ($ENV{HTTPS} ? "https" : "http");
+
+sub DieHandler {
+  $ldap->unbind if (defined($ldap));
+}
+
+$SIG{__DIE__} = \&DieHandler;
+
+if (!$dosearch) {
+  # No action yet, send back the search form...
+  print "Content-type: text/html\n\n";
+  open (F, "<$config{websearchhtml}") || &Util::HTMLError($!);
+  while (<F>) {
+    s/~id~/$id/g;
+    s/~authtoken~/$authtoken/g;    
+    print;
+  }
+  close F;
+} else {
+  # Go ahead and construct the search terms
+  my %searchdata = (
+    cn             => { fuzzy => 'cnfuzzy', formname => 'cn' }, # First name
+    mn             => { fuzzy => 'mnfuzzy', formname => 'mn' }, # Middle name
+    sn             => { fuzzy => 'snfuzzy', formname => 'sn' }, # Last name
+    email          => { fuzzy => 'emailfuzzy', formname => 'email' }, # email
+    uid            => { fuzzy => 'uidfuzzy', formname => 'uid' }, # Login name
+    ircnick        => { fuzzy => 'ircfuzzy', formname => 'ircnick' }, # IRC nickname
+    keyfingerprint => { fuzzy => 'fpfuzzy', formname => 'fingerprint' }, # PGP/GPG fingerprint
+    c              => { formname => 'country'}, # Country
+  );
+
+  # Do a little preprocessing - strip the spaces out of the fingerprint
+  my $temp = $query->param('fingerprint');
+  $temp =~ s/ //g; $query->param('fingerprint', $temp);
+
+  # go through %searchdata and pull out all the search criteria the user
+  # specified...
+  my $filter = undef;
+  foreach (keys(%searchdata)) {
+    if ($query->param($searchdata{$_}{formname})) {    
+      if ($query->param($searchdata{$_}{fuzzy})) {
+        # fuzzy search
+       $filter .= "($_~=".$query->param($searchdata{$_}{formname}).")";
+      } else {
+       $filter .= "($_=".$query->param($searchdata{$_}{formname}).")";
+      }
+    }
+  }
+  
+  # Vacation is a special case
+  $filter .= "(onvacation=*)" if ($query->param('vacation'));
+
+  # AND all the search terms together
+  $filter = "(&$filter)";
+  
+  # Read in the result template...
+  my ($lineref, $dataspecref) = ParseResult($config{websearchresulthtml});
+
+  # Now, we are ready to connect to the LDAP server.
+  $ldap = Net::LDAP->new($config{ldaphost}) || &Util::HTMLError($!);
+  my $auth = 0;
+  my $mesg;
+
+  if ($id && $password) {
+    $mesg = $ldap->bind("uid=$id,$config{basedn}", password => $password);
+    $mesg->sync;
+    $auth = ($mesg->code == LDAP_SUCCESS);
+  }
+
+  if (!$auth) { # Not authenticated - either the above failed, or no password supplied
+    $ldap->bind;
+  }
+
+#  &Util::HTMLPrint("Searching in $config{basedn} for $filter...\n");
+  
+  $mesg = $ldap->search(base   => ($searchdn ? $searchdn : $config{basedn}),
+                        filter => ($searchdn ? "(uid=*)" : $filter));
+  $mesg->code && &Util::HTMLError($mesg->error);
+
+  my %outsub; # this hash will contain all the substitution tokens in the output
+  $outsub{count} = $mesg->count; # Count number of requests, also ensures we're done with the search
+  $outsub{auth} = $authtoken;
+  $outsub{authtoken} = $authtoken; # alias
+  $outsub{id} = $id;
+  $outsub{searchresults} = undef;
+  
+  my $entries = $mesg->as_struct; # entries contain a hashref to all the search results
+  my ($dn, $attr, $data); 
+
+  # Format the output....
+  foreach $dn (sort {$entries->{$a}->{sn}->[0] <=> $entries->{$b}->{sn}->[0]} keys(%$entries)) {
+    $data = $entries->{$dn};
+
+    # These are local variables.. i have enough global vars as it is... <sigh>
+    my ($ufdn, $login, $name, $email, $fingerprint, $address, $latlong, $vacation, $created, $modified) = undef;
+    
+    $ufdn = $dn; # Net::LDAP does not have a dn2ufn function, but this is close enough :)
+    
+    # Assemble name, attach web page link if present.
+    $name = $data->{cn}->[0]." ".$data->{mn}->[0]." ".$data->{sn}->[0];
+    if (my $url = $data->{labeledurl}->[0]) {
+      $name = "<a href=\"$url\">$name</a>";
+    }
+    
+    # Add links to all email addresses
+    foreach (@{$data->{emailforward}}) {
+      $email .= "<br>" if ($email);
+      $email .= "<a href=\"mailto:$_\">$_</a>";
+    }
+
+    # Format PGP/GPG key fingerprints
+    my $fi;
+    foreach (@{$data->{keyfingerprint}}) {
+      $fingerprint .= "<br>" if ($fingerprint);
+      $fingerprint .= sprintf("%d:- <a href=\"fetchkey.cgi?fingerprint=%s\">%s</a>", ++$fi, $_, &Util::FormatFingerPrint($_));
+    }
+    
+    # Assemble addresses
+    $address = $data->{postaladdress}->[0] || "- unlisted -";
+    $address =~ s/\$/<br>/g;
+    $address .= "<br>".$data->{l}->[0]."<br>".&Util::LookupCountry($data->{c}->[0])."<br>".$data->{postalcode}->[0];
+
+    # Assemble latitude/longitude
+    $latlong  = $data->{latitude}->[0] || "none";
+    $latlong .= " / ";
+    $latlong .= $data->{longitude}->[0] || "none";    
+
+    # Modified/created time. TODO: maybe add is the name of the creator/modifier
+    $modified = &Util::FormatTimestamp($data->{modifytimestamp}->[0]);
+    $created =  &Util::FormatTimestamp($data->{createtimestamp}->[0]);
+
+    # Link in the debian login id 
+    $login = $data->{uid}->[0]."\@debian.org";
+    $login = "<a href=\"mailto:$login\">$login</a>";
+    
+    # See if the user has a vacation message
+    $vacation = $data->{onvacation}->[0];
+
+    # OK, now generate output... (i.e. put the output into the buffer )
+    $outsub{searchresults} .= '<table border=2 cellpadding=2 cellspacing=0 bgcolor="#DDDDDD" width="80%">';
+    $outsub{searchresults} .= '<tr><th bgcolor="#44CCCC" colspan=2><font size=+1>'."$name</font> ";
+    $outsub{searchresults} .= "($ufdn)</th></tr>\n";
+    
+    if ($vacation) {
+      $outsub{searchresults} .= "<tr><td colspan=2 align=center><b>$vacation</b></td></tr>\n";
+    }
+
+    $outsub{searchresults} .= FormatEntry($dataspecref->{uid}, $login);
+    $outsub{searchresults} .= FormatEntry($dataspecref->{ircnick}, $data->{ircnick}->[0]);
+    $outsub{searchresults} .= FormatEntry($dataspecref->{loginshell}, $data->{loginshell}->[0]);
+    $outsub{searchresults} .= FormatEntry($dataspecref->{fingerprint}, $fingerprint);
+    
+    if ($auth) {
+      # Some data should only be available to authorized users...
+      if ($id eq $data->{uid}->[0]) {
+        $outsub{searchresults} .= FormatEntry($dataspecref->{email}, $email);
+      }
+      $outsub{searchresults} .= FormatEntry($dataspecref->{address}, $address);
+      $outsub{searchresults} .= FormatEntry($dataspecref->{latlong}, $latlong);
+      $outsub{searchresults} .= FormatEntry($dataspecref->{phone}, $data->{telephonenumber}->[0] || "- unlisted -");
+      $outsub{searchresults} .= FormatEntry($dataspecref->{fax}, $data->{fascimiletelephonenumber}->[0] || "- unlisted -");
+    }
+    $outsub{searchresults} .= FormatEntry($dataspecref->{created}, $created);
+    $outsub{searchresults} .= FormatEntry($dataspecref->{modified}, $modified);
+    
+    $outsub{searchresults} .= "</table>";
+    
+    # If this is ourselves, present a link to do mods
+    if ($auth && ($id eq $data->{uid}->[0])) { #TODO: extract this string into a url for translation...
+     $outsub{searchresults} .= "<a href=\"$proto://$ENV{SERVER_NAME}/$config{webupdateurl}?id=$id&authtoken=$authtoken&editdn=".uri_escape($dn, "\x00-\x40\x7f-\xff")."\">Edit my settings</a>\n";
+    }
+    
+    $outsub{searchresults} .= "<br><br><br>\n";
+  }
+  
+  # Finally, we can write the output... yuck...
+  &Util::HTMLSendHeader;
+  foreach (@$lineref) {
+    if (/<\?ifauth(.+?)\?>/) {
+      $_ = ($auth ? $1 : "");
+    } elsif (/<\?ifnoauth(.+?)\?>/) {
+      $_ = ($auth ? "" : $1);
+    }
+    s/~(.+?)~/$outsub{$1}/g;
+    print;
+  }
+
+  $ldap->unbind;
+}
+
+sub ParseResult {
+  # Reads the output html file and find out how the output should be named
+  # -- this gives us a way to do translations more easily
+  # Returns the contents of the template (w/o the searchresult portion) and
+  # the output specification
+  my $fn = shift;
+  my $insec = 0;
+  my @lines;
+  my %hash;
+  
+  open (F, "<$fn") || &Util::HTMLError("$fn: $!");
+  while (<F>) {
+    if (!$insec) {
+      if (/<\?searchresults/i) {
+        $insec = 1;
+       push(@lines, "~searchresults~\n"); # Leave token so we know where to put the result
+      } else {
+        push(@lines, $_);
+      }
+    } else {
+      if (/searchresults\?>/i) {
+        $insec = 0;
+      } else {
+        if (!/^\s*#/) {
+         s/^ *\(//; 
+         s/\) *$//; # remove leading/trailing () and spaces
+         chomp;
+         my ($desc, $attr) = split(/, /, $_, 2);
+         $hash{$attr} = $desc;
+        }
+      }
+    }
+  }
+  close F;
+  return (\@lines, \%hash);
+}
+
+sub FormatEntry {
+  my ($key, $val) = @_;
+  
+  return "<tr><td align=right><b>$key:</b></td><td>&nbsp;$val</td></tr>\n";
+}
diff --git a/web/searchform.html b/web/searchform.html
new file mode 100644 (file)
index 0000000..f9d57ec
--- /dev/null
@@ -0,0 +1,277 @@
+<html><head><title>debian.org Developers LDAP Search</title>
+<body bgcolor=#9999ff link=red alink=red vlink=red text=black>
+<br><br><center>
+<form method=post action="search.cgi">
+<input type=hidden name=id value="~id~" >
+<input type=hidden name=authtoken value="~authtoken~">
+<table border=5 cellspacing=0 cellpadding=3 width="70%" bgcolor=#DDDDDD> 
+<tr><th colspan=2 bgcolor="#44CCCC" align=center><font size="+2">Debian Developers Database Search</font>
+<br><font size="-1">(any field can be left blank....)</font><br>
+<a href="searchhelp.html">Help on searching</a></th></tr>
+<tr><td align=right><b>First name:</b></td><td><input name=cn>
+<input name=cnfuzzy value=1 type=checkbox>Fuzzy search</td></tr>
+<tr><td align=right><b>Last name:</b></td><td><input name=sn>
+<input name=snfuzzy value=1 type=checkbox>Fuzzy search</td></tr>
+<tr><td align=right><b>login:</b></td><td><input name=uid>
+<input name=uidfuzzy value=1 type=checkbox>Fuzzy search</td></tr>
+<tr><td align=right><b>IRC nickname:</b></td><td><input name=ircnick>
+<input name=ircfuzzy value=1 type=checkbox>Fuzzy search</td></tr>
+<tr><td align=right><b>PGP/GPG fingerprint:</b></td><td><input name=fingerprint>
+<input name=fpfuzzy value=1 type=checkbox>Fuzzy search</td></tr>
+<tr><td align=right><b>country:</b></td><td>
+<select name=country size=5>
+<option value="">(any)
+<option value=af>Afghanistan
+<option value=al>Albania
+<option value=dz>Algeria
+<option value=as>American Samoa
+<option value=ad>Andorra
+<option value=ao>Angola
+<option value=ai>Anguilla
+<option value=aq>Antarctica
+<option value=ag>Antigua and Barbuda
+<option value=ar>Argentina
+<option value=am>Armenia
+<option value=aw>Aruba
+<option value=au>Australia
+<option value=at>Austria
+<option value=az>Azerbaidjan
+<option value=bs>Bahamas
+<option value=bh>Bahrain
+<option value=bd>Bangladesh
+<option value=bb>Barbados
+<option value=by>Belarus
+<option value=be>Belgium
+<option value=bz>Belize
+<option value=bj>Benin
+<option value=bm>Bermuda
+<option value=bt>Bhutan
+<option value=bo>Bolivia
+<option value=ba>Bosnia-Herzegovina
+<option value=bw>Botswana
+<option value=bv>Bouvet Island
+<option value=br>Brazil
+<option value=io>British Indian Ocean Territory
+<option value=bn>Brunei Darussalam
+<option value=bg>Bulgaria
+<option value=bf>Burkina Faso
+<option value=bi>Burundi
+<option value=kh>Cambodia
+<option value=cm>Cameroon
+<option value=ca>Canada
+<option value=cv>Cape Verde
+<option value=ky>Cayman Islands
+<option value=cf>Central African Republic
+<option value=td>Chad
+<option value=cl>Chile
+<option value=cn>China
+<option value=cx>Christmas Island
+<option value=cc>Cocos (Keeling) Islands
+<option value=co>Colombia
+<option value=km>Comoros
+<option value=cg>Congo
+<option value=ck>Cook Islands
+<option value=cr>Costa Rica
+<option value=hr>Croatia
+<option value=cu>Cuba
+<option value=cy>Cyprus
+<option value=cz>Czech Republic
+<option value=cs>Czech Republic and Slovakia
+<option value=cd>Democratic Republic of the Congo
+<option value=zr>Democratic Republic of the Congo
+<option value=dk>Denmark
+<option value=dj>Djibouti
+<option value=dm>Dominica
+<option value=do>Dominican Republic
+<option value=tp>East Timor
+<option value=ec>Ecuador
+<option value=eg>Egypt
+<option value=sv>El Salvador
+<option value=gq>Equatorial Guinea
+<option value=er>Eritrea
+<option value=ee>Estonia
+<option value=et>Ethiopia
+<option value=fk>Falkland Islands
+<option value=fo>Faroe Islands
+<option value=fj>Fiji
+<option value=fi>Finland
+<option value=su>Former USSR
+<option value=fr>France
+<option value=fx>France (European Territory)
+<option value=gf>French Guyana
+<option value=tf>French Southern Territories
+<option value=ga>Gabon
+<option value=gm>Gambia
+<option value=ge>Georgia
+<option value=de>Germany
+<option value=gh>Ghana
+<option value=gi>Gibraltar
+<option value=gr>Greece
+<option value=gl>Greenland
+<option value=gd>Grenada
+<option value=gp>Guadeloupe (French)
+<option value=gu>Guam (USA)
+<option value=gt>Guatemala
+<option value=gg>Guernsey
+<option value=gn>Guinea
+<option value=gw>Guinea Bissau
+<option value=gy>Guyana
+<option value=ht>Haiti
+<option value=hm>Heard and McDonald Islands
+<option value=hn>Honduras
+<option value=hk>Hong Kong
+<option value=hu>Hungary
+<option value=is>Iceland
+<option value=in>India
+<option value=id>Indonesia
+<option value=ir>Iran
+<option value=iq>Iraq
+<option value=ie>Ireland
+<option value=im>Isle of Man
+<option value=il>Israel
+<option value=it>Italy
+<option value=ci>Ivory Coast (Cote d'Ivoire)
+<option value=jm>Jamaica
+<option value=jp>Japan
+<option value=je>Jersey
+<option value=jo>Jordan
+<option value=kz>Kazakhstan
+<option value=ke>Kenya
+<option value=ki>Kiribati
+<option value=kw>Kuwait
+<option value=kg>Kyrgyzstan
+<option value=la>Laos
+<option value=lv>Latvia
+<option value=lb>Lebanon
+<option value=ls>Lesotho
+<option value=lr>Liberia
+<option value=ly>Libya
+<option value=li>Liechtenstein
+<option value=lt>Lithuania
+<option value=lu>Luxembourg
+<option value=mo>Macau
+<option value=mk>Macedonia
+<option value=mg>Madagascar
+<option value=mw>Malawi
+<option value=my>Malaysia
+<option value=mv>Maldives
+<option value=ml>Mali
+<option value=mt>Malta
+<option value=mh>Marshall Islands
+<option value=mq>Martinique (French)
+<option value=mr>Mauritania
+<option value=mu>Mauritius
+<option value=yt>Mayotte
+<option value=mx>Mexico
+<option value=fm>Micronesia
+<option value=md>Moldavia
+<option value=mc>Monaco
+<option value=mn>Mongolia
+<option value=ms>Montserrat
+<option value=ma>Morocco
+<option value=mz>Mozambique
+<option value=mm>Myanmar
+<option value=na>Namibia
+<option value=nr>Nauru
+<option value=np>Nepal
+<option value=nl>Netherlands
+<option value=an>Netherlands Antilles
+<option value=nc>New Caledonia (French)
+<option value=nz>New Zealand
+<option value=ni>Nicaragua
+<option value=ne>Niger
+<option value=ng>Nigeria
+<option value=nu>Niue
+<option value=nf>Norfolk Island
+<option value=kp>North Korea
+<option value=mp>Northern Mariana Islands
+<option value=no>Norway
+<option value=om>Oman
+<option value=pk>Pakistan
+<option value=pw>Palau
+<option value=pa>Panama
+<option value=pg>Papua New Guinea
+<option value=py>Paraguay
+<option value=pe>Peru
+<option value=ph>Philippines
+<option value=pn>Pitcairn Island
+<option value=pl>Poland
+<option value=pf>Polynesia (French)
+<option value=pt>Portugal
+<option value=pr>Puerto Rico
+<option value=qa>Qatar
+<option value=re>Reunion (French)
+<option value=ro>Romania
+<option value=ru>Russia
+<option value=rw>Rwanda
+<option value=gs>S. Georgia & S. Sandwich Isls.
+<option value=sh>Saint Helena
+<option value=kn>Saint Kitts & Nevis
+<option value=lc>Saint Lucia
+<option value=pm>Saint Pierre and Miquelon
+<option value=st>Saint Tome and Principe
+<option value=vc>Saint Vincent & Grenadines
+<option value=ws>Samoa
+<option value=sm>San Marino
+<option value=sa>Saudi Arabia
+<option value=sn>Senegal
+<option value=sc>Seychelles
+<option value=sl>Sierra Leone
+<option value=sg>Singapore
+<option value=sk>Slovak Republic
+<option value=si>Slovenia
+<option value=sb>Solomon Islands
+<option value=so>Somalia
+<option value=za>South Africa
+<option value=kr>South Korea
+<option value=es>Spain
+<option value=lk>Sri Lanka
+<option value=sd>Sudan
+<option value=sr>Suriname
+<option value=sj>Svalbard and Jan Mayen Islands
+<option value=sz>Swaziland
+<option value=se>Sweden
+<option value=ch>Switzerland
+<option value=sy>Syria
+<option value=tj>Tadjikistan
+<option value=tw>Taiwan
+<option value=tz>Tanzania
+<option value=th>Thailand
+<option value=tg>Togo
+<option value=tk>Tokelau
+<option value=to>Tonga
+<option value=tt>Trinidad and Tobago
+<option value=tn>Tunisia
+<option value=tr>Turkey
+<option value=tm>Turkmenistan
+<option value=tc>Turks and Caicos Islands
+<option value=tv>Tuvalu
+<option value=um>USA Minor Outlying Islands
+<option value=ug>Uganda
+<option value=ua>Ukraine
+<option value=ae>United Arab Emirates
+<option value=uk>United Kingdom
+<option value=us>United States
+<option value=uy>Uruguay
+<option value=uz>Uzbekistan
+<option value=vu>Vanuatu
+<option value=va>Vatican City State
+<option value=ve>Venezuela
+<option value=vn>Vietnam
+<option value=vg>Virgin Islands (British)
+<option value=vi>Virgin Islands (USA)
+<option value=wf>Wallis and Futuna Islands
+<option value=eh>Western Sahara
+<option value=ye>Yemen
+<option value=yu>Yugoslavia
+<option value=zm>Zambia
+<option value=zw>Zimbabwe
+</select>
+</td></tr>
+<tr><td colspan=2 align=center><input type=checkbox value=1 name=vacation> On vacation</td></tr>
+<tr><td colspan=2 align="center">
+<input type=submit name=dosearch value="Search...">
+</td></tr></table>
+<a href="login.html">Login</a> (registered developers only)
+</center>
+</form>
diff --git a/web/searchhelp.html b/web/searchhelp.html
new file mode 100644 (file)
index 0000000..1167078
--- /dev/null
@@ -0,0 +1,18 @@
+<html><head><title>debian.org Developers Online Database</title>
+<body bgcolor=#9999ff link=red alink=red vlink=red text=black>
+  <center>
+  <table border=5 cellspacing=0 cellpadding=10 width="70%" bgcolor=#DDDDDD> 
+  <tr><td align="left"><br>
+To look up information about Debian developers, enter your search criteria
+in the form. Results are returned which match all of the search criteria.
+Wildcards may be used. For example, entering <code>*de*</code> in the last name
+field will return all developers whose surname contains the substring
+<code>de</code>. Matches are case-insensitive, and all searching criteria that
+are left empty will be ignored. Selecting the "fuzzy search" option will turn 
+on approximate searching.<p>
+
+The "On vacation" field will return all developers who have left a vacation
+message.<p>
+
+</br></td></tr></table></center>
+</body></html>
diff --git a/web/searchresults.html b/web/searchresults.html
new file mode 100644 (file)
index 0000000..daec13d
--- /dev/null
@@ -0,0 +1,37 @@
+<!-- note: comments in this page may be significant. Do not remove -->
+<!-- ~blah~ will be replaced by the contents of the blah variable when this html page is returned -->
+<html><head><title>Search results</title></head>
+<body bgcolor=#9999ff link=red alink=red vlink=red>
+<p align=center>
+
+<?ifauth <a href="logout.cgi">Logout</a> | ?>
+<?ifnoauth <a href="login.html">Login</a> (developers only) | ?>
+<a href="search.cgi?id=~id~&authtoken=~authtoken~">Search again</a></p><br><br>
+<p align=center>Number of entries matched: <b>~count~</b></p>
+<center>
+
+<!-- search results - this will be filled in by the script -->
+<!-- specify pairs of descriptive names, followed by the attribute, one per line  -->
+<!-- Note that the attributes specified here do not determine the order of the output, nor which attributes get returned. that is defined in the script -->
+<?searchresults
+(login, uid)
+(irc Nickname, ircnick)
+(Preferred shell, loginshell)
+(e-mail forwarded to, email)
+(PGP/GPG fingerprint, fingerprint)
+(Record created, created)
+(Last modified, modified)
+(Address, address)
+(Lat/Long, latlong)
+(Phone, phone)
+(FAX, fax)
+searchresults?>
+
+</center>
+<br><br>
+<p align=center>
+<?ifauth <a href="logout.cgi">Logout</a> | ?>
+<?ifnoauth <a href="login.html">Login</a> (developers only) | ?>
+<a href="search.cgi?id=~id~&authtoken=~authtoken~">Search again</a></p><br><br>
+</p>
+</body></html>
diff --git a/web/settings.cfg b/web/settings.cfg
new file mode 100644 (file)
index 0000000..6738fae
--- /dev/null
@@ -0,0 +1,26 @@
+# Config file for ldap scripts
+
+ldaphost = "db.debian.org";
+basedn   = "ou=users,dc=debian,dc=org";
+
+gpg = "/usr/bin/gpg";
+keyrings = "/usr/share/keyrings/debian-keyring.gpg:/usr/share/keyrings/debian-keyring.pgp";
+
+webloginhtml = "login.html";
+websearchhtml = "searchform.html";
+websearchresulthtml = "searchresults.html";
+webupdatehtml = "update.html";
+
+webloginurl = "debian/login.cgi";
+websearchurl = "debian/search.cgi";
+webupdateurl = "debian/update.cgi";
+
+# When should authentication tokens expire?
+authexpires = 600;
+
+# How many bytes to use for the blowfish key (max = 56 (448 bits))
+blowfishkeylen = 10;
+
+# Change this!
+authtokenpath = "/tmp/ldapsessions";
+countrylist = "/usr/lib/analog/domains.tab";
diff --git a/web/update.cgi b/web/update.cgi
new file mode 100755 (executable)
index 0000000..5fdfb74
--- /dev/null
@@ -0,0 +1,152 @@
+#!/usr/bin/perl
+
+# (c) 1999 Debian and Randolph Chung. Licensed under the GPL. <tausq@debian.org>
+
+use lib '.';
+use strict vars;
+#use Apache::Registry;
+use CGI;
+use Util;
+use URI::Escape;
+use Net::LDAP qw(:all);
+
+my %config = &Util::ReadConfigFile;
+
+my $query = new CGI;
+my $proto = ($ENV{HTTPS} ? "https" : "http");
+
+my $id = $query->param('id');
+my $authtoken = $query->param('authtoken');
+my $password = &Util::CheckAuthToken($authtoken);
+my $editdn = $query->param('editdn');
+
+if (!($id && $password)) {
+  print "Location: $proto://$ENV{SERVER_NAME}/$config{webloginurl}\n\n";
+  exit;
+} 
+
+my $ldap;
+
+sub DieHandler {
+  $ldap->unbind if (defined($ldap));
+}
+
+$SIG{__DIE__} = \&DieHandler;
+
+$ldap = Net::LDAP->new($config{ldaphost});
+my $auth = 0;
+my $mesg;
+$mesg = $ldap->bind($editdn, password => $password);
+$mesg->sync;
+$auth = ($mesg->code == LDAP_SUCCESS);
+
+if (!$auth) {
+  $ldap->unbind;
+  &Util::HTMLError("You have not been authenticated. Please <a href=\"$proto://$ENV{SERVER_NAME}/$config{webloginurl}\">Login</a>");
+}
+
+# Authenticated....
+# Get our entry...
+$mesg = $ldap->search(base   => $editdn,
+                      filter => "uid=*");
+$mesg->code && &Util::HTMLError($mesg->error);
+  
+my $entries = $mesg->as_struct;
+if ($mesg->count != 1) {
+  # complain and quit
+}
+  
+my @dns = keys(%$entries);
+my $entry = $entries->{$dns[0]};
+
+if (!($query->param('doupdate'))) {
+  # Not yet update, just fill in the form with the current values
+  my %data;
+  
+  # Fill in %data
+  # First do the easy stuff - this catches most of the cases
+  foreach (keys(%$entry)) {
+    $data{$_} = $entry->{$_}->[0];
+  }
+  
+  # Now we have to fill in the rest that needs some processing...
+  $data{id} = $id;
+  $data{authtoken} = $authtoken;
+  $data{editdn} = $editdn;
+  $data{staddress} = $entry->{postaladdress}->[0];
+  $data{staddress} =~ s/\$/\n/;
+  $data{countryname} = &Util::LookupCountry($data{c});
+  
+  $data{email} = join(", ", @{$entry->{emailforward}});  
+
+  # finally we can send output...
+  my ($sub, $substr);
+  &Util::HTMLSendHeader;
+  open (F, "<$config{webupdatehtml}") || &Util::HTMLError($!);
+  while (<F>) {
+    s/~(.+?)~/$data{$1}/g;
+    print;
+  }
+  close F;
+  
+} else {
+  # Actually update stuff...
+  my ($newpassword, $newstaddress);
+  
+  if ($query->param('newpass') && $query->param('newpassvrfy')) {
+    if ($query->param('newpass') ne $query->param('newpassvrfy')) {
+      # passwords don't match...
+      &Util::HTMLError("The passwords you specified do not match. Please go back and try again.");
+    }    
+    # create a md5 crypted password
+    $newpassword = '{crypt}'.crypt($query->param('newpass'), &Util::CreateCryptSalt(1));
+    
+    LDAPUpdate($ldap, $editdn, 'userPassword', $newpassword);
+    &Util::UpdateAuthToken($authtoken, $query->param('newpass'));
+  }  
+
+  $newstaddress = $query->param('staddress');
+  $newstaddress =~ s/\n/\$/m;
+  
+  my ($lat, $long);
+  ($lat, $long) = &Util::CheckLatLong($query->param('latitude'), 
+                                      $query->param('longitude'));
+  
+  LDAPUpdate($ldap, $editdn, 'postalAddress', $newstaddress);
+  LDAPUpdate($ldap, $editdn, 'l', $query->param('l'));
+  LDAPUpdate($ldap, $editdn, 'latitude', $lat);
+  LDAPUpdate($ldap, $editdn, 'longitude', $long);
+  LDAPUpdate($ldap, $editdn, 'c', $query->param('country'));
+  LDAPUpdate($ldap, $editdn, 'postalcode', $query->param('postalcode'));
+  LDAPUpdate($ldap, $editdn, 'telephoneNumber', $query->param('telephonenumber'));
+  LDAPUpdate($ldap, $editdn, 'facsimileTelephoneNumber', $query->param('facsimiletelephonenumber'));
+  LDAPUpdate($ldap, $editdn, 'loginShell', $query->param('loginshell'));
+  LDAPUpdate($ldap, $editdn, 'emailForward', $query->param('email'));
+  LDAPUpdate($ldap, $editdn, 'privatesub', $query->param('privatesub'));
+  LDAPUpdate($ldap, $editdn, 'ircNick', $query->param('ircnick'));
+  LDAPUpdate($ldap, $editdn, 'labeledUrl', $query->param('labeledurl'));
+  LDAPUpdate($ldap, $editdn, 'onvacation', $query->param('onvacation'));
+
+  # when we are done, reload the page with the updated details.
+  my $url = "$proto://$ENV{SERVER_NAME}/$config{webupdateurl}?id=$id&authtoken=$authtoken&editdn=";
+  $url .= uri_escape($editdn, "\x00-\x40\x7f-\xff");
+  print "Location: $url\n\n";  
+}
+
+$ldap->unbind;
+
+sub LDAPUpdate {
+  my $ldap = shift;
+  my $dn = shift;
+  my $attr = shift;
+  my $val = shift;
+  my $mesg;
+  
+  if (!$val) {
+    $mesg = $ldap->modify($dn, delete => { $attr => [] });
+  } else {
+    $val = [ $val ] if (!ref($val));
+    $mesg = $ldap->modify($dn, replace => { $attr => $val });
+    $mesg->code && &Util::HTMLError("error updating $attr: ".$mesg->error);
+  }
+}
diff --git a/web/update.html b/web/update.html
new file mode 100644 (file)
index 0000000..1abb9e3
--- /dev/null
@@ -0,0 +1,362 @@
+  <html><head><title>debian.org Developers LDAP Maintainence</title>
+  <body bgcolor=#9999ff link=red alink=red vlink=red text=black>
+  <br><br><center>
+  <form method=post action="update.cgi">
+  <input type=hidden name=editdn value="~editdn~">  
+  <input type=hidden name=id value="~id~" >
+  <input type=hidden name=authtoken value="~authtoken~">
+  <table border=5 cellspacing=0 cellpadding=3 width="70%" bgcolor=#DDDDDD> 
+  <tr><th colspan=2 bgcolor="#44CCCC" align=center><font size="+2">Debian Developers Database Maintanence</font></th></tr>
+
+  <tr><td align=right>
+    <b>login:</b></td><td>~uid~@debian.org
+  </td></tr>
+
+  <tr><td align=right>
+    <b>Name:</b></td><td>~cn~ ~mn~ ~sn~
+  </td></tr>
+
+  <tr><td align=right>
+    <b>Change password:</b><br><font size="-1">(re-enter to verify)</font>
+  </td><td>
+    <input size=30 name=newpass type=password><br>
+    <input size=30 name=newpassvrfy type=password>
+  </td></tr>
+
+  <tr><td align=right>
+    <b>Street address:</b>
+  </td><td>
+    <textarea name=staddress cols=30 rows=4>~staddress~</textarea>
+  </td></tr>
+  
+  <tr><td align=right>
+    <b>City/State:</b>
+  </td><td>
+    <input name=l value="~l~" size=30>
+  </td></tr>
+
+  <tr><td align=right>
+    <b>Country:</b>
+  </td><td>
+    <select name=country size=5>
+    <option value="~c~" selected>~countryname~
+    <option value=af>Afghanistan
+    <option value=al>Albania
+    <option value=dz>Algeria
+    <option value=as>American Samoa
+    <option value=ad>Andorra
+    <option value=ao>Angola
+    <option value=ai>Anguilla
+    <option value=aq>Antarctica
+    <option value=ag>Antigua and Barbuda
+    <option value=ar>Argentina
+    <option value=am>Armenia
+    <option value=aw>Aruba
+    <option value=au>Australia
+    <option value=at>Austria
+    <option value=az>Azerbaidjan
+    <option value=bs>Bahamas
+    <option value=bh>Bahrain
+    <option value=bd>Bangladesh
+    <option value=bb>Barbados
+    <option value=by>Belarus
+    <option value=be>Belgium
+    <option value=bz>Belize
+    <option value=bj>Benin
+    <option value=bm>Bermuda
+    <option value=bt>Bhutan
+    <option value=bo>Bolivia
+    <option value=ba>Bosnia-Herzegovina
+    <option value=bw>Botswana
+    <option value=bv>Bouvet Island
+    <option value=br>Brazil
+    <option value=io>British Indian Ocean Territory
+    <option value=bn>Brunei Darussalam
+    <option value=bg>Bulgaria
+    <option value=bf>Burkina Faso
+    <option value=bi>Burundi
+    <option value=kh>Cambodia
+    <option value=cm>Cameroon
+    <option value=ca>Canada
+    <option value=cv>Cape Verde
+    <option value=ky>Cayman Islands
+    <option value=cf>Central African Republic
+    <option value=td>Chad
+    <option value=cl>Chile
+    <option value=cn>China
+    <option value=cx>Christmas Island
+    <option value=cc>Cocos (Keeling) Islands
+    <option value=co>Colombia
+    <option value=km>Comoros
+    <option value=cg>Congo
+    <option value=ck>Cook Islands
+    <option value=cr>Costa Rica
+    <option value=hr>Croatia
+    <option value=cu>Cuba
+    <option value=cy>Cyprus
+    <option value=cz>Czech Republic
+    <option value=cs>Czech Republic and Slovakia
+    <option value=cd>Democratic Republic of the Congo
+    <option value=zr>Democratic Republic of the Congo
+    <option value=dk>Denmark
+    <option value=dj>Djibouti
+    <option value=dm>Dominica
+    <option value=do>Dominican Republic
+    <option value=tp>East Timor
+    <option value=ec>Ecuador
+    <option value=eg>Egypt
+    <option value=sv>El Salvador
+    <option value=gq>Equatorial Guinea
+    <option value=er>Eritrea
+    <option value=ee>Estonia
+    <option value=et>Ethiopia
+    <option value=fk>Falkland Islands
+    <option value=fo>Faroe Islands
+    <option value=fj>Fiji
+    <option value=fi>Finland
+    <option value=su>Former USSR
+    <option value=fr>France
+    <option value=fx>France (European Territory)
+    <option value=gf>French Guyana
+    <option value=tf>French Southern Territories
+    <option value=ga>Gabon
+    <option value=gm>Gambia
+    <option value=ge>Georgia
+    <option value=de>Germany
+    <option value=gh>Ghana
+    <option value=gi>Gibraltar
+    <option value=gr>Greece
+    <option value=gl>Greenland
+    <option value=gd>Grenada
+    <option value=gp>Guadeloupe (French)
+    <option value=gu>Guam (USA)
+    <option value=gt>Guatemala
+    <option value=gg>Guernsey
+    <option value=gn>Guinea
+    <option value=gw>Guinea Bissau
+    <option value=gy>Guyana
+    <option value=ht>Haiti
+    <option value=hm>Heard and McDonald Islands
+    <option value=hn>Honduras
+    <option value=hk>Hong Kong
+    <option value=hu>Hungary
+    <option value=is>Iceland
+    <option value=in>India
+    <option value=id>Indonesia
+    <option value=ir>Iran
+    <option value=iq>Iraq
+    <option value=ie>Ireland
+    <option value=im>Isle of Man
+    <option value=il>Israel
+    <option value=it>Italy
+    <option value=ci>Ivory Coast (Cote d'Ivoire)
+    <option value=jm>Jamaica
+    <option value=jp>Japan
+    <option value=je>Jersey
+    <option value=jo>Jordan
+    <option value=kz>Kazakhstan
+    <option value=ke>Kenya
+    <option value=ki>Kiribati
+    <option value=kw>Kuwait
+    <option value=kg>Kyrgyzstan
+    <option value=la>Laos
+    <option value=lv>Latvia
+    <option value=lb>Lebanon
+    <option value=ls>Lesotho
+    <option value=lr>Liberia
+    <option value=ly>Libya
+    <option value=li>Liechtenstein
+    <option value=lt>Lithuania
+    <option value=lu>Luxembourg
+    <option value=mo>Macau
+    <option value=mk>Macedonia
+    <option value=mg>Madagascar
+    <option value=mw>Malawi
+    <option value=my>Malaysia
+    <option value=mv>Maldives
+    <option value=ml>Mali
+    <option value=mt>Malta
+    <option value=mh>Marshall Islands
+    <option value=mq>Martinique (French)
+    <option value=mr>Mauritania
+    <option value=mu>Mauritius
+    <option value=yt>Mayotte
+    <option value=mx>Mexico
+    <option value=fm>Micronesia
+    <option value=md>Moldavia
+    <option value=mc>Monaco
+    <option value=mn>Mongolia
+    <option value=ms>Montserrat
+    <option value=ma>Morocco
+    <option value=mz>Mozambique
+    <option value=mm>Myanmar
+    <option value=na>Namibia
+    <option value=nr>Nauru
+    <option value=np>Nepal
+    <option value=nl>Netherlands
+    <option value=an>Netherlands Antilles
+    <option value=nc>New Caledonia (French)
+    <option value=nz>New Zealand
+    <option value=ni>Nicaragua
+    <option value=ne>Niger
+    <option value=ng>Nigeria
+    <option value=nu>Niue
+    <option value=nf>Norfolk Island
+    <option value=kp>North Korea
+    <option value=mp>Northern Mariana Islands
+    <option value=no>Norway
+    <option value=om>Oman
+    <option value=pk>Pakistan
+    <option value=pw>Palau
+    <option value=pa>Panama
+    <option value=pg>Papua New Guinea
+    <option value=py>Paraguay
+    <option value=pe>Peru
+    <option value=ph>Philippines
+    <option value=pn>Pitcairn Island
+    <option value=pl>Poland
+    <option value=pf>Polynesia (French)
+    <option value=pt>Portugal
+    <option value=pr>Puerto Rico
+    <option value=qa>Qatar
+    <option value=re>Reunion (French)
+    <option value=ro>Romania
+    <option value=ru>Russia
+    <option value=rw>Rwanda
+    <option value=gs>S. Georgia & S. Sandwich Isls.
+    <option value=sh>Saint Helena
+    <option value=kn>Saint Kitts & Nevis
+    <option value=lc>Saint Lucia
+    <option value=pm>Saint Pierre and Miquelon
+    <option value=st>Saint Tome and Principe
+    <option value=vc>Saint Vincent & Grenadines
+    <option value=ws>Samoa
+    <option value=sm>San Marino
+    <option value=sa>Saudi Arabia
+    <option value=sn>Senegal
+    <option value=sc>Seychelles
+    <option value=sl>Sierra Leone
+    <option value=sg>Singapore
+    <option value=sk>Slovak Republic
+    <option value=si>Slovenia
+    <option value=sb>Solomon Islands
+    <option value=so>Somalia
+    <option value=za>South Africa
+    <option value=kr>South Korea
+    <option value=es>Spain
+    <option value=lk>Sri Lanka
+    <option value=sd>Sudan
+    <option value=sr>Suriname
+    <option value=sj>Svalbard and Jan Mayen Islands
+    <option value=sz>Swaziland
+    <option value=se>Sweden
+    <option value=ch>Switzerland
+    <option value=sy>Syria
+    <option value=tj>Tadjikistan
+    <option value=tw>Taiwan
+    <option value=tz>Tanzania
+    <option value=th>Thailand
+    <option value=tg>Togo
+    <option value=tk>Tokelau
+    <option value=to>Tonga
+    <option value=tt>Trinidad and Tobago
+    <option value=tn>Tunisia
+    <option value=tr>Turkey
+    <option value=tm>Turkmenistan
+    <option value=tc>Turks and Caicos Islands
+    <option value=tv>Tuvalu
+    <option value=um>USA Minor Outlying Islands
+    <option value=ug>Uganda
+    <option value=ua>Ukraine
+    <option value=ae>United Arab Emirates
+    <option value=uk>United Kingdom
+    <option value=us>United States
+    <option value=uy>Uruguay
+    <option value=uz>Uzbekistan
+    <option value=vu>Vanuatu
+    <option value=va>Vatican City State
+    <option value=ve>Venezuela
+    <option value=vn>Vietnam
+    <option value=vg>Virgin Islands (British)
+    <option value=vi>Virgin Islands (USA)
+    <option value=wf>Wallis and Futuna Islands
+    <option value=eh>Western Sahara
+    <option value=ye>Yemen
+    <option value=yu>Yugoslavia
+    <option value=zm>Zambia
+    <option value=zw>Zimbabwe   
+    </select>
+  </td></tr>
+  
+  <tr><td align=right>
+    <b>Postal code:</b>
+  </td><td>
+    <input name=postalcode value="~postalcode~" size=30>
+  </td></tr>
+
+  <tr><td align=right>
+    <b>Latitude / Longitude:</b><br>
+    <font size="-1">(format: +-DDDMMSS; + is north/east)</font>
+  </td><td>
+    <input name=latitude value="~latitude~" size=14> /
+    <input name=longitude value="~longitude~" size=14>
+  </td></tr>  
+
+  <tr><td align=right>
+    <b>Phone:</b>
+  </td><td>
+    <input name=telephonenumber value="~telephonenumber~" size=30>
+  </td></tr>
+
+  <tr><td align=right>
+    <b>FAX:</b><br>
+  </td><td>
+    <input name=facsimiletelephonenumber value="~facsimiletelephonenumber~" size=30>
+  </td></tr>
+
+  <tr><td align=right>
+    <b>Preferred shell:</b>
+  </td><td>
+    <input name=loginshell value="~loginshell~" size=30>
+  </td></tr>
+
+  <tr><td align=right>
+    <b>email forwarded to:</b>
+  </td><td>
+    <input name=email value="~email~" size=30>
+  </td></tr>
+
+  <tr><td align=right>
+    <b>debian-private subscript addr:</b>
+  </td><td>
+    <input name=privatesub value="~privatesub~" size=30>
+  </td></tr>
+
+  <tr><td align=right>
+    <b>IRC nickname:</b>
+  </td><td>
+    <input name=ircnick value="~ircnick~" size=30>
+  </td></tr>
+
+  <tr><td align=right>
+    <b>Web page:</b>
+  </td><td>
+    <input name=labeledurl value="~labeledurl~" size=30>
+  </td></tr>
+
+  <tr><td align=right>
+    <b>Vacation message:</b><br>
+    <font size="-1">(Note: if this is set, you will be shown to<br>
+    be on vacation)</font>
+  </td><td>
+    <input name=onvacation value="~onvacation~" size=30>
+  </td></tr>
+
+  <tr><td colspan=2 align="center">
+    <input type=submit name="doupdate" value="Update ...">
+  </td></tr></form>
+  </table>
+  <a href="search.cgi?id=~id~&authtoken=~authtoken~">Return to search page</a><br>
+  <a href="logout.cgi">Logout!</a>
+  </center>
+  </body></html>