diff --git a/projects/samskivert/src/java/com/samskivert/servlet/Makefile b/projects/samskivert/src/java/com/samskivert/servlet/Makefile new file mode 100644 index 00000000..2a1361b8 --- /dev/null +++ b/projects/samskivert/src/java/com/samskivert/servlet/Makefile @@ -0,0 +1,9 @@ +# +# $Id: Makefile,v 1.1 2001/03/02 01:21:06 mdb Exp $ + +ROOT = ../../.. + +SRCS = \ + RedirectException.java \ + +include $(ROOT)/build/Makefile.java diff --git a/projects/samskivert/src/java/com/samskivert/servlet/RedirectException.java b/projects/samskivert/src/java/com/samskivert/servlet/RedirectException.java new file mode 100644 index 00000000..6f7a7384 --- /dev/null +++ b/projects/samskivert/src/java/com/samskivert/servlet/RedirectException.java @@ -0,0 +1,25 @@ +// +// $Id: RedirectException.java,v 1.1 2001/03/02 01:21:06 mdb Exp $ + +package com.samskivert.servlet; + +/** + * A redirect exception is thrown by servlet services when they require + * that the user be redirected to a different URL rather than continue + * processing this request. It is expected that redirect handling can be + * implemented in a single place such that servlets can simply allow this + * exception to propagate up to the proper handler which will then issue + * the appropriate redirect header. + */ +public class RedirectException extends Exception +{ + public RedirectException (String url) + { + super(url); + } + + public String getRedirectURL () + { + return getMessage(); + } +} diff --git a/projects/samskivert/src/java/com/samskivert/servlet/user/InvalidUsernameException.java b/projects/samskivert/src/java/com/samskivert/servlet/user/InvalidUsernameException.java new file mode 100644 index 00000000..033cb8ab --- /dev/null +++ b/projects/samskivert/src/java/com/samskivert/servlet/user/InvalidUsernameException.java @@ -0,0 +1,12 @@ +// +// $Id: InvalidUsernameException.java,v 1.1 2001/03/02 01:21:06 mdb Exp $ + +package com.samskivert.servlet.user; + +public class InvalidUsernameException extends Exception +{ + public InvalidUsernameException (String message) + { + super(message); + } +} diff --git a/projects/samskivert/src/java/com/samskivert/servlet/user/Makefile b/projects/samskivert/src/java/com/samskivert/servlet/user/Makefile new file mode 100644 index 00000000..362aa764 --- /dev/null +++ b/projects/samskivert/src/java/com/samskivert/servlet/user/Makefile @@ -0,0 +1,13 @@ +# +# $Id: Makefile,v 1.1 2001/03/02 01:21:06 mdb Exp $ + +ROOT = ../../../.. + +SRCS = \ + InvalidUsernameException.java \ + User.java \ + UserExistsException.java \ + UserManager.java \ + UserRepository.java \ + +include $(ROOT)/build/Makefile.java diff --git a/projects/samskivert/src/java/com/samskivert/servlet/user/User.java b/projects/samskivert/src/java/com/samskivert/servlet/user/User.java new file mode 100644 index 00000000..ce3687a5 --- /dev/null +++ b/projects/samskivert/src/java/com/samskivert/servlet/user/User.java @@ -0,0 +1,66 @@ +// +// $Id: User.java,v 1.1 2001/03/02 01:21:06 mdb Exp $ + +package com.samskivert.servlet.user; + +import java.sql.Date; + +/** + * A user object contains information about a registered user in our web + * application environment. Users are stored in the user repository (a + * database table) and loaded during a request based on authentication + * information provided with the request headers. + * + *

Note: Do not modify any of the fields of this object + * diretly. Use the set methods to make updates. If no set + * methods exist, you shouldn't be modifying that field. + */ +public class User +{ + /** The user's assigned integer userid. */ + public int userid; + + /** The user's chosen username. */ + public String username; + + /** The date this record was created. */ + public Date created; + + /** + * The user's real name (first, last and whatever else they opt to + * provide). + */ + public String realname; + + /** The user's chosen password (encrypted). */ + public String password; + + /** The user's email address. */ + public String email; + + /** + * Updates the user's real name. + */ + public void setRealName (String realname) + { + this.realname = realname; + } + + /** + * Updates the user's password. + * + * @param password The user's new (unencrypted) password. + */ + public void setPassword (String password) + { + this.password = UserRepository.encryptPassword(username, password); + } + + /** + * Updates the user's email address. + */ + public void setEmail (String email) + { + this.email = email; + } +} diff --git a/projects/samskivert/src/java/com/samskivert/servlet/user/UserExistsException.java b/projects/samskivert/src/java/com/samskivert/servlet/user/UserExistsException.java new file mode 100644 index 00000000..e3a33e7c --- /dev/null +++ b/projects/samskivert/src/java/com/samskivert/servlet/user/UserExistsException.java @@ -0,0 +1,12 @@ +// +// $Id: UserExistsException.java,v 1.1 2001/03/02 01:21:06 mdb Exp $ + +package com.samskivert.servlet.user; + +public class UserExistsException extends Exception +{ + public UserExistsException (String message) + { + super(message); + } +} diff --git a/projects/samskivert/src/java/com/samskivert/servlet/user/UserManager.java b/projects/samskivert/src/java/com/samskivert/servlet/user/UserManager.java new file mode 100644 index 00000000..e84d4243 --- /dev/null +++ b/projects/samskivert/src/java/com/samskivert/servlet/user/UserManager.java @@ -0,0 +1,162 @@ +// +// $Id: UserManager.java,v 1.1 2001/03/02 01:21:06 mdb Exp $ + +package com.samskivert.servlet.user; + +import java.net.URLEncoder; +import java.sql.SQLException; +import java.util.Properties; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpUtils; + +import com.samskivert.Log; +import com.samskivert.servlet.RedirectException; +import com.samskivert.util.Interval; +import com.samskivert.util.IntervalManager; +import com.samskivert.util.StringUtil; + +/** + * The user manager provides easy access to user objects for servlets. It + * takes care of cookie management involved in login, logout and loading a + * user record during an authenticated session. + */ +public class UserManager +{ + /** + * A user manager creates a user repository through which to load and + * save user records. The properties needed to configure the user + * repository must be provided to the user manager at construct time. + * + *

Presently the user manager requires the following configuration + * information: + *

+ * + * @see UserRepository#UserRepository + */ + public UserManager (Properties props) + throws SQLException + { + // open up the user repository + _repository = new UserRepository(props); + + // fetch the login URL from the properties + _loginURL = props.getProperty("login_url"); + if (_loginURL == null) { + Log.warning("No login_url supplied in user manager config. " + + "Authentication won't work."); + } + + // register a cron job to prune the session table every hour + Interval pruner = new Interval() { + public void intervalExpired (int id, Object arg) + { + try { + _repository.pruneSessions(); + } catch (SQLException sqe) { + Log.warning("Error pruning session table: " + sqe); + } + } + }; + _prunerid = IntervalManager.register(pruner, SESSION_PRUNE_INTERVAL, + null, true); + } + + public void shutdown () + { + // shut down the user repository + try { + _repository.shutdown(); + } catch (SQLException sqe) { + Log.warning("Error shutting down user repository: " + sqe); + } + + // cancel our session table pruning thread + IntervalManager.remove(_prunerid); + } + + /** + * Fetches the necessary authentication information from the http + * request and loads the user identified by that information. + * + * @return the user associated with the request or null if no user was + * associated with the request or if the authentication information is + * bogus. + */ + public User loadUser (HttpServletRequest req) + throws SQLException + { + String authcode = getUserAuthCookie(req); + if (authcode != null) { + return _repository.getUserBySession(authcode); + } else { + return null; + } + } + + /** + * Fetches the necessary authentication information from the http + * request and loads the user identified by that information. If no + * user could be loaded (because the requester is not authenticated), + * a redirect exception will be thrown to redirect the user to the + * login page specified in the user manager configuration. + * + * @return the user associated with the request. + */ + public User requireUser (HttpServletRequest req) + throws SQLException, RedirectException + { + User user = loadUser(req); + // if no user was loaded, we need to redirect these fine people to + // the login page + if (user == null) { + // first construct the redirect URL + StringBuffer rurl = HttpUtils.getRequestURL(req); + String qs = req.getQueryString(); + if (qs != null) { + rurl.append("?").append(qs); + } + String eurl = URLEncoder.encode(rurl.toString()); + String target = StringUtil.replace(_loginURL, "%R", eurl); + throw new RedirectException(target); + } + return user; + } + + protected static String getUserAuthCookie (HttpServletRequest req) + { + Cookie[] cookies = req.getCookies(); + if (cookies == null) { + return null; + } + for (int i = 0; i < cookies.length; i++) { + if (cookies[i].getName().equals(USERAUTH_COOKIE)) { + return cookies[i].getValue(); + } + } + return null; + } + + protected UserRepository _repository; + protected int _prunerid = -1; + + protected String _loginURL; + + protected static final String USERAUTH_COOKIE = "id_"; + + /** Prune the session table every hour. */ + protected static final long SESSION_PRUNE_INTERVAL = 60L * 60L * 1000L; +} diff --git a/projects/samskivert/src/java/com/samskivert/servlet/user/UserRepository.java b/projects/samskivert/src/java/com/samskivert/servlet/user/UserRepository.java new file mode 100644 index 00000000..2a7f5c9a --- /dev/null +++ b/projects/samskivert/src/java/com/samskivert/servlet/user/UserRepository.java @@ -0,0 +1,260 @@ +// +// $Id: UserRepository.java,v 1.1 2001/03/02 01:21:06 mdb Exp $ + +package com.samskivert.servlet.user; + +import java.sql.*; +import java.util.Properties; + +import org.apache.regexp.*; + +import com.samskivert.Log; +import com.samskivert.jdbc.MySQLRepository; +import com.samskivert.jdbc.jora.*; +import com.samskivert.util.Crypt; + +public class UserRepository extends MySQLRepository +{ + /** + * Creates the repository and opens the user database. A properties + * object should be supplied with the following fields: + * + *
+     * driver=[jdbc driver class]
+     * url=[jdbc driver url]
+     * username=[jdbc username]
+     * password=[jdbc password]
+     * 
+ * + * @param props a properties object containing the configuration + * parameters for the repository. + */ + public UserRepository (Properties props) + throws SQLException + { + super(props); + } + + protected void createTables () + throws SQLException + { + // create our table object + _utable = new Table(User.class.getName(), "users", _session, + "userid"); + } + + /** + * Requests that a new user be created in the repository. + * + * @param username The username of the new user to create. Usernames + * must consist only of characters that match the following regular + * expression: [_A-Za-z0-9]+ and be longer than two + * characters. + * @param password The (unencrypted) password for the new user. + * @param realname The user's real name. + * @param email The user's email address. + * + * @return The userid of the newly created user. + */ + public int createUser (String username, String password, + String realname, String email) + throws InvalidUsernameException, UserExistsException, SQLException + { + // check minimum length + if (username.length() < MINIMUM_USERNAME_LENGTH) { + throw new InvalidUsernameException("error.username_too_short"); + } + + // check that it's only valid characters + if (!_userre.match(username)) { + throw new InvalidUsernameException("error.invalid_username"); + } + + // create a new user object and stick it into the database + final User user = new User(); + user.username = username; + user.password = encryptPassword(username, password); + user.realname = realname; + user.email = email; + user.created = new Date(System.currentTimeMillis()); + + try { + execute(new Operation () { + public void invoke () throws SQLException + { + _utable.insert(user); + // update the userid now that it's known + user.userid = lastInsertedId(); + } + }); + + } catch (SQLException sqe) { + if (isDuplicateRowException(sqe)) { + throw new UserExistsException("error.user_exists"); + } else { + throw sqe; + } + } + + return user.userid; + } + + /** + * Encrypts the user's password according to our preferred scheme. + */ + public static String encryptPassword (String username, String password) + { + return Crypt.crypt(username.substring(0, 2), password); + } + + /** + * Looks up a user by username. + * + * @return the user with the specified user id or null if no user with + * that id exists. + */ + public User getUser (String username) + throws SQLException + { + // look up the user + Cursor ec = _utable.select("where username = '" + username + "'"); + + // fetch the user from the cursor + User user = (User)ec.next(); + if (user != null) { + // call next() again to cause the cursor to close itself + ec.next(); + } + + return user; + } + + /** + * Looks up a user by userid. + * + * @return the user with the specified user id or null if no user with + * that id exists. + */ + public User getUser (int userid) + throws SQLException + { + // look up the user + Cursor ec = _utable.select("where userid = " + userid); + + // fetch the user from the cursor + User user = (User)ec.next(); + if (user != null) { + // call next() again to cause the cursor to close itself + ec.next(); + } + + return user; + } + + /** + * Looks up a user by a session identifier. + * + * @return the user associated with the specified session or null of + * no session exists with the supplied identifier. + */ + public User getUserBySession (String sessionKey) + throws SQLException + { + // look up the user + Cursor ec = _utable.select("sessions", + "where authcode = '" + sessionKey + + "' AND sessions.userid = users.userid"); + + // fetch the user from the cursor + User user = (User)ec.next(); + if (user != null) { + // call next() again to cause the cursor to close itself + ec.next(); + } + + return user; + } + + /** + * Updates a user that was previously fetched from the repository. + */ + public void updateUser (final User user) + throws SQLException + { + execute(new Operation () { + public void invoke () throws SQLException + { + _utable.update(user); + } + }); + } + + /** + * Removes the user from the repository. + */ + public void deleteUser (final User user) + throws SQLException + { + execute(new Operation () { + public void invoke () throws SQLException + { + _utable.delete(user); + } + }); + } + + /** + * Prunes any expired sessions from the sessions table. + */ + public void pruneSessions () + throws SQLException + { + execute(new Operation () { + public void invoke () throws SQLException + { + _session.execute("delete from sessions where " + + "expires >= CURRENT_DATE()"); + } + }); + } + + public static void main (String[] args) + { + Properties props = new Properties(); + props.put("driver", "org.gjt.mm.mysql.Driver"); + props.put("url", "jdbc:mysql://localhost:3306/samskivert"); + props.put("username", "www"); + props.put("password", "Il0ve2PL@Y"); + + try { + UserRepository rep = new UserRepository(props); + + System.out.println(rep.getUser("mdb")); + System.out.println(rep.getUserBySession("auth")); + + rep.createUser("samskivert", "foobar", "Michael Bayne", + "mdb@samskivert.com"); + rep.createUser("mdb", "foobar", "Michael Bayne", + "mdb@samskivert.com"); + + rep.shutdown(); + + } catch (Throwable t) { + t.printStackTrace(System.err); + } + } + + protected Table _utable; + + protected static final int MINIMUM_USERNAME_LENGTH = 2; + + /** Used to check usernames for invalid characteres. */ + protected static RE _userre; + static { + try { + _userre = new RE("^[_A-Za-z0-9]+$"); + } catch (RESyntaxException rese) { + Log.warning("Unable to initialize user regexp?! " + rese); + } + } +}