From: jis Date: Sun, 23 Sep 2001 04:08:28 +0000 (+0000) Subject: Added WebMoira sources X-Git-Url: http://andersk.mit.edu/gitweb/moira.git/commitdiff_plain/bde1652c9c4187fb2f212baf2f35696e81d0c08a Added WebMoira sources --- diff --git a/webmoira/Makefile.linux b/webmoira/Makefile.linux new file mode 100644 index 00000000..c7048c88 --- /dev/null +++ b/webmoira/Makefile.linux @@ -0,0 +1,12 @@ + +all: mit/moira/libmoirajava.so + +moirai.o: moirai.c mit_moira_MoiraConnectInternal.h + gcc -I/usr/lib/java/include -I/usr/lib/java/include/green_threads -I/usr/lib/java/include/genunix -I/u1/jis/moiradev/moira/include -I/usr/athena/include -I/u1/jis/moiradev/moira/lib -g -c moirai.c -Wall + + +mit/moira/libmoirajava.so: moirai.o + gcc -shared -o libmoirajava.so moirai.o -L /u1/jis/moiradev/moira/lib -lmoira -L/usr/athena/lib -lhesiod -lkrb4 -lkrb5 -lcom_err -ldes425 -lcrypto -lc + cp -p libmoirajava.so libmoirajava_g.so + mv libmoirajava* mit/moira/ + diff --git a/webmoira/Makefile.sparc b/webmoira/Makefile.sparc new file mode 100644 index 00000000..c586b596 --- /dev/null +++ b/webmoira/Makefile.sparc @@ -0,0 +1,12 @@ + +all: mit/moira/libmoirajava.so + +moirai.o: moirai.c mit_moira_MoiraConnectInternal.h + gcc -I/usr/java/include -I/usr/java/include/solaris -I/alt/jis/moira/include -I/usr/athena/include -I/alt/jis/moira/lib -g -c moirai.c -Wall + + +mit/moira/libmoirajava.so: moirai.o + gcc -G -o libmoirajava.so moirai.o -L /alt/jis/moira/lib -lmoira -L/usr/athena/lib -lhesiod -lkrb4 -lkrb5 -lcom_err -ldes425 -lcrypto + cp -p libmoirajava.so libmoirajava_g.so + mv libmoirajava* mit/moira/ + diff --git a/webmoira/fileparts/addfinal.html b/webmoira/fileparts/addfinal.html new file mode 100644 index 00000000..0dcf934d --- /dev/null +++ b/webmoira/fileparts/addfinal.html @@ -0,0 +1,4 @@ +

The members you are adding will receive email sent to this list +within 24 hours. If you are adding members to an Athena group with access to files in AFS, they +immediately have access to those files.

diff --git a/webmoira/fileparts/addme.html b/webmoira/fileparts/addme.html new file mode 100644 index 00000000..968908b3 --- /dev/null +++ b/webmoira/fileparts/addme.html @@ -0,0 +1,5 @@ +

You will begin receiving email from this list within 24 hours. If +you have added yourself to an Athena group with access to files in AFS, you +will have immediate access to those files.

+ diff --git a/webmoira/fileparts/addmember.html b/webmoira/fileparts/addmember.html new file mode 100644 index 00000000..db092828 --- /dev/null +++ b/webmoira/fileparts/addmember.html @@ -0,0 +1,28 @@ +

List owners may add list members of three common types:
+ + +

+ +

+ +

Select the type of member you wish to add, and enter the member(s) +you wish to add. You may enter more than one member of the same +type by listing each member on its own line.

+ diff --git a/webmoira/fileparts/delconfirm.html b/webmoira/fileparts/delconfirm.html new file mode 100644 index 00000000..29176e41 --- /dev/null +++ b/webmoira/fileparts/delconfirm.html @@ -0,0 +1,4 @@ +

The members you are deleting will stop receiving email from this +list within 24 hours. If you have removed members from an Athena group with access to files in AFS, they no +longer have access to those files.

diff --git a/webmoira/fileparts/delme.html b/webmoira/fileparts/delme.html new file mode 100644 index 00000000..184b26e4 --- /dev/null +++ b/webmoira/fileparts/delme.html @@ -0,0 +1 @@ +

You will stop receiving email from this list within 24 hours.

diff --git a/webmoira/fileparts/delmembers.html b/webmoira/fileparts/delmembers.html new file mode 100644 index 00000000..31e2f349 --- /dev/null +++ b/webmoira/fileparts/delmembers.html @@ -0,0 +1,4 @@ +

The members you are deleting will stop receiving email from this +list within 24 hours. If you are removing members from an Athena group with access to files in AFS, they +will no longer have access to those files.

diff --git a/webmoira/fileparts/displaylistinfo.html b/webmoira/fileparts/displaylistinfo.html new file mode 100644 index 00000000..392333ec --- /dev/null +++ b/webmoira/fileparts/displaylistinfo.html @@ -0,0 +1,17 @@ +

An Athena list has a set of characteristics that describe the +list's functionality and purpose. The list's owner can set many of the +list's characteristics. The characteristics include + +description, + +list type (mailing list, group), + +administrator, + +list permissions (active, +inactive, public, private, visible, hidden), + +and last modification. + +

diff --git a/webmoira/fileparts/editme.html b/webmoira/fileparts/editme.html new file mode 100644 index 00000000..7f33d560 --- /dev/null +++ b/webmoira/fileparts/editme.html @@ -0,0 +1,11 @@ +

MIT community members may add themselves to any public Athena list +and remove themselves from any Athena lists.

+ +

Below you will see if you are currently a member of a list, and be +prompted to change your membership.

+ +

Be aware that Athena lists can contain other Athena mailing lists. +If you are not a member of a list directly, you may still be on the +list through another list that is a member (called a sublist). If +this is the case, you can use the show list members function to +see what sublists are members of this list.

diff --git a/webmoira/fileparts/getmembers.html b/webmoira/fileparts/getmembers.html new file mode 100644 index 00000000..bc260570 --- /dev/null +++ b/webmoira/fileparts/getmembers.html @@ -0,0 +1,17 @@ +

The members of this list are displayed with their type. List members +can be of four different types: + +

+ +

diff --git a/webmoira/fileparts/getnewlist.html b/webmoira/fileparts/getnewlist.html new file mode 100644 index 00000000..6313e5a3 --- /dev/null +++ b/webmoira/fileparts/getnewlist.html @@ -0,0 +1,11 @@ +

 

+

MIT community members may request + a list from Athena User Accounts for MIT-specific purposes. Lists + may be used to distribute messages to multiple recipients, to share files + in a locker, or to serve as an email alias for a particular user. Lists + must be owned by a single user or another list.

+

Course mailing lists should be requested using the ACS Course Mailing List Request + Form.

+

 

+

 

diff --git a/webmoira/fileparts/help.html b/webmoira/fileparts/help.html new file mode 100644 index 00000000..98e61c78 --- /dev/null +++ b/webmoira/fileparts/help.html @@ -0,0 +1,134 @@ +

How to +Manage your Athena Lists | Who to Contact for +Help | Glossary of List Management +Terms

+ +

How to Manage your Athena +Lists

+ +

Athena List Management is one of the tools list owners have to +manage their lists. Other tools include blanche and listmaint on +Athena. To get started, you will need MIT Personal Certificates on +your machine. Once you have them, select any function, provide a list +name, and press enter on your keyboard, or 'Go'. Each page of the web +interface has context sensitive help to guide you. [top of help]

+ +

 

Who to +Contact for Help

+ +

If you do need further assistance with your lists, Athena User Accounts provides +assistance with Athena list concerns, including creating new lists, +modifying lists, and deleting lists. If you have trouble using this +interface to maintain your lists, please contact Accounts at accounts@mit.edu, or +617-253-1325. [top of +help]

+ +

 

+ +

Glossary of List +Management Terms

+ +

List Description
A short +description may be specified for an Athena list. This description is +displayed for lists which are not marked hidden if list information is +generated by another Athena user. [top of help]

+ +

Mailing List
An Athena list is by +default a mailing list. That means that the members of the list will +be mailable through the address listname@mit.edu, which will +distribute the mail the all the members of the list. Such a list may +include email addresses for users outside of MIT. Lists can be both a +mailing list and a group. [top of +help]

+ +

Group
In addition to (or instead of) +being a mailing list, an Athena list can also be a group. A group can +be used as an access control list on the AFS file system, for +example. If you wish to be able to set access permissions on an Athena +directory or locker for the members of your list, you should choose to +make it a group. When a list can maintain a file space on Athena, it +is also has a group ID number.

+ +

Note that only Athena users on your list will be able to take +advantage of this feature. If you have any other members on your list, +such as email addresses outside of MIT, they will not be able to +access Athena file systems. [top of +help]

+ +

List Administrator(s)
Every Athena +mailing list must be owned by one or more +administrators. Administrators have the power to change list +characteristics. Administrators can be a user or another Athena +list. [top of help]

+ +

List Permissions
These +describe the state of the list and what users who are not the +administrator can do with the list. A list's permissions can be viewed +with the "display list characteristics" option, and edited +by list owners with the "update list characteristics" +option.

+ + + +[top of help] + +

Last Modification
This tells you +when the list was last changed in some way and by who. [top of help]

+ +

Athena User
Email addresses of the +form name@mit.edu usually indicate name is an Athena username (also +sometimes called a Kerberos Name or MIT Network ID). To add an Athena +user to an Athena list, you enter only member name to the left of the +@. For example, to add eclapton@mit.edu to an Athena list, provide +'eclapton' as the member name. [top +of help]

+ +

List
An Athena list is by default a +mailing list. That means that the members of the list will be mailable +through the address listname@mit.edu, which will distribute the mail +the all the members of the list. Such a list may include email +addresses for users outside of MIT.

+ +

List members can be other Athena lists, of the form +listname@mit.edu. To add an Athena list as a member of another Athena +list, enter only the part of the address before the @ symbol. For +example, to add accounts@mit.edu as a member, provide 'accounts' as +the member name. [top of +help]

+ +

String
Athena lists often have +non-MIT users and lists as members. To add an email address that +contains an @ but is outside of mit.edu, such as +mitalum@anotherisp.net, you add them as member type string. To add a +string, provide the complete address, including information on both +sides of the @. [top of +help]

+ +

Kerberos Principal
+Specifying a list member as a Kerberos principal allows a member to be +on a list without receiving any email sent to the list. This is a +rarely used member type. [top of +help].

diff --git a/webmoira/fileparts/removemembers.html b/webmoira/fileparts/removemembers.html new file mode 100644 index 00000000..c1c1432b --- /dev/null +++ b/webmoira/fileparts/removemembers.html @@ -0,0 +1,12 @@ +

List owners may delete any list members. If you wish to have a list +deleted, please contact Athena +Accounts.

+ + +

Check each member you wish to delete, and then 'Delete +Selected'. For list with many members, you will see initially see ten +members per screen, and will be able to move to the next or previous +ten members, selecting as many members to delete as you wish. If you +would prefer to see all the members of a list, select 'Show All' to +display all list members. Please note that displaying all members at +once may take a long time to load.

diff --git a/webmoira/fileparts/top.html b/webmoira/fileparts/top.html new file mode 100644 index 00000000..423e6fa0 --- /dev/null +++ b/webmoira/fileparts/top.html @@ -0,0 +1,24 @@ + +

Welcome to the Athena list management web +interface. This is one of several +tools you can use to update list membership information and +characteristics.

+ +

This service requires MIT Personal + Certificates.

+ + +

To begin, +

    +
  1. select an option
  2. +
  3. enter a list name (e.g. accounts)
  4. +
  5. either press enter on your keyboard or select 'Go'.
  6. +

+ + +

Feedback and questions about this service may be directed to web-listmaint@mit.edu.

+ +
diff --git a/webmoira/fileparts/updatelistinfo.html b/webmoira/fileparts/updatelistinfo.html new file mode 100644 index 00000000..ad9ff575 --- /dev/null +++ b/webmoira/fileparts/updatelistinfo.html @@ -0,0 +1,9 @@ +

An Athena list has a set of characteristics that describe the +list's functionality and purpose. The list's owner may update many of +the list's characteristics below, including if the list is a mailing list, a public or private list, and if it is +a hidden or visible +list. You may also edit the list administrator and description. diff --git a/webmoira/fileparts/updatelistinfoconf.html b/webmoira/fileparts/updatelistinfoconf.html new file mode 100644 index 00000000..b80883be --- /dev/null +++ b/webmoira/fileparts/updatelistinfoconf.html @@ -0,0 +1 @@ +

Updates to list characteristics will take effect within 24 hours.

diff --git a/webmoira/help.jhtml b/webmoira/help.jhtml new file mode 100644 index 00000000..584e547f --- /dev/null +++ b/webmoira/help.jhtml @@ -0,0 +1,107 @@ + + + + +help with list management + + + + + + + + + + + + + + + +
+ homelist + management services: help
+ + select an + option: +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + display + list characteristics
+ + show list members
+ + add + or remove yourself from a list
+ list owners may
+ + update + list characteristics
+ + add + list members
+ + remove + list members
+ enter a list name:
+ + +
+ + request + a list +

help +

+
+ Forgot a username?
Check the MIT Directory:

+ + +
+ How to use the MIT Directory
+
+

+

+
+ + + + + +
+

 

+ + diff --git a/webmoira/home.gif b/webmoira/home.gif new file mode 100644 index 00000000..9e5cac4c Binary files /dev/null and b/webmoira/home.gif differ diff --git a/webmoira/index.jhtml b/webmoira/index.jhtml new file mode 100644 index 00000000..ed3eebea --- /dev/null +++ b/webmoira/index.jhtml @@ -0,0 +1,109 @@ + + + + + + + +athena list management + + + + + + + + + + + + +
+ athena list + management
+ + select an + option: +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + display + list characteristics
+ + show list members
+ + add + or remove yourself from a list
+ list owners may
+ + update + list characteristics
+ + add + list members
+ + remove + list members
+ enter a list name:
+ + +
+ + request + a new list +

help +

+
+ Forgot a username?
Check the MIT Directory:

+ + +
+ How to use the MIT Directory
+
+

+

+
+ + + + +
+ + +
+

 

+ + diff --git a/webmoira/mit.gif b/webmoira/mit.gif new file mode 100644 index 00000000..68932712 Binary files /dev/null and b/webmoira/mit.gif differ diff --git a/webmoira/mit/moira/AuthenticationError.java b/webmoira/mit/moira/AuthenticationError.java new file mode 100644 index 00000000..43630e86 --- /dev/null +++ b/webmoira/mit/moira/AuthenticationError.java @@ -0,0 +1,8 @@ +package mit.moira; + +public class AuthenticationError extends Exception { + public AuthenticationError(String message) { + super(message); + } + +} diff --git a/webmoira/mit/moira/Coder.java b/webmoira/mit/moira/Coder.java new file mode 100644 index 00000000..8ac18193 --- /dev/null +++ b/webmoira/mit/moira/Coder.java @@ -0,0 +1,146 @@ +package mit.moira; + +import java.util.Vector; +import java.util.Enumeration; + +/** + * Class with static functions for base64 encoding and decoding as well + * as other utilities. + */ + +public class Coder { + static final char encv[] = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' }; + static char x2c (char [] what, int off) { + int digit; + digit = (what[off] >= 'A' ? ((what[off] & 0xdf) - 'A')+10 : (what[off] - '0')); + digit *= 16; + digit += (what[off+1] >= 'A' ? ((what[off+1] & 0xdf) - 'A')+10 : (what[off+1] - '0')); + return((char)digit); + } + + private Coder() { // No Constructors... + } + + /** + * Remove URL escape sequences + * + * @param aurl A String with potential embedded escapes. + * @return the String with the escapes removed. + */ + public static String unescape_url(String aurl) { + int x, y, i; + char [] url = new char [aurl.length()]; + aurl.getChars(0, aurl.length(), url, 0); + StringBuffer ret = new StringBuffer(); + for (i = 0; i < url.length; i++) { + if (url[i] == '%') { + ret.append(x2c(url, i+1)); + i += 2; + } else if (url[i] == '+') { + ret.append(' '); + } else ret.append(url[i]); + } + return (new String(ret)); + } + + /** + * Encodes a byte array in base64 characters and returns the result + * as a String. + * + * @param data the Byte array + * @return The encoded string + */ + + public static String encode(byte [] data) { + StringBuffer ret = new StringBuffer(); + int len = data.length; + int c; + int p = 0; + int i = 0; + int nib1, nib2, nib3, nib4; + while (i < len) { + if ((i+3) <= len) { + c = (data[i++] & 0xFF); + c = (c << 8) | (data[i++] & 0xFF); + c = (c << 8) | (data[i++] & 0xFF); + nib1 = (c & 0x0FC0000) >>> 18; + nib2 = (c & 0x003F000) >>> 12; + nib3 = (c & 0x0000FC0) >>> 6; + nib4 = (c & 0x000003F); + ret.append(encv[nib1]); + ret.append(encv[nib2]); + ret.append(encv[nib3]); + ret.append(encv[nib4]); + } else { + p = len - i; + c = data[i++]; + c = c << 8; + if (p > 1) c |= (data[i++] & 0xFF); + c = c << 8; + if (p > 2) c |= (data[i++] & 0xFF); // Should never happen + nib1 = (c & 0x0FC0000) >>> 18; + nib2 = (c & 0x003F000) >>> 12; + nib3 = (c & 0x0000FC0) >>> 6; + nib4 = (c & 0x000003F); + ret.append(encv[nib1]); + ret.append(encv[nib2]); + if (p == 1) ret.append('='); + else ret.append(encv[nib3]); + ret.append('='); + } + } + return (new String(ret)); + } + + /** + * Decodes a base64 encoded string and returns the decoded value + * in a byte array. + * + * @param s The base64 encoded string + * @return the byte array decoded + */ + public static byte [] decode (String s) { + Vector v = new Vector(); + int p = 0; + int i = 0; + int k = 0; + int z = 0; + int reg = 0; + for (i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '/') z = 63; + else if (c == '+') z = 62; + else if ((c >= 'a') && (c <= 'z')) z = (c - 'a') + 26; + else if ((c >= 'A') && (c <= 'Z')) z = (c - 'A'); + else if ((c >= '0') && (c <= '9')) z = (c - '0') + 52; + else if (c == '=') { + p++; + z = 0; + } else continue; + reg <<= 6; + reg += z; + k++; + if (k == 4) { + for (z = 0; z < 3; z++) { + v.addElement(new Integer((reg & 0xff0000) >>> 16)); + reg = (reg << 8); + } + k = 0; + } + } + Enumeration e = v.elements(); + byte [] retval = new byte[v.size() - p]; + for(i = 0; i < v.size() - p; i++) { + retval[i] = ((Integer) e.nextElement()).byteValue(); + } + return (retval); + } +} + + + diff --git a/webmoira/mit/moira/Kticket.java b/webmoira/mit/moira/Kticket.java new file mode 100644 index 00000000..28c6f445 --- /dev/null +++ b/webmoira/mit/moira/Kticket.java @@ -0,0 +1,64 @@ +package mit.moira; + +import java.util.Date; +import java.io.IOException; + +public class Kticket implements Runnable { + Object LOCK; + String name; + String instance; + String realm; + boolean dostop = false; + Runtime r; + Date renewTime; + + Kticket(String name, String instance, String realm, Object lock) { + this.name = name; + this.instance = instance; + this.realm = realm; + this.LOCK = lock; + r = Runtime.getRuntime(); + renewTime = new Date(); + } + + public void run() { + for(;;) { + if (dostop) return; + Date now = new Date(); + if (now.after(renewTime)) { + renew(); + renewTime = new Date(System.currentTimeMillis() + 6*3600*1000L); // 6 hours + } + try { + Thread.sleep(30*1000L); // Sleep for 30 seconds + } catch (InterruptedException i) { + // Nothing to be done about it + } + } + } + + public void renew() { + synchronized(LOCK) { + try { + Process p = r.exec("/usr/athena/bin/kinit -k -t /mit/jis/javahacking/moira/KEY " + name + "/" + instance + "@" + realm); + p.waitFor(); + } catch (IOException e) { + e.printStackTrace(); + } catch (InterruptedException i) { + } + } + } + + public void destroy() { + dostop = true; + synchronized(LOCK) { + try { + Process p = r.exec("/usr/athena/bin/kdestroy"); + p.waitFor(); + } catch (IOException e) { + e.printStackTrace(); + } catch (InterruptedException i) { + } + } + } +} diff --git a/webmoira/mit/moira/ListInfo.java b/webmoira/mit/moira/ListInfo.java new file mode 100644 index 00000000..34b8ee7b --- /dev/null +++ b/webmoira/mit/moira/ListInfo.java @@ -0,0 +1,48 @@ +package mit.moira; + +import java.util.Date; +import java.text.SimpleDateFormat; + +public class ListInfo { + String name; + boolean active; + boolean bpublic; + boolean hidden; + boolean maillist; + boolean grouplist; + String gid_original; + int gid; + String ace_type; + String ace_name; + String description; + Date modtime; + String moduser; + String modwith; + static final SimpleDateFormat df = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss"); + + public ListInfo(String name, String active, String spublic, String hidden, String maillist, String grouplist, String gid, String ace_type, String ace_name, String description, String modtime, String moduser, String modwith) throws MoiraException { + this.name = name; + this.active = (active.equals("1")) ? true : false; + this.bpublic = (spublic.equals("1")) ? true : false; + this.hidden = (hidden.equals("1")) ? true : false; + this.maillist = (maillist.equals("1")) ? true : false; + this.grouplist = (grouplist.equals("1")) ? true : false; + this.gid_original = gid; + try { + this.gid = Integer.parseInt(gid); + } catch (NumberFormatException e) { + this.gid = 0; + } + this.ace_type = ace_type; + this.ace_name = ace_name; + this.description = description; + try { + this.modtime = df.parse(modtime); + } catch (java.text.ParseException p) { + throw new MoiraException(p.getMessage()); + } + this.moduser = moduser; + this.modwith = modwith; + } +} + diff --git a/webmoira/mit/moira/Member.java b/webmoira/mit/moira/Member.java new file mode 100644 index 00000000..5bbf51d3 --- /dev/null +++ b/webmoira/mit/moira/Member.java @@ -0,0 +1,23 @@ +package mit.moira; + +public class Member { + String member_type; + String member_id; + + public Member(String mt, String mi) { + member_type = mt; + member_id = mi; + } + + public String getMemberType() { + return (member_type); + } + + public String getMemberId() { + return (member_id); + } + + public String toString() { + return("[Member type=" + member_type + ", id=" + member_id + "]"); + } +} diff --git a/webmoira/mit/moira/MoiraConnect.java b/webmoira/mit/moira/MoiraConnect.java new file mode 100644 index 00000000..d34d1856 --- /dev/null +++ b/webmoira/mit/moira/MoiraConnect.java @@ -0,0 +1,119 @@ +package mit.moira; + +public class MoiraConnect { + boolean connected = false; + boolean isauth = false; + Object LOCK; + + String server = ""; + MoiraConnect() { + } + + public MoiraConnect(String server, Object lock) { + this.server = server; + this.LOCK = lock; + } + + public void connect() throws MoiraException { + if (!connected) { + MoiraConnectInternal.connect(server); + connected = true; + } + } + + public void auth() throws MoiraException { + if (!isauth) { + synchronized(LOCK) { + MoiraConnectInternal.auth(); + } + isauth = true; + } + } + + public void proxy(String user) throws MoiraException { + MoiraConnectInternal.proxy(user); + } + + public void disconnect() { + if (connected) { + MoiraConnectInternal.disconnect(); + connected = false; + isauth = false; + } + } + + public ListInfo get_list_info(String list) throws MoiraException { + if (!connected) throw new MoiraException("Not Connected"); + String [] args = new String[1]; + args[0] = list; + Object [] retinternal = MoiraConnectInternal.mr_query("get_list_info", args); + if (retinternal == null) return (null); + if (retinternal.length == 0) return (null); + if (retinternal.length != 1) throw new MoiraException("get_list_info returned more then one list!"); + String [] entry = (String []) retinternal[0]; + return (new ListInfo(entry[0], entry[1], entry[2], entry[3], entry[4], entry[5], entry[6], entry[7], entry[8], entry[9], entry[10], entry[11], entry[12])); + } + + void update_list_info(String list, ListInfo info) throws MoiraException { + if (!connected) throw new MoiraException("Not Connected"); + String [] args = new String[11]; + args[0] = list; + args[1] = info.name; + args[2] = info.active ? "1" : "0"; + args[3] = info.bpublic ? "1" : "0"; + args[4] = info.hidden ? "1" : "0"; + args[5] = info.maillist ? "1" : "0"; + args[6] = info.grouplist ? "1" : "0"; + args[7] = info.gid_original; + args[8] = info.ace_type; + args[9] = info.ace_name; + args[10] = info.description; + Object [] retinternal = MoiraConnectInternal.mr_query("update_list", args); + return; + } + + void delete_member_from_list(String list, String type, String member) throws MoiraException { + if (!connected) throw new MoiraException("Not Connected"); + String [] args = new String[3]; + args[0] = list; + args[1] = type; + args[2] = member; + Object [] retinternal = MoiraConnectInternal.mr_query("delete_member_from_list", args); + return; + } + + void add_member_to_list(String list, String type, String member) throws MoiraException { + if (!connected) throw new MoiraException("Not Connected"); + String [] args = new String[3]; + args[0] = list; + args[1] = type; + args[2] = member; + Object [] retinternal = MoiraConnectInternal.mr_query("add_member_to_list", args); + return; + } + + public Member [] get_members_of_list(String list) throws MoiraException { + if (!connected) throw new MoiraException("Not Connected"); + String [] args = new String[1]; + args[0] = list; + Object [] retinternal = MoiraConnectInternal.mr_query("get_members_of_list", args); + if (retinternal == null) return (null); + Member [] retval = new Member[retinternal.length]; + for (int i = 0; i < retinternal.length; i++) { + String [] entry = (String []) retinternal[i]; + retval[i] = new Member(entry[0], entry[1]); + } + return (retval); + } +} + +class MoiraConnectInternal { + static native void connect(String server) throws MoiraException; + static native void auth() throws MoiraException; + static native void proxy(String user) throws MoiraException; + static native void disconnect(); + static native Object [] mr_query(String command, String [] args) throws MoiraException; + static { + System.loadLibrary("moirajava"); + } +} diff --git a/webmoira/mit/moira/MoiraException.java b/webmoira/mit/moira/MoiraException.java new file mode 100644 index 00000000..87ac82f7 --- /dev/null +++ b/webmoira/mit/moira/MoiraException.java @@ -0,0 +1,19 @@ +package mit.moira; + +public class MoiraException extends Exception { + int code = 0; + + MoiraException(String mess) { + super(mess); + } + + MoiraException(String mess, int code) { + super(mess); + this.code = code; + } + + public int getCode() { + return (code); + } + +} diff --git a/webmoira/mit/moira/MoiraServlet.java b/webmoira/mit/moira/MoiraServlet.java new file mode 100644 index 00000000..eeebbe3a --- /dev/null +++ b/webmoira/mit/moira/MoiraServlet.java @@ -0,0 +1,1339 @@ +package mit.moira; + +import javax.servlet.*; +import javax.servlet.http.*; +import java.io.*; +import java.util.*; +import java.sql.SQLException; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.ResourceBundle; + +public class MoiraServlet extends HttpServlet { + static final String MOIRA_SERVER = "moira.mit.edu"; + Object LOCK = new Object(); // Used to synchronize Moira authentication + // with the ticket fetching thread + Kticket kt = new Kticket("jis5", "foobar", "ATHENA.MIT.EDU", LOCK); + boolean ktinit = false; + private static final int MAXDISPLAY = 10; + Hashtable FileParts = new Hashtable(); + Hashtable FileTimes = new Hashtable(); + static final SimpleDateFormat df = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss"); + static final int KE_RD_AP_BADD = 39525414; // This is a kludge + + + public synchronized void service(HttpServletRequest request, HttpServletResponse response) { + System.err.println("MoiraSerlvet: Service"); + System.err.println("---------------------"); + + if (!ktinit) { // Once only initialization + ktinit = true; + Thread t = new Thread(kt); + t.start(); + } + + String msg = null; + + Hashtable qv = new Hashtable(); + + String modifier = ""; + + try { // Get Parameters + Enumeration e = request.getParameterNames(); + while (e.hasMoreElements()) { + String pname = (String)e.nextElement(); + String [] pvaluearray = request.getParameterValues(pname); + String pvalue = ""; + if (pvaluearray.length > 1) { + for (int i = 0; i < pvaluearray.length; i++) + pvalue += pvaluearray[i]; + } else pvalue = pvaluearray[0]; + + System.err.println(pname + " = " + pvalue); + qv.put(pname, pvalue); + } + + modifier = (String)qv.get("modifier"); + if (modifier == null) modifier = ""; + + if (modifier.equals("showform")) { + do_showform(qv, request, response); + return; + } else if (modifier.equals("showurl")) { + do_showurl(qv, request, response); + return; + } else if (modifier.equals("makesession")) { + request.getSession(true); + return; + } + + String operation = (String) qv.get("operation"); + if (operation == null) { + try { + DataOutputStream out = new DataOutputStream(response.getOutputStream()); + response.setContentType("text/html"); + if (modifier.equals("getfile")) + out.writeBytes("You need to select an operation from the list on the left!"); + else if (modifier.equals("displayonly")) + out.writeBytes("error"); + else if (modifier.equals("getlistname")) { + out.writeBytes(do_getlistinput(qv, request, response)); + } + out.flush(); + } catch (Exception ee) { + ee.printStackTrace(); + } + return; + } + if (operation.charAt(operation.length()-2) == '\r') // Trim newline stuff + operation = operation.substring(0, operation.length() - 2); + if (modifier.equals("getlistname")) { + msg = do_getlistinput(qv, request, response); + if (msg.equals("")) return; + } else if (modifier.equals("getfile")) { + msg = getfile(operation); + } else if (operation.equals("getmembers")) { + if (modifier.equals("displayonly")) { + msg = "show members"; + } else + msg = do_getmembers(qv, request, response); + if (msg.equals("")) return; + } else if (operation.equals("addfinal")) { + if (modifier.equals("displayonly")) { + msg = "added members"; + } else + msg = do_addfinal(qv, request, response); + if (msg.equals("")) return; + } else if (operation.equals("editme")) { + if (modifier.equals("displayonly")) { + msg = "add or remove yourself from a list"; + } else + msg = do_editme(qv, request, response); + if (msg.equals("")) return; + } else if (operation.equals("removemembers")) { + if (modifier.equals("displayonly")) { + msg = "edit/remove members"; + } else + msg = do_removemembers(qv, request, response); + if (msg.equals("")) return; + } else if (operation.equals("delmembers")) { + if (modifier.equals("displayonly")) { + msg = "edit/remove members"; + } else + msg = do_delmember(qv, request, response); + if (msg.equals("")) return; + } else if (operation.equals("delconfirm")) { + if (modifier.equals("displayonly")) { + msg = "member deleted"; + } else + msg = do_delconfirm(qv, request, response); + if (msg.equals("")) return; + } else if (operation.equals("addmember")) { + if (modifier.equals("displayonly")) { + msg = "add member(s)"; + } else + msg = do_addmember(qv, request, response); + if (msg.equals("")) return; + } else if (operation.equals("addme")) { + if (modifier.equals("displayonly")) { + msg = "added to list"; + } else + msg = do_addremme(qv, request, response, true); + } else if (operation.equals("delme")) { + if (modifier.equals("displayonly")) { + msg = "removed from list"; + } else + msg = do_addremme(qv, request, response, false); + } else if (operation.equals("displaylistinfo")) { + if (modifier.equals("displayonly")) { + msg = "display list characteristics"; + } else + msg = do_showlistinfo(qv, request, response); + } else if (operation.equals("updatelistinfo")) { + if (modifier.equals("displayonly")) { + msg = "update list characteristics"; + } else + msg = do_updatelistinfo(qv, request, response); + } else if (operation.equals("updatelistinfoconf")) { + if (modifier.equals("displayonly")) { + msg = "update list characteristics"; + } else + msg = do_updatelistinfoconf(qv, request, response); + } else { + sendError("Unimplemented Operation: " + (String)qv.get("operation"), response, true); + return; + } + + } catch (AuthenticationError e) { + msg = "" + e.getMessage() + "

\r\n"; + } catch (Exception e) { + msg += "

Error during Processing

\r\n"; + msg += "Please try again later\r\n"; + e.printStackTrace(); + } + + try { + DataOutputStream out = new DataOutputStream(response.getOutputStream()); + response.setContentType("text/html"); + if (!MOIRA_SERVER.equals("moira.mit.edu") && modifier.equals("")) + out.writeBytes("Moira Server: " + MOIRA_SERVER + "
\r\n"); + out.writeBytes(msg); + out.flush(); + } catch (Exception e) { + e.printStackTrace(); + } + + } + void sendError(String message, HttpServletResponse response, boolean showheader) { + String msg; + if (showheader) { + msg = "Error\r\n"; + msg += "

Error

\r\n"; + msg += message; + msg += "

\r\n"; + } else msg = "" + message + ""; + try { + DataOutputStream out = new DataOutputStream(response.getOutputStream()); + response.setContentType("text/html"); + out.writeBytes(msg); + out.flush(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Authenticate the user. + * + * @param request The HttpServlet request object + * @return String value of the Kerberos username who owns this request + * @exception AuthenticationError if authentication cannot be performed + */ + String do_authentication(HttpServletRequest request) throws AuthenticationError { + + // Attempt to obtain authenticated username from the session + + HttpSession session = request.getSession(true); + String kname = (String) session.getValue("kname"); + if (kname != null) return (kname); + + String client_email = (String)request.getAttribute("org.apache.jserv.SSL_CLIENT_EMAIL"); + if (client_email == null) + throw new AuthenticationError("You must use a Certificate to access this service."); + + // Need to remove the @MIT.EDU portion + int i = client_email.indexOf('@'); + if (i == -1) + throw new AuthenticationError("Malformed or non-MIT Certificate, shouldn't happen!"); + if (!client_email.substring(i+1).equals("MIT.EDU")) + throw new AuthenticationError("Certificate Email Address must end in MIT.EDU"); + kname = client_email.substring(0, i); + session.putValue("kname", kname); + return (kname); + } + + String do_getmembers(Hashtable qv, HttpServletRequest request, HttpServletResponse response) throws AuthenticationError { + String kname = do_authentication(request); + + String msg = null; + String arg = (String) qv.get("list"); + if (arg == null || arg.equals("")) { + msg = "

Argument is required

"; + return (msg); + } + + // Obtain the list of members stashed in the session object + // If this is the first call to do_getmembers for this list, + // we won't have one in which case we will obtain it from Moira + // below + + String list = ""; + Member [] members = null; + HttpSession session = request.getSession(true); + if (session != null) { + list = (String)session.getValue("list"); + if (list == null) list = ""; + members = (Member []) session.getValue("members"); + } + if (!list.equals(arg)) // Different list from argument, new session + members = null; // Force obtaining members from Moira + + // Determine offset of list to view + int offset = 0; + String tmp = (String)qv.get("offset"); + if (tmp != null) + offset = Integer.parseInt(tmp); + + boolean showall = false; + if (offset < 0) showall = true; + + if (members == null) { + MoiraConnect mc = null; + try { + mc = connect(); + mc.proxy(kname); + members = mc.get_members_of_list(arg); + mc.disconnect(); + mc = null; + if (members != null) + session.putValue("members", members); + session.putValue("list", arg); + } catch (MoiraException m) { + try { + msg = "

\r\n"; + msg += m.getMessage(); + msg += "

\r\n"; + msg += "\r\n"; + return (msg); + } catch (Exception e) { + e.printStackTrace(); + } + } finally { + if (mc != null) mc.disconnect(); + } + } + + // Do the actual display of the members + if (members == null) + return("No members for list " + arg + ""); + + msg = "\r\n"; + msg += "\r\n"; + if ((offset > 0) && !showall) { + msg += "\r\n"; + } else + msg += ""; + if (!showall && ((offset > 0) || (members.length > len))) { + msg += "\r\n"; + } else + msg += ""; + if (members.length > len && !showall) { + msg += "\r\n"; + } else + msg += ""; + msg += "\r\n"; + msg += "
 \r\n"; + msg += "\r\n"; + int len = offset + MAXDISPLAY; + if (len > members.length) len = members.length; + if (showall) { + offset = 0; + len = members.length; + } + for (int i = offset; i < len; i++) { + msg += ""; + msg += "\r\n"; + } + msg += "
Members of list: " + arg + "
" + members[i].getMemberType() + "" + members[i].getMemberId() + "
 
\r\n"; + msg += ""; + msg += "\r\n"; + msg += ""; + msg += "
     
\r\n"; + msg += ""; + msg += ""; + msg += ""; + msg += "
 
\r\n"; + msg += ""; + msg += "\r\n"; + msg += ""; + msg += "
     
\r\n"; + return (msg); + } + + void do_showform(Hashtable qv, HttpServletRequest request, HttpServletResponse response) { + String server = request.getServerName(); + if (server.indexOf(".") == -1) + server += ".mit.edu"; // MIT Specific kludge!!! XXX + String msg = "
"; + try { + DataOutputStream out = new DataOutputStream(response.getOutputStream()); + response.setContentType("text/html"); + out.writeBytes(msg); + out.flush(); + } catch (Exception e) { + e.printStackTrace(); // Nothing else I can do here + } + } + + void do_showurl(Hashtable qv, HttpServletRequest request, HttpServletResponse response) { + HttpSession session = request.getSession(true); // Sigh, have to create the session here if it doesn't already exist. + String msg = ""; + try { + DataOutputStream out = new DataOutputStream(response.getOutputStream()); + response.setContentType("text/html"); + out.writeBytes(msg); + out.flush(); + } catch (Exception e) { + e.printStackTrace(); // Nothing else I can do here + } + } + + String do_getlistinput(Hashtable qv, HttpServletRequest request, HttpServletResponse response) { + HttpSession session = request.getSession(false); + String list = null; + list = (String) qv.get("list"); + if ((list == null) && (session != null)) list = (String) session.getValue("list"); + if (list == null) list = ""; + return("\r\n"); + } + + String do_addmember(Hashtable qv, HttpServletRequest request, HttpServletResponse response) throws AuthenticationError { + String msg = ""; + String list = (String) qv.get("list"); + if (list == null || list.equals("")) { + msg = "

Argument is required

"; + return (msg); + } + + String kname = do_authentication(request); + + HttpSession session = request.getSession(true); + + MoiraConnect mc = null; + ListInfo li = null; + try { + mc = connect(); + mc.proxy(kname); + li = mc.get_list_info(list); + } catch (MoiraException m) { + msg += "Error getting list info: " + m.getMessage(); + msg += "\r\n"; + return (msg); + } finally { + try { + if (mc != null) mc.disconnect(); + mc = null; + } catch (Exception e) { + } + } + + String list_description = ""; + if (li != null) list_description = descript(li.description); + + msg += "\r\n"; + msg += " \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n

Add member(s) to list " + list + "
\r\n
Description: " + list_description + "
\r\n (you may enter more than one member of the same type by listing \r\n each member on its own line)

\r\n
\r\n \r\n \r\n \r\n
\r\n"; + msg += "\r\n"; + msg += "\r\n"; + msg += "\r\n"; + session.putValue("list", list); + session.removeValue("members"); // In case we had them from old list + return (msg); + } + + String do_editme(Hashtable qv, HttpServletRequest request, HttpServletResponse response) throws AuthenticationError { + String msg = null; + String arg = (String) qv.get("list"); + if (arg == null || arg.equals("")) { + msg = "

Argument is required

"; + return (msg); + } + + String kname = do_authentication(request); + + HttpSession session = request.getSession(true); + + MoiraConnect mc = null; + boolean found = false; + boolean sublists = false; + Member [] members = null; + try { + mc = connect(); + mc.proxy(kname); + members = mc.get_members_of_list(arg); + if (members != null) { + for (int i = 0; i < members.length; i++) { + if (members[i].getMemberId().equals(kname)) { + found = true; + if (sublists) break; + } + if (members[i].getMemberType().equals("LIST")) { + sublists = true; + if (found) break; + } + } + } + mc.disconnect(); + mc = null; + } catch (MoiraException m) { + try { + msg = "

\r\n"; + msg += m.getMessage(); + msg += "

\r\n"; + msg += "\r\n"; + return (msg); + } catch (Exception e) { + e.printStackTrace(); + } + } finally { + if (mc != null) mc.disconnect(); + } + msg = "You are " + (found ? "" : "not ") + "a member of the list " + arg + ".
\r\n"; + if (!found && sublists) + msg += "Note: You may be a member of a sublist of " + arg + ". You may wish to check the sublists by using the show list members function.
\r\n"; + msg += "\r\n"; + msg += "\r\n"; + if (found) + msg += "\r\n"; + else + msg += "\r\n"; + msg += "\r\n"; + msg += ""; + if (members != null) { + session.putValue("list", arg); + session.putValue("members", members); + } + return (msg); + } + + String do_addremme(Hashtable qv, HttpServletRequest request, HttpServletResponse response, boolean add) throws AuthenticationError { + HttpSession session = request.getSession(false); // Better be one + if (session == null) { + return("

Could not proceed, has it been 30 minutes since you last interaction. If so, back up and try again.

"); + } + + String kname = do_authentication(request); + + String msg = null; + String listname = (String) session.getValue("list"); + if (listname == null) { + return("

Could not find list name (shouldn't happen).

"); + } + MoiraConnect mc = null; + try { + mc = connect(); + mc.proxy(kname); + if (!add) + mc.delete_member_from_list(listname, "USER", kname); + else + mc.add_member_to_list(listname, "USER", kname); + } catch (MoiraException m) { + msg = "

Unable to " + (add? "add" : "remove") + " you " + (add? "to" : "from") + " the " + listname + " list.
\r\n"; + msg += "The error from Moira was: " + m.getMessage() + "

"; + msg += "\r\n"; + return (msg); + } finally { + try { + if (mc != null) mc.disconnect(); + } catch (Exception e) { + } + } + if (add) msg = "You have been added to the " + listname + " list."; + else msg = "You have been removed from the " + listname + " list."; + return (msg); + } + + String do_removemembers(Hashtable qv, HttpServletRequest request, HttpServletResponse response) throws AuthenticationError { + Delmember [] del = null; + String msg = null; + String arg = (String) qv.get("list"); + if (arg == null || arg.equals("")) { + msg = "

Argument is required

"; + return (msg); + } + + String kname = do_authentication(request); + + HttpSession session = request.getSession(true); + + MoiraConnect mc = null; + try { + mc = connect(); + mc.proxy(kname); + Member [] members = mc.get_members_of_list(arg); + if (members == null) { + return("

No such list or empty list

."); + } + del = new Delmember[members.length]; + for (int i = 0; i < members.length; i++) { + del[i] = new Delmember(members[i]); + } + mc.disconnect(); + mc = null; + session.putValue("list", arg); + session.removeValue("members"); // In case left over from previous call + session.putValue("delmembers", del); + } catch (MoiraException m) { + try { + msg = "

\r\n"; + msg += m.getMessage(); + msg += "

\r\n"; + msg += "\r\n"; + } catch (Exception e) { + e.printStackTrace(); + } + } finally { + if (mc != null) mc.disconnect(); + } + return (do_remdisplay(qv, request, response, del, 0)); + } + + String do_remdisplay(Hashtable qv, HttpServletRequest request, HttpServletResponse response, Delmember [] del, int offset) { + // Offset = -1 means show everybody + boolean showall = false; + if (offset < 0) { + showall = true; + offset = 0; + } + String msg = "

Select Members to Delete

\r\n"; + if (((offset > 0) || del.length > MAXDISPLAY) && !showall) + msg += "

Note: Selections are remembered when you select the \"Previous\" and \"Next Buttons\".

\r\n"; + msg += "
\r\n"; + msg += "\r\n"; + msg += "\r\n"; + if (offset > 0) + msg += ""; + else + msg += ""; + if (!showall && (del.length > MAXDISPLAY)) + msg += ""; + else msg += ""; + if (((offset + MAXDISPLAY) < del.length) && !showall) + msg += "\r\n"; + else msg += "\r\n"; + msg += "
\r\n"; + msg += "\r\n"; + int len; + if (showall) len = del.length; + else len = offset + MAXDISPLAY; + if (del.length < len) + len = del.length; + for (int i = offset; i < len; i++) { + msg += "\r\n"; + } + msg += "
"; + msg += "" + del[i].member.getMemberId() + "
          
     
\r\n"; + HttpSession session = request.getSession(false); + session.putValue("offset", new Integer(offset)); + return (msg); + } + + + String do_delmember(Hashtable qv, HttpServletRequest request, HttpServletResponse response) throws AuthenticationError { + String kname = do_authentication(request); + + HttpSession session = request.getSession(false); // Better be one + if (session == null) { + return("

Could not proceed, has it been 30 minutes since you last interaction. If so, back up and try again.

"); + } + + Delmember [] del = (Delmember []) session.getValue("delmembers"); + if (del == null) + return("

Could not proceed, membership list doesn't exist, this should not happen!

"); + Integer OffsetO = (Integer) session.getValue("offset"); + if (OffsetO == null) { // Hmmm.... + OffsetO = new Integer(0); + } + + int offset = OffsetO.intValue(); + + String dodel = (String)qv.get("dodel"); // This is the submit button + if (dodel == null) + return("

Could not proceed, submit button incorrect, shouldn't happen.

"); + + // Process the selections on this form + + int last = offset + MAXDISPLAY; + if (last > del.length) last = del.length; + for (int i = offset; i < last; i++) // Clear marked bits for displayed + del[i].marked = false; // entries + + String selected = (String)qv.get("selected"); + if (selected == null) selected = ""; // None selected + + StreamTokenizer tk = new StreamTokenizer(new StringReader(selected)); + try { + tk.parseNumbers(); + while (tk.nextToken() != StreamTokenizer.TT_EOF) { + if (tk.ttype != StreamTokenizer.TT_NUMBER) continue; + int v = (int) tk.nval; + del[v].marked = true; + } + } catch (IOException e) { + e.printStackTrace(); + return("

Exception while processing...

"); + } + + if (dodel.equals("Next")) + return(do_remdisplay(qv, request, response, del, offset + MAXDISPLAY)); + if (dodel.equals("Previous")) + return(do_remdisplay(qv, request, response, del, offset - MAXDISPLAY)); + if (dodel.equals("Show All")) + return(do_remdisplay(qv, request, response, del, -1)); + if (!dodel.equals("Delete Selected")) + return("

Cannot proceed, bad submit value, should not happen

"); + // At this point we are going to display a list of who will be + // deleted and offer a confirmation. + + String listname = (String) session.getValue("list"); + if (listname == null) { + return("

Could not find list name (shouldn't happen).

"); + } + + // Check to see if anyone will be removed + + boolean havesome = false; + for (int i = 0; i < del.length; i++) { + if (del[i].marked) { + havesome = true; + break; + } + } + if (!havesome) + return("No members selected to be removed"); + + // Some will be, so throw up confirmation dialog + + String msg = "

Please Confirm Deletion of:

\r\n"; + msg += "\r\n"; + for (int i = 0; i < del.length; i++) { + if (del[i].marked) { + msg += ""; + msg += "\r\n"; + } + } + msg += "
" + del[i].member.getMemberType() + "" + del[i].member.getMemberId() + "
\r\n"; + msg += "
\r\n"; + msg += "\r\n"; + msg += "\r\n"; + msg += "
\r\n"; + return (msg); + } + + String do_delconfirm(Hashtable qv, HttpServletRequest request, HttpServletResponse response) throws AuthenticationError { + + String kname = do_authentication(request); + + String msg = ""; + + HttpSession session = request.getSession(false); // Better be one + if (session == null) { + return("

Could not proceed, has it been 30 minutes since you last interaction. If so, back up and try again.

"); + } + + String listname = (String)session.getValue("list"); + + Delmember [] del = (Delmember []) session.getValue("delmembers"); + if (del == null) + return("

Cannot proceed, nothing to delete, shouldn't happen!

"); + + Vector problems = new Vector(); + Vector success = new Vector(); + + MoiraConnect mc = null; + try { + mc = connect(); + mc.proxy(kname); + for (int i = 0; i < del.length; i++) { + try { + if (del[i].marked) { + mc.delete_member_from_list(listname, del[i].member.getMemberType(), del[i].member.getMemberId()); + success.addElement(del[i].member); + } + } catch (MoiraException m) { + problems.addElement("" + del[i].member.getMemberId() + "" + m.getMessage() + "\r\n"); + } + } + } catch (MoiraException m) { + msg = "

" + m.getMessage() + "

"; + msg += "\r\n"; + return (msg); + } finally { + try { + if (mc != null) mc.disconnect(); + } catch (Exception e) { + } + } + + msg = ""; + if (success.size() > 0) { + msg += "

Deleted the following:

\r\n"; + msg += "\r\n"; + for (int i = 0; i < success.size(); i++) { + msg += "\r\n"; + } + msg += "
" + ((Member)success.elementAt(i)).getMemberId() + "
\r\n"; + } + if (problems.size() > 0) { + msg += "

There were difficulties removing:

\r\n"; + msg += "\r\n"; + for (int i = 0; i < problems.size(); i++) { + msg += (String)problems.elementAt(i); + } + msg += "
\r\n"; + } + + return (msg); + } + + String do_showlistinfo(Hashtable qv, HttpServletRequest request, HttpServletResponse response) throws AuthenticationError { + + String kname = do_authentication(request); + + String list = (String)qv.get("list"); + + if (list == null || list.equals("")) { + return("Hmmm, no list specified"); + } + + HttpSession session = request.getSession(true); + + String msg = ""; + MoiraConnect mc = null; + ListInfo li = null; + try { + mc = connect(); + mc.proxy(kname); + li = mc.get_list_info(list); + } catch (MoiraException m) { + msg += "Error getting list info: " + m.getMessage(); + msg += "\r\n"; + return (msg); + } finally { + try { + if (mc != null) mc.disconnect(); + } catch (Exception e) { + } + } + if (li == null) { + return("Did not find list info."); + } + msg += "List: " + list + "
\r\n"; + msg += "Description: " + descript(li.description) + "
\r\n"; + if (li.maillist) + msg += "This list is a mailing list.
\r\n"; + if (li.grouplist) + msg += "This list is a Group and its ID number is " + li.gid + ".
\r\n"; + msg += "The Administrator of this list is the " + li.ace_type + ": " + li.ace_name + ".
\r\n"; + msg += "This list is: " + mkflags(li) + ".
\r\n"; + msg += "Last modification by " + li.moduser + " at " + df.format(li.modtime) + " with " + li.modwith + ".
\r\n"; + return (msg); + } + + String do_updatelistinfo(Hashtable qv, HttpServletRequest request, HttpServletResponse response) throws AuthenticationError { + + String kname = do_authentication(request); + + String list = (String)qv.get("list"); + + if (list == null || list.equals("")) { + return("Hmmm, no list specified"); + } + + HttpSession session = request.getSession(true); + + String msg = ""; + MoiraConnect mc = null; + ListInfo li = null; + try { + mc = connect(); + mc.proxy(kname); + li = mc.get_list_info(list); + } catch (MoiraException m) { + msg += "Error getting list info: " + m.getMessage(); + msg += "\r\n"; + return (msg); + } finally { + try { + if (mc != null) mc.disconnect(); + } catch (Exception e) { + } + } + if (li == null) { + return("Did not find list info."); + } + + session.putValue("listinfo", li); // Save for the confirmation call + session.putValue("list", list); // Save for side bar update + session.removeValue("members"); // In case left over... + + msg += "
\r\n"; + msg += "\r\n"; + msg += "Update characteristics of list " + list + "

\r\n"; + msg += "Is this list a maillist? Yes No
\r\n"; + // Note: We don't update group information here + + msg += "Is this list a public list? Yes No
\r\n"; + msg += "Is this list a hidden list? Yes No

\r\n"; + + msg += "The Administrator for this list is
\r\n"; + msg += ""; + msg += "
\r\n"; + msg += "Description:
\r\n"; + msg += "
\r\n"; + msg += "

\r\n"; + return (msg); + } + + String do_updatelistinfoconf(Hashtable qv, HttpServletRequest request, HttpServletResponse response) throws AuthenticationError { + + String kname = do_authentication(request); + HttpSession session = request.getSession(false); // Better be one + if (session == null) { + return("

Could not proceed, has it been 30 minutes since you last interaction. If so, back up and try again.

"); + } + + ListInfo li = (ListInfo)session.getValue("listinfo"); + if (li == null) { + return("

Could not proceed, could not find orignial list info, should not happen!

"); + } + + String tmp = (String)qv.get("description"); + if (tmp != null) li.description = tmp; + tmp = (String)qv.get("maillist"); + if (tmp != null) { + if (tmp.equals("1")) li.maillist = true; + else li.maillist = false; + } + tmp = (String)qv.get("public"); + if (tmp != null) { + if (tmp.equals("1")) li.bpublic = true; + else li.bpublic = false; + } + tmp = (String)qv.get("hidden"); + if (tmp != null) { + if (tmp.equals("1")) li.hidden = true; + else li.hidden = false; + } + tmp = (String)qv.get("ace_type"); + if (tmp != null) + li.ace_type = tmp; + + tmp = (String)qv.get("ace_name"); + if (tmp != null) + li.ace_name = tmp; + + MoiraConnect mc = null; + try { + mc = connect(); + mc.proxy(kname); + mc.update_list_info(li.name, li); + mc.disconnect(); + mc = null; + } catch (MoiraException e) { + String msg; + if (e.getMessage().startsWith("No such list")) { + msg = "

Error updating " + li.name + ".
\r\n"; + msg += "The list you specified as administrator does not exist.

\r\n"; + } else if(e.getMessage().startsWith("No such user")) { + msg = "

Error updating " + li.name + ".
\r\n"; + msg += "The user you specified as administrator is not known to Moira.

\r\n"; + } else + msg = "

Error during update of " + li.name + ": " + e.getMessage() + "

"; + msg += "\r\n"; + return (msg); + } finally { + try { + if (mc != null) mc.disconnect(); + } catch (Exception e) { + } + } + String msg = "Update of " + li.name + " succeeded

\r\n"; + qv.put("list", li.name); + msg += do_showlistinfo(qv, request, response); + return (msg); + } + + String do_addfinal(Hashtable qv, HttpServletRequest request, HttpServletResponse response) throws AuthenticationError { + HttpSession session = request.getSession(false); // Better be one + if (session == null) { + return("

Could not proceed, has it been 30 minutes since you last interaction. If so, back up and try again.

"); + } + + String kname = do_authentication(request); + + String listname = (String) session.getValue("list"); + if (listname == null) { + return("

Unable to find list name! (shouldn't happen).

"); + } + String msg = ""; + String member = (String)qv.get("member"); + String type = (String)qv.get("type"); + if (member == null || type == null || member.equals("")) { + return("

No names selected to be added!

"); + } + MoiraConnect mc = null; + StreamTokenizer tk = null; + msg += "\r\n"; + boolean addheader = false; + Vector warnings = new Vector(); + Vector problems = new Vector(); + try { + mc = connect(); + mc.proxy(kname); + tk = new StreamTokenizer(new StringReader(member)); + tk.wordChars('@', '@'); + tk.wordChars('0', '9'); + tk.wordChars('_', '_'); + while (tk.nextToken() != StreamTokenizer.TT_EOF) { + if (tk.ttype != StreamTokenizer.TT_WORD) continue; + try { + + // Pre-process user input + String [] user = canonicalize(tk.sval, type); + + mc.add_member_to_list(listname, user[1], user[0]); + if (!addheader) { + msg += "\r\n"; + addheader = true; + } + msg += "\r\n"; + if (user.length > 2) + warnings.addElement(user[2]); + } catch (MoiraException e) { + String err = "\r\n"; + problems.addElement(err); + } + } + if (!addheader) // We didn't seem to be able to add anyone + msg += "\r\n"; + msg += "
Added to list
" + user[1] + "" + user[0] + "
" + tk.sval + "" + e.getMessage() + "
No one added!
\r\n"; + mc.disconnect(); + mc = null; + } catch (MoiraException e) { + if (tk != null) + msg = "

Error adding " + tk.sval + ": " + e.getMessage() + "

"; + else + msg = "

Error during add: " + e.getMessage() + "

"; + msg += "\r\n"; + return (msg); + } catch (IOException e) { + e.printStackTrace(); // Shouldn't happen + } finally { + try { + if (mc != null) mc.disconnect(); + } catch (Exception e) { + } + } + if (problems.size() != 0) { + msg += "

There were difficulties adding the following users:
\r\n"; + msg += "\r\n"; + for (int i = 0; i < problems.size(); i++) + msg += problems.elementAt(i); + msg += "
\r\n"; + } + if (warnings.size() != 0) { + msg += "

The following warnings were generated:
\r\n"; + msg += "\r\n"; + for (int i = 0; i < warnings.size(); i++) + msg += "\r\n"; + msg += "
" + warnings.elementAt(i) + "
\r\n"; + } + return (msg); + } + + String getfile(String operation) { + ResourceBundle bd = null; + try { + bd = ResourceBundle.getBundle("mit.moira.FileParts"); + } catch (java.util.MissingResourceException e) { + return("Cannot find FileParts.properties"); + } + if (bd == null) return("Cannot find FileParts.properties!"); + String filename = null; + try { + filename = bd.getString(operation); + } catch (java.util.MissingResourceException e) { + return("Cannot find file for operation: " + operation); + } + if (filename == null) { + return("Cannot find file for operation: " + operation); + } + File file = new File(filename); + if (!file.isFile()) return("Cannot file file: " + filename); + long filemodtime = file.lastModified(); + Long cachemodtime = (Long) FileTimes.get(filename); + if (cachemodtime == null || filemodtime > cachemodtime.longValue()) { + // Need to fetch the file into the cache + byte [] buffer = null; + try { + BufferedInputStream input = new BufferedInputStream(new FileInputStream(filename)); + buffer = new byte[input.available()]; + input.read(buffer); + input.close(); + } catch (FileNotFoundException f) { + // Should never happen given check above + } catch (IOException e) { + return("IO Error Reading: " + filename); + } + String data = new String(buffer); + data += "\r\n\r\n"; + FileTimes.put(filename, new Long(filemodtime)); + FileParts.put(filename, data); + return (data); + } else { + return ((String)FileParts.get(filename)); + } + } + + String mkflags(ListInfo li) { + String retval = ""; + int state = 0; + if (!li.hidden) { + retval = "visible"; + state = 1; + } + if (!li.bpublic) { + switch (state) { + case 0: + retval = "private"; + state = 1; + break; + case 1: + retval = "private and " + retval; + state = 2; + break; + case 2: + retval = "private, " + retval; + } + } + if (li.active) { + switch (state) { + case 0: + retval = "active"; + state = 1; + break; + case 1: + retval = "active and " + retval; + state = 2; + break; + case 2: + retval = "active, " + retval; + } + } + return (retval); + } + + /** + * pre-process user input. Mostly this catches cases where the + * incorrect type is used to add someone to a list. + * + * @param user String or Userid being proposed for an add + * @param type Either LIST,STRING or USER + * @return an array of elements. The first is the user portion followed by the type and an optional warning message. + */ + + private String [] canonicalize(String user, String type) throws MoiraException { + int i; + String [] retval = null; + if (type.equals("STRING")) { + i = user.indexOf('@'); + if (i != -1) { + String host = user.substring(i + 1); + if (host.equalsIgnoreCase("mit.edu")) { + retval = new String[3]; + retval[0] = user.substring(0, i); + retval[1] = "USER"; + retval[2] = "Converted " + user + " to userid " + retval[0]; + return (retval); + } + // Don't convert + retval = new String[2]; + retval[0] = user; + retval[1] = "STRING"; + return (retval); + } else { // No @ sign + throw new MoiraException("STRING (mailing list entries) must have an \'@\' in them!"); + } + } else if (type.equals("USER")) { + i = user.indexOf('@'); + if (i != -1) { + String host = user.substring(i + 1); + if (host.equalsIgnoreCase("mit.edu")) { + retval = new String[3]; + retval[0] = user.substring(0, i); + retval[1] = "USER"; + retval[2] = "Converted " + user + " to userid " + retval[0]; + return (retval); + } + throw new MoiraException("USER types must not have \'@\'s in them!"); + } + retval = new String[2]; + retval[0] = user; + retval[1] = type; + return (retval); + } else if (type.equals("LIST")) { + i = user.indexOf('@'); + if (i == -1) { // No '@' sign, just process normally + retval = new String[2]; + retval[0] = user; + retval[1] = type; + return (retval); + } else { + String host = user.substring(i + 1); + if (host.equalsIgnoreCase("mit.edu")) { // trim mit.edu + retval = new String[3]; + retval[0] = user.substring(0, i); + retval[1] = "LIST"; + retval[2] = "Converted " + user + " to list named " + retval[0]; + return (retval); + } else + throw new MoiraException("LIST types may not contain \'@\'s in them!"); + } + } else { // Just pass through everything else for now + retval = new String[2]; + retval[0] = user; + retval[1] = type; + return (retval); + } + } + + /** + * Remove <SCRIPT> tags from input String + * + * @param input String to check out + * @return String with <SCRIPT> tag removed + */ + private String descript(String input) { + String lc = input.toLowerCase(); + int i = lc.indexOf(" 8)) { + if (retval != null) { + try { + retval.disconnect(); + } catch (Exception e) { + } + } + throw m; // Re-throw if not the error we expect or we are looping + } + } + if (retval != null) { + try { + retval.disconnect(); + } catch (Exception e) { + } + retval = null; + } + System.err.println("MoiraServlet: Forced Renewal..."); + kt.renew(); // Renew tickets + } + return (null); + } + +} + +class Delmember { + boolean marked = false; + Member member = null; + + Delmember(Member m) { + member = m; + } +} diff --git a/webmoira/moirai.c b/webmoira/moirai.c new file mode 100644 index 00000000..31c0c3e5 --- /dev/null +++ b/webmoira/moirai.c @@ -0,0 +1,298 @@ +#include "mit_moira_MoiraConnectInternal.h" +#include +#include +#include +#include +#include + +static void throwMoiraException(JNIEnv *env, int code) +{ + char buffer[1024]; + jmethodID mid; + jobject tothrow; + jclass IOE; + jstring jmess; + + IOE = (*env)->FindClass(env, "mit/moira/MoiraException"); + if (IOE == NULL) { + fprintf(stderr, "moirai: No Class\n"); + goto die; + } + mid = (*env)->GetMethodID(env, IOE, "", "(Ljava/lang/String;I)V"); + if (mid == NULL) { + fprintf(stderr, "moirai: No Method\n"); + goto die; + } + sprintf(buffer, "%s", error_message(code)); + jmess = (*env)->NewStringUTF(env, buffer); + if (jmess == NULL) { + fprintf(stderr, "Cannot get new string\n"); + goto die; + } + tothrow = (*env)->NewObject(env, IOE, mid, jmess, (jint) code); + if (tothrow == NULL) { + fprintf(stderr, "moirai: No Throw\n"); + goto die; + } + (*env)->Throw(env, (jthrowable) tothrow); + return; + die: + abort(); + return; +} + +static void throwMoiraExceptionMess(JNIEnv *env, char *mess) +{ + jclass IOE = (*env)->FindClass(env, "mit/moira/MoiraException"); + if (IOE == NULL) abort(); + (*env)->ThrowNew(env, IOE, mess); + return; +} + +JNIEXPORT void JNICALL Java_mit_moira_MoiraConnectInternal_connect(JNIEnv *env, + jclass Class, jstring server) { + int status; + char buffer[1024]; + const char *aserver = (*env)->GetStringUTFChars(env, server, 0); + if (strlen(aserver) > sizeof(buffer)) abort(); + strcpy(buffer, aserver); + (*env)->ReleaseStringUTFChars(env, server, aserver); + status = mr_connect(buffer); + if (status != MR_SUCCESS) throwMoiraException(env, status); + status = mr_version(2); + if (status != MR_SUCCESS) { + if (status == MR_VERSION_LOW) return; /* This is OK */ + else { + mr_disconnect(); + throwMoiraException(env, status); + } + } + return; +} + +JNIEXPORT void JNICALL Java_mit_moira_MoiraConnectInternal_proxy(JNIEnv *env, + jclass Class, + jstring jUser) { + int status; + char buffer[1024]; + const char *user = (*env)->GetStringUTFChars(env, jUser, 0); + if (strlen(user) > sizeof(buffer)) abort(); + strcpy(buffer, user); + (*env)->ReleaseStringUTFChars(env, jUser, user); + status = mr_proxy(buffer, "Java"); + if (status != MR_SUCCESS) throwMoiraException(env, status); + return; +} + + +JNIEXPORT void JNICALL Java_mit_moira_MoiraConnectInternal_auth(JNIEnv *env, + jclass Class) { + int status; + status = mr_auth("JavaInterface"); + if (status != MR_SUCCESS) throwMoiraException(env, status); + return; +} + +JNIEXPORT void JNICALL Java_mit_moira_MoiraConnectInternal_disconnect + (JNIEnv *env, jclass Class) { + mr_disconnect(); + return; +} + +typedef struct ArgBlock { + int alloccount; + int count; + char **stringval; +} ArgBlock; + +typedef struct ListBlock { + int alloccount; + int count; + ArgBlock **Args; +} ListBlock; + +static jobjectArray ArgBlockToJavaArray(JNIEnv *env, ArgBlock *argb); +static jobjectArray ListBlockToJavaArray(JNIEnv *env, ListBlock *lb); + +extern int mr_query(char *name, int argc, char **argv, + int (*proc)(int, char **, void *), void *hint); +static ListBlock *NewListBlock(); +static ArgBlock *NewArgBlock(); +static void FreeListBlock(ListBlock *); +static int StashResults(int argc, char **argv, void *callback); +static char **ConvertJavaArgs(JNIEnv *env, jobjectArray jargs); +static void FreeArgs(char **args); + +JNIEXPORT jobjectArray JNICALL Java_mit_moira_MoiraConnectInternal_mr_1query + (JNIEnv *env, jclass Class, jstring jcommand, jobjectArray jargs) { + ListBlock *listblock = NewListBlock(); + jobjectArray retval; + int status; + const char *command; + char icommand[1024]; + char **args = ConvertJavaArgs(env, jargs); + if (args == NULL) return (NULL); /* It probably thru an exception */ + command = (*env)->GetStringUTFChars(env, jcommand, 0); + strncpy(icommand, command, sizeof(icommand)); + + status = mr_query(icommand, (*env)->GetArrayLength(env, jargs), args, + StashResults, listblock); + FreeArgs(args); /* Don't need them anymore */ + if (status != MR_SUCCESS) { + FreeListBlock(listblock); + throwMoiraException(env, status); + return (0); + } + /* if (listblock->count == 0) { / * No such list or empty list * / + FreeListBlock(listblock); + throwMoiraExceptionMess(env, "No Such List or Empty List"); + return(0); + } */ + + if (listblock->count == 0) return (0); + + retval = ListBlockToJavaArray(env, listblock); + FreeListBlock(listblock); + return (retval); + +} + +static ArgBlock *NewArgBlock() { + ArgBlock *argb = (ArgBlock *)malloc(sizeof(ArgBlock)); + argb->alloccount = 10; + argb->stringval = (char **) malloc(10 * sizeof(char *)); + argb->count = 0; + return (argb); +} + +static void AddArg(ArgBlock *argb, char *arg) { + int i; + if (argb->alloccount <= (argb->count + 1)) { + char **st = malloc((2 * argb->alloccount) * sizeof(char *)); + argb->alloccount *= 2; + for (i = 0; i < argb->count; i++) + st[i] = argb->stringval[i]; + free(argb->stringval); + argb->stringval = st; + } + argb->stringval[argb->count++] = arg; +} + +static ListBlock *NewListBlock() { + ListBlock *list = (ListBlock *)malloc(sizeof(ListBlock)); + list->alloccount = 10; + list->Args = (ArgBlock **) malloc(10 * sizeof(ArgBlock *)); + list->count = 0; + return (list); +} + +static void FreeArgBlock(ArgBlock *argb) { + int i; + for (i = 0; i < argb->count; i++) free(argb->stringval[i]); + free(argb->stringval); + free(argb); +} + +static void FreeListBlock(ListBlock *list) { + int i; + for (i = 0; i < list->count; i++) FreeArgBlock(list->Args[i]); + free(list->Args); + free(list); +} + +static jobjectArray ArgBlockToJavaArray(JNIEnv *env, ArgBlock *argb) { + jobjectArray retval; + int i; + retval = (*env)->NewObjectArray(env, argb->count, + (*env)->FindClass(env, "java/lang/String"), + NULL); + for (i = 0; i < argb->count; i++) { + (*env)->SetObjectArrayElement(env, retval, i, + (*env)->NewStringUTF(env, argb->stringval[i])); + } + return (retval); +} + +static jobjectArray ListBlockToJavaArray(JNIEnv *env, ListBlock *lb) { + jobjectArray retval; + int i; + retval = (*env)->NewObjectArray(env, lb->count, + (*env)->FindClass(env, "java/lang/Object"), + NULL); + for (i = 0; i < lb->count; i++) + (*env)->SetObjectArrayElement(env, retval, i, + ArgBlockToJavaArray(env, lb->Args[i])); + return (retval); +} + +static int StashResults(int argc, char **argv, void *callback) { + ListBlock *list = (ListBlock *) callback; + ArgBlock *argb; + char *arg; + int i; + + /* printf("DEBUG: StashResults argc = %d\n", argc); + * for (i = 0; i < argc; i++) + * printf("DEBUG: StashResults argv[%d] = %s\n", i, argv[i]); + */ + + while (list->alloccount <= (list->count + 1)) { + ArgBlock **args = (ArgBlock **) malloc(list->alloccount * 2 * sizeof(char *)); + list->alloccount = list->alloccount * 2; + for(i = 0; i < list->count; i++) { + args[i] = list->Args[i]; + } + free(list->Args); + list->Args = args; + } + + argb = NewArgBlock(); + + for (i = 0; i < argc; i++) { + arg = (char *) malloc(strlen(argv[i]) + 1); + strcpy(arg, argv[i]); + AddArg(argb, arg); + } + list->Args[list->count++] = argb; + return (0); +} + +static char **ConvertJavaArgs(JNIEnv *env, jobjectArray jargs) { + char **retval; + int i, j; + const char *iarg; + int length = (*env)->GetArrayLength(env, jargs); + if (length == 0) { /* Does this happen in a non-error situation? */ + retval = (char **) malloc(sizeof (char *)); + retval[0] = NULL; + return(retval); + } + retval = (char **) malloc((length + 1) * sizeof(char *)); + for (i = 0; i < length; i++) { + jobject jarg = (*env)->GetObjectArrayElement(env, jargs, i); + if ((*env)->ExceptionOccurred(env)) { + for (j = 0; j < i; j++) free(retval[j]); + free (retval); + return (NULL); + } + iarg = (*env)->GetStringUTFChars(env, jarg, 0); + if ((*env)->ExceptionOccurred(env)) { + for (j = 0; j < i; j++) free(retval[j]); + free (retval); + return (NULL); + } + retval[i] = malloc(strlen(iarg) + 1); + strcpy(retval[i], iarg); + (*env)->ReleaseStringUTFChars(env, jarg, iarg); + } + retval[length] = NULL; + return (retval); +} + +static void FreeArgs(char **args) { + int i; + i = 0; + while (args[i] != NULL) free(args[i++]); + free(args); +} + diff --git a/webmoira/request.jhtml b/webmoira/request.jhtml new file mode 100644 index 00000000..5964c147 --- /dev/null +++ b/webmoira/request.jhtml @@ -0,0 +1,106 @@ + + + + +request a list + + + + + + + + + + + + + + + +
+ homelist + management services: request a list
+ + select an + option: +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + display + list characteristics
+ + show list members
+ + add + or remove yourself from a list
+ list owners may
+ + update + list characteristics
+ + add + list members
+ + remove + list members
+ enter a list name:
+ + +
+ + request + a list +

help +

+
+ Forgot a username?
Check the MIT Directory:

+ + +
+ How to use the MIT Directory
+
+

+

+
+ + + + +
+

 

+ + diff --git a/webmoira/showresult.jhtml b/webmoira/showresult.jhtml new file mode 100644 index 00000000..e7f34882 --- /dev/null +++ b/webmoira/showresult.jhtml @@ -0,0 +1,109 @@ + + + + + + +<servlet code="mit.moira.MoiraServlet"><param name=modifier value=displayonly></servlet> + + + + + + + + + + + +
+ homeathena list + management:
+ + select an + option: +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + display + list characteristics
+ + show list members
+ + add + or remove yourself from a list
+ list owners may
+ + update + list characteristics
+ + add + list members
+ + remove + list members
+ enter a list name:
+ + +
+ + request + a new list +

help +

+
+ Forgot a username?
Check the MIT Directory:

+ + +
+ How to use the MIT Directory
+
+

+

+
+ + + +
+ + + + + +
+

 

+ +