From 8c9a2037b5c0fed881a3ad6dd9cff364eed603d9 Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Tue, 14 Jun 2011 16:55:13 -0400
Subject: [PATCH] Added AccessRestrictionFilter and simplified authentication.

---
 src/com/gitblit/utils/StringUtils.java               |   65 ++
 tests/com/gitblit/tests/GitBlitSuite.java            |    5 
 src/com/gitblit/wicket/panels/RepositoriesPanel.java |    4 
 src/com/gitblit/AccessRestrictionFilter.java         |  240 ++++++++++
 src/com/gitblit/models/UserModel.java                |    8 
 src/com/gitblit/wicket/pages/RepositoryPage.java     |   46 -
 src/com/gitblit/ServletRequestWrapper.java           |  311 ++++++++++++
 src/com/gitblit/GitFilter.java                       |   98 ++++
 src/com/gitblit/GitBlit.java                         |   45 +
 src/com/gitblit/wicket/WicketUtils.java              |   46 +
 src/com/gitblit/wicket/pages/CommitPage.java         |    2 
 src/com/gitblit/GitBlitServer.java                   |   96 +--
 src/com/gitblit/wicket/panels/RepositoriesPanel.html |    9 
 src/com/gitblit/FileLoginService.java                |  202 ++------
 docs/00_index.mkd                                    |    3 
 src/com/gitblit/Constants.java                       |    2 
 src/com/gitblit/FileSettings.java                    |    2 
 src/com/gitblit/SyndicationFilter.java               |   44 +
 src/com/gitblit/DownloadZipServlet.java              |    2 
 src/com/gitblit/wicket/pages/EditUserPage.java       |    7 
 src/com/gitblit/SyndicationServlet.java              |   56 +
 src/com/gitblit/wicket/pages/LogPage.java            |    2 
 /dev/null                                            |  108 ----
 src/com/gitblit/utils/SyndicationUtils.java          |   19 
 src/com/gitblit/wicket/GitBlitWebApp.java            |    2 
 src/com/gitblit/wicket/pages/SummaryPage.java        |   16 
 26 files changed, 1,020 insertions(+), 420 deletions(-)

diff --git a/docs/00_index.mkd b/docs/00_index.mkd
index f84773e..bcf41e1 100644
--- a/docs/00_index.mkd
+++ b/docs/00_index.mkd
@@ -61,14 +61,13 @@
 - Gitblit may have security holes.  Patches welcome.  :)
 
 ### Todo List
-- Custom BASIC authentication servlet or servlet filter
 - Code documentation
 - Unit testing
 - Update Build.java to JGit 1.0.0, when its released
+- WAR solution
 
 ### Idea List
 - Consider clone remote repository feature
-- Consider [Apache Shiro](http://shiro.apache.org) for authentication
 - Stronger Ticgit read-only integration
     - activity/timeline
     - query feature with paging support
diff --git a/src/com/gitblit/AccessRestrictionFilter.java b/src/com/gitblit/AccessRestrictionFilter.java
new file mode 100644
index 0000000..3aca103
--- /dev/null
+++ b/src/com/gitblit/AccessRestrictionFilter.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.io.IOException;
+import java.security.Principal;
+import java.text.MessageFormat;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * 
+ * http://en.wikipedia.org/wiki/Basic_access_authentication
+ */
+public abstract class AccessRestrictionFilter implements Filter {
+
+	private static final String BASIC = "Basic";
+
+	private static final String CHALLENGE = BASIC + " realm=\"" + Constants.NAME + "\"";
+
+	private static final String SESSION_SECURED = "com.gitblit.secured";
+
+	protected transient Logger logger;
+
+	public AccessRestrictionFilter() {
+		logger = LoggerFactory.getLogger(getClass());
+	}
+
+	protected abstract String extractRepositoryName(String url);
+
+	protected abstract String getUrlRequestType(String url);
+
+	protected abstract boolean requiresAuthentication(RepositoryModel repository);
+
+	protected abstract boolean canAccess(RepositoryModel repository, UserModel user,
+			String restrictedUrl);
+
+	@Override
+	public void doFilter(final ServletRequest request, final ServletResponse response,
+			final FilterChain chain) throws IOException, ServletException {
+
+		HttpServletRequest httpRequest = (HttpServletRequest) request;
+		HttpServletResponse httpResponse = (HttpServletResponse) response;
+
+		// Wrap the HttpServletRequest with the AccessRestrictionRequest which
+		// overrides the servlet container user principal methods.
+		// JGit requires either:
+		//
+		// 1. servlet container authenticated user
+		// 2. http.receivepack = true in each repository's config
+		//
+		// Gitblit must conditionally authenticate users per-repository so just
+		// enabling http.receivepack is insufficient.
+
+		AccessRestrictionRequest accessRequest = new AccessRestrictionRequest(httpRequest);
+
+		String url = httpRequest.getRequestURI().substring(httpRequest.getServletPath().length());
+		String params = httpRequest.getQueryString();
+		if (url.length() > 0 && url.charAt(0) == '/') {
+			url = url.substring(1);
+		}
+		String fullUrl = url + (StringUtils.isEmpty(params) ? "" : ("?" + params));
+
+		String repository = extractRepositoryName(url);
+
+		// Determine if the request URL is restricted
+		String fullSuffix = fullUrl.substring(repository.length());
+		String urlRequestType = getUrlRequestType(fullSuffix);
+
+		// Load the repository model
+		RepositoryModel model = GitBlit.self().getRepositoryModel(repository);
+		if (model == null) {
+			// repository not found. send 404.
+			logger.info("ARF: " + fullUrl + " (" + HttpServletResponse.SC_NOT_FOUND + ")");
+			httpResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
+			return;
+		}
+
+		// BASIC authentication challenge and response processing
+		if (!StringUtils.isEmpty(urlRequestType) && requiresAuthentication(model)) {
+			// look for client authorization credentials in header
+			final String authorization = httpRequest.getHeader("Authorization");
+			if (authorization != null && authorization.startsWith(BASIC)) {
+				// Authorization: Basic base64credentials
+				String base64Credentials = authorization.substring(BASIC.length()).trim();
+				String credentials = StringUtils.decodeBase64(base64Credentials);
+				if (GitBlit.isDebugMode()) {
+					logger.info(MessageFormat.format("AUTH: {0} ({1})", authorization, credentials));
+				}
+				// credentials = username:password
+				final String[] values = credentials.split(":");
+
+				if (values.length == 2) {
+					String username = values[0];
+					char[] password = values[1].toCharArray();
+					UserModel user = GitBlit.self().authenticate(username, password);
+					if (user != null) {
+						accessRequest.setUser(user);
+						if (user.canAdmin || canAccess(model, user, urlRequestType)) {
+							// authenticated request permitted.
+							// pass processing to the restricted servlet.
+							newSession(accessRequest, httpResponse);
+							logger.info("ARF: " + fullUrl + " (" + HttpServletResponse.SC_CONTINUE + ") authenticated");
+							chain.doFilter(accessRequest, httpResponse);
+							return;
+						}
+						// valid user, but not for requested access. send 403.
+						if (GitBlit.isDebugMode()) {
+							logger.info("ARF: " + fullUrl + " (" + HttpServletResponse.SC_FORBIDDEN
+									+ ")");
+							logger.info(MessageFormat.format("AUTH: {0} forbidden to access {1}",
+									user.username, url));
+						}
+						httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN);
+						return;
+					}
+				}
+				if (GitBlit.isDebugMode()) {
+					logger.info(MessageFormat
+							.format("AUTH: invalid credentials ({0})", credentials));
+				}
+			}
+
+			// challenge client to provide credentials. send 401.
+			if (GitBlit.isDebugMode()) {
+				logger.info("ARF: " + fullUrl + " (" + HttpServletResponse.SC_UNAUTHORIZED + ")");
+				logger.info("AUTH: Challenge " + CHALLENGE);
+			}
+			httpResponse.setHeader("WWW-Authenticate", CHALLENGE);
+			httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+			return;
+		}
+
+		if (GitBlit.isDebugMode()) {
+			logger.info("ARF: " + fullUrl + " (" + HttpServletResponse.SC_CONTINUE + ") unauthenticated");
+		}
+		// unauthenticated request permitted.
+		// pass processing to the restricted servlet.
+		chain.doFilter(accessRequest, httpResponse);
+	}
+
+	/**
+	 * Taken from Jetty's LoginAuthenticator.renewSessionOnAuthentication()
+	 */
+	protected void newSession(HttpServletRequest request, HttpServletResponse response) {
+		HttpSession oldSession = request.getSession(false);
+		if (oldSession != null && oldSession.getAttribute(SESSION_SECURED) == null) {
+			synchronized (this) {
+				Map<String, Object> attributes = new HashMap<String, Object>();
+				Enumeration<String> e = oldSession.getAttributeNames();
+				while (e.hasMoreElements()) {
+					String name = e.nextElement();
+					attributes.put(name, oldSession.getAttribute(name));
+					oldSession.removeAttribute(name);
+				}
+				oldSession.invalidate();
+
+				HttpSession newSession = request.getSession(true);
+				newSession.setAttribute(SESSION_SECURED, Boolean.TRUE);
+				for (Map.Entry<String, Object> entry : attributes.entrySet()) {
+					newSession.setAttribute(entry.getKey(), entry.getValue());
+				}
+			}
+		}
+	}
+
+	@Override
+	public void init(final FilterConfig config) throws ServletException {
+	}
+
+	@Override
+	public void destroy() {
+	}
+	
+	/**
+	 * Wraps a standard HttpServletRequest and overrides user principal methods.
+	 */
+	public static class AccessRestrictionRequest extends ServletRequestWrapper {
+
+		private UserModel user;
+		
+		public AccessRestrictionRequest(HttpServletRequest req) {
+			super(req);
+			user = new UserModel("anonymous");
+		}
+		
+		void setUser(UserModel user) {
+			this.user = user;
+		}
+
+		@Override
+		public String getRemoteUser() {
+			return user.username;
+		}
+
+		@Override
+		public boolean isUserInRole(String role) {
+			if (role.equals(Constants.ADMIN_ROLE)) {
+				return user.canAdmin;
+			}
+			return user.canAccessRepository(role);
+		}
+
+		@Override
+		public Principal getUserPrincipal() {
+			return user;
+		}
+	}
+}
\ No newline at end of file
diff --git a/src/com/gitblit/Constants.java b/src/com/gitblit/Constants.java
index 88b13e0..68e7b67 100644
--- a/src/com/gitblit/Constants.java
+++ b/src/com/gitblit/Constants.java
@@ -38,6 +38,8 @@
 	public static final String ZIP_SERVLET_PATH = "/zip/";
 	
 	public static final String SYNDICATION_SERVLET_PATH = "/feed/";
+	
+	public static final String RESOURCE_PATH = "/com/gitblit/wicket/resources/";
 
 	public static final String BORDER = "***********************************************************";
 
diff --git a/src/com/gitblit/DownloadZipServlet.java b/src/com/gitblit/DownloadZipServlet.java
index 1745474..3b02cba 100644
--- a/src/com/gitblit/DownloadZipServlet.java
+++ b/src/com/gitblit/DownloadZipServlet.java
@@ -41,7 +41,7 @@
 	}
 
 	public static String asLink(String baseURL, String repository, String objectId, String path) {
-		if (baseURL.charAt(baseURL.length() - 1) == '/') {
+		if (baseURL.length() > 0 && baseURL.charAt(baseURL.length() - 1) == '/') {
 			baseURL = baseURL.substring(0, baseURL.length() - 1);
 		}
 		return baseURL + Constants.ZIP_SERVLET_PATH + "?r=" + repository
diff --git a/src/com/gitblit/JettyLoginService.java b/src/com/gitblit/FileLoginService.java
similarity index 62%
rename from src/com/gitblit/JettyLoginService.java
rename to src/com/gitblit/FileLoginService.java
index 22f9ce3..b59a776 100644
--- a/src/com/gitblit/JettyLoginService.java
+++ b/src/com/gitblit/FileLoginService.java
@@ -16,98 +16,72 @@
 package com.gitblit;
 
 import java.io.File;
-import java.io.FileReader;
 import java.io.FileWriter;
 import java.io.IOException;
-import java.security.Principal;
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Properties;
 import java.util.Set;
 
-import javax.security.auth.Subject;
-
-import org.eclipse.jetty.http.security.Credential;
-import org.eclipse.jetty.security.IdentityService;
-import org.eclipse.jetty.security.MappedLoginService;
-import org.eclipse.jetty.server.UserIdentity;
-import org.eclipse.jetty.util.log.Log;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.gitblit.models.UserModel;
+import com.gitblit.utils.StringUtils;
 
-public class JettyLoginService extends MappedLoginService implements ILoginService {
+public class FileLoginService extends FileSettings implements ILoginService {
 
-	private final Logger logger = LoggerFactory.getLogger(JettyLoginService.class);
+	private final Logger logger = LoggerFactory.getLogger(FileLoginService.class);
 
-	private final File realmFile;
-
-	public JettyLoginService(File realmFile) {
-		super();
-		setName(Constants.NAME);
-		this.realmFile = realmFile;
+	public FileLoginService(File realmFile) {
+		super(realmFile.getAbsolutePath());
 	}
 
 	@Override
 	public UserModel authenticate(String username, char[] password) {
-		UserIdentity identity = login(username, new String(password));
-		if (identity == null || identity.equals(UserIdentity.UNAUTHENTICATED_IDENTITY)) {
+		Properties allUsers = read();
+		String userInfo = allUsers.getProperty(username);
+		if (StringUtils.isEmpty(userInfo)) {
 			return null;
 		}
-		UserModel user = new UserModel(username);
-		user.canAdmin = identity.isUserInRole(Constants.ADMIN_ROLE, null);
-
-		// Add repositories
-		for (Principal principal : identity.getSubject().getPrincipals()) {
-			if (principal instanceof RolePrincipal) {
-				RolePrincipal role = (RolePrincipal) principal;
-				String roleName = role.getName();
-				if (roleName.charAt(0) != '#') {
-					user.addRepository(roleName);
-				}
+		UserModel returnedUser = null;
+		UserModel user = getUserModel(username);
+		if (user.password.startsWith(StringUtils.MD5_TYPE)) {
+			String md5 = StringUtils.MD5_TYPE + StringUtils.getMD5(new String(password));
+			if (user.password.equalsIgnoreCase(md5)) {
+				returnedUser = user;
 			}
 		}
-		return user;
+		if (user.password.equals(new String(password))) {
+			returnedUser = user;
+		}
+		return returnedUser;
 	}
 
 	@Override
 	public UserModel getUserModel(String username) {
-		UserIdentity identity = _users.get(username);
-		if (identity == null) {
+		Properties allUsers = read();
+		String userInfo = allUsers.getProperty(username);
+		if (userInfo == null) {
 			return null;
 		}
 		UserModel model = new UserModel(username);
-		Subject subject = identity.getSubject();
-		for (Principal principal : subject.getPrincipals()) {
-			if (principal instanceof RolePrincipal) {
-				RolePrincipal role = (RolePrincipal) principal;
-				String name = role.getName();
-				switch (name.charAt(0)) {
-				case '#':
-					// Permissions
-					if (name.equalsIgnoreCase(Constants.ADMIN_ROLE)) {
-						model.canAdmin = true;
-					}
-					break;
-				default:
-					model.addRepository(name);
+		String[] userValues = userInfo.split(",");
+		model.password = userValues[0];
+		for (int i = 1; i < userValues.length; i++) {
+			String role = userValues[i];
+			switch (role.charAt(0)) {
+			case '#':
+				// Permissions
+				if (role.equalsIgnoreCase(Constants.ADMIN_ROLE)) {
+					model.canAdmin = true;
 				}
+				break;
+			default:
+				model.addRepository(role);
 			}
-		}
-		// Retrieve the password from the realm file.
-		// Stupid, I know, but the password is buried within protected inner
-		// classes in private variables. Too much work to reflectively retrieve.
-		try {
-			Properties allUsers = readRealmFile();
-			String value = allUsers.getProperty(username);
-			String password = value.split(",")[0];
-			model.password = password;
-		} catch (Throwable t) {
-			logger.error(MessageFormat.format("Failed to read password for user {0}!", username), t);
 		}
 		return model;
 	}
@@ -120,7 +94,7 @@
 	@Override
 	public boolean updateUserModel(String username, UserModel model) {
 		try {
-			Properties allUsers = readRealmFile();
+			Properties allUsers = read();
 			ArrayList<String> roles = new ArrayList<String>(model.repositories);
 
 			// Permissions
@@ -140,12 +114,7 @@
 			allUsers.remove(username);
 			allUsers.put(model.username, sb.toString());
 
-			writeRealmFile(allUsers);
-
-			// Update login service
-			removeUser(username);
-			putUser(model.username, Credential.getCredential(model.password),
-					roles.toArray(new String[0]));
+			write(allUsers);
 			return true;
 		} catch (Throwable t) {
 			logger.error(MessageFormat.format("Failed to update user model {0}!", model.username),
@@ -163,12 +132,9 @@
 	public boolean deleteUser(String username) {
 		try {
 			// Read realm file
-			Properties allUsers = readRealmFile();
+			Properties allUsers = read();
 			allUsers.remove(username);
-			writeRealmFile(allUsers);
-
-			// Drop user from map
-			removeUser(username);
+			write(allUsers);
 			return true;
 		} catch (Throwable t) {
 			logger.error(MessageFormat.format("Failed to delete user {0}!", username), t);
@@ -178,8 +144,8 @@
 
 	@Override
 	public List<String> getAllUsernames() {
-		List<String> list = new ArrayList<String>();
-		list.addAll(_users.keySet());
+		Properties allUsers = read();
+		List<String> list = new ArrayList<String>(allUsers.stringPropertyNames());
 		return list;
 	}
 
@@ -187,7 +153,7 @@
 	public List<String> getUsernamesForRole(String role) {
 		List<String> list = new ArrayList<String>();
 		try {
-			Properties allUsers = readRealmFile();
+			Properties allUsers = read();
 			for (String username : allUsers.stringPropertyNames()) {
 				String value = allUsers.getProperty(username);
 				String[] values = value.split(",");
@@ -214,7 +180,7 @@
 			Set<String> needsRemoveRole = new HashSet<String>();
 
 			// identify users which require add and remove role
-			Properties allUsers = readRealmFile();
+			Properties allUsers = read();
 			for (String username : allUsers.stringPropertyNames()) {
 				String value = allUsers.getProperty(username);
 				String[] values = value.split(",");
@@ -239,11 +205,6 @@
 				String userValues = allUsers.getProperty(user);
 				userValues += "," + role;
 				allUsers.put(user, userValues);
-				String[] values = userValues.split(",");
-				String password = values[0];
-				String[] roles = new String[values.length - 1];
-				System.arraycopy(values, 1, roles, 0, values.length - 1);
-				putUser(user, Credential.getCredential(password), roles);
 			}
 
 			// remove role from user
@@ -267,14 +228,10 @@
 
 				// update properties
 				allUsers.put(user, sb.toString());
-
-				// update memory
-				putUser(user, Credential.getCredential(password),
-						revisedRoles.toArray(new String[0]));
 			}
 
 			// persist changes
-			writeRealmFile(allUsers);
+			write(allUsers);
 			return true;
 		} catch (Throwable t) {
 			logger.error(MessageFormat.format("Failed to set usernames for role {0}!", role), t);
@@ -285,7 +242,7 @@
 	@Override
 	public boolean renameRole(String oldRole, String newRole) {
 		try {
-			Properties allUsers = readRealmFile();
+			Properties allUsers = read();
 			Set<String> needsRenameRole = new HashSet<String>();
 
 			// identify users which require role rename
@@ -325,14 +282,10 @@
 
 				// update properties
 				allUsers.put(user, sb.toString());
-
-				// update memory
-				putUser(user, Credential.getCredential(password),
-						revisedRoles.toArray(new String[0]));
 			}
 
 			// persist changes
-			writeRealmFile(allUsers);
+			write(allUsers);
 			return true;
 		} catch (Throwable t) {
 			logger.error(
@@ -344,7 +297,7 @@
 	@Override
 	public boolean deleteRole(String role) {
 		try {
-			Properties allUsers = readRealmFile();
+			Properties allUsers = read();
 			Set<String> needsDeleteRole = new HashSet<String>();
 
 			// identify users which require role rename
@@ -383,14 +336,10 @@
 
 				// update properties
 				allUsers.put(user, sb.toString());
-
-				// update memory
-				putUser(user, Credential.getCredential(password),
-						revisedRoles.toArray(new String[0]));
 			}
 
 			// persist changes
-			writeRealmFile(allUsers);
+			write(allUsers);
 			return true;
 		} catch (Throwable t) {
 			logger.error(MessageFormat.format("Failed to delete role {0}!", role), t);
@@ -398,74 +347,27 @@
 		return false;
 	}
 
-	private Properties readRealmFile() throws IOException {
-		Properties allUsers = new Properties();
-		FileReader reader = new FileReader(realmFile);
-		allUsers.load(reader);
-		reader.close();
-		return allUsers;
-	}
-
-	private void writeRealmFile(Properties properties) throws IOException {
+	private void write(Properties properties) throws IOException {
 		// Update realm file
-		File realmFileCopy = new File(realmFile.getAbsolutePath() + ".tmp");
+		File realmFileCopy = new File(propertiesFile.getAbsolutePath() + ".tmp");
 		FileWriter writer = new FileWriter(realmFileCopy);
 		properties
 				.store(writer,
 						"# Gitblit realm file format: username=password,\\#permission,repository1,repository2...");
 		writer.close();
 		if (realmFileCopy.exists() && realmFileCopy.length() > 0) {
-			if (realmFile.delete()) {
-				if (!realmFileCopy.renameTo(realmFile)) {
+			if (propertiesFile.delete()) {
+				if (!realmFileCopy.renameTo(propertiesFile)) {
 					throw new IOException(MessageFormat.format("Failed to rename {0} to {1}!",
-							realmFileCopy.getAbsolutePath(), realmFile.getAbsolutePath()));
+							realmFileCopy.getAbsolutePath(), propertiesFile.getAbsolutePath()));
 				}
 			} else {
 				throw new IOException(MessageFormat.format("Failed to delete (0)!",
-						realmFile.getAbsolutePath()));
+						propertiesFile.getAbsolutePath()));
 			}
 		} else {
 			throw new IOException(MessageFormat.format("Failed to save {0}!",
 					realmFileCopy.getAbsolutePath()));
 		}
-	}
-
-	/* ------------------------------------------------------------ */
-	@Override
-	public void loadUsers() throws IOException {
-		if (realmFile == null) {
-			return;
-		}
-
-		if (Log.isDebugEnabled()) {
-			Log.debug("Load " + this + " from " + realmFile);
-		}
-		Properties allUsers = readRealmFile();
-
-		// Map Users
-		for (Map.Entry<Object, Object> entry : allUsers.entrySet()) {
-			String username = ((String) entry.getKey()).trim();
-			String credentials = ((String) entry.getValue()).trim();
-			String roles = null;
-			int c = credentials.indexOf(',');
-			if (c > 0) {
-				roles = credentials.substring(c + 1).trim();
-				credentials = credentials.substring(0, c).trim();
-			}
-
-			if (username != null && username.length() > 0 && credentials != null
-					&& credentials.length() > 0) {
-				String[] roleArray = IdentityService.NO_ROLES;
-				if (roles != null && roles.length() > 0) {
-					roleArray = roles.split(",");
-				}
-				putUser(username, Credential.getCredential(credentials), roleArray);
-			}
-		}
-	}
-
-	@Override
-	protected UserIdentity loadUser(String username) {
-		return null;
 	}
 }
diff --git a/src/com/gitblit/FileSettings.java b/src/com/gitblit/FileSettings.java
index b70daa0..e213e80 100644
--- a/src/com/gitblit/FileSettings.java
+++ b/src/com/gitblit/FileSettings.java
@@ -26,7 +26,7 @@
  */
 public class FileSettings extends IStoredSettings {
 
-	private final File propertiesFile;
+	protected final File propertiesFile;
 
 	private final Properties properties = new Properties();
 
diff --git a/src/com/gitblit/GitBlit.java b/src/com/gitblit/GitBlit.java
index 0132623..fa593f9 100644
--- a/src/com/gitblit/GitBlit.java
+++ b/src/com/gitblit/GitBlit.java
@@ -20,7 +20,10 @@
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
 
 import javax.servlet.ServletContextEvent;
 import javax.servlet.ServletContextListener;
@@ -87,8 +90,8 @@
 		return GITBLIT.storedSettings.getAllKeys(startingWith);
 	}
 
-	public boolean isDebugMode() {
-		return storedSettings.getBoolean(Keys.web.debugMode, false);
+	public static boolean isDebugMode() {
+		return GITBLIT.storedSettings.getBoolean(Keys.web.debugMode, false);
 	}
 
 	public List<String> getOtherCloneUrls(String repositoryName) {
@@ -312,6 +315,41 @@
 		return false;
 	}
 
+	public String processCommitMessage(String repositoryName, String text) {
+		String html = StringUtils.breakLinesForHtml(text);
+		Map<String, String> map = new HashMap<String, String>();
+		// global regex keys
+		if (storedSettings.getBoolean(Keys.regex.global, false)) {
+			for (String key : storedSettings.getAllKeys(Keys.regex.global)) {
+				if (!key.equals(Keys.regex.global)) {
+					String subKey = key.substring(key.lastIndexOf('.') + 1);
+					map.put(subKey, storedSettings.getString(key, ""));
+				}
+			}
+		}
+
+		// repository-specific regex keys
+		List<String> keys = storedSettings.getAllKeys(Keys.regex._ROOT + "."
+				+ repositoryName.toLowerCase());
+		for (String key : keys) {
+			String subKey = key.substring(key.lastIndexOf('.') + 1);
+			map.put(subKey, storedSettings.getString(key, ""));
+		}
+
+		for (Entry<String, String> entry : map.entrySet()) {
+			String definition = entry.getValue().trim();
+			String[] chunks = definition.split("!!!");
+			if (chunks.length == 2) {
+				html = html.replaceAll(chunks[0], chunks[1]);
+			} else {
+				logger.warn(entry.getKey()
+						+ " improperly formatted.  Use !!! to separate match from replacement: "
+						+ definition);
+			}
+		}
+		return html;
+	}
+
 	public void configureContext(IStoredSettings settings) {
 		logger.info("Reading configuration from " + settings.toString());
 		this.storedSettings = settings;
@@ -323,7 +361,8 @@
 	@Override
 	public void contextInitialized(ServletContextEvent contextEvent) {
 		if (storedSettings == null) {
-			// for running gitblit as a traditional webapp in a servlet container
+			// for running gitblit as a traditional webapp in a servlet
+			// container
 			WebXmlSettings webxmlSettings = new WebXmlSettings(contextEvent.getServletContext());
 			configureContext(webxmlSettings);
 		}
diff --git a/src/com/gitblit/GitBlitServer.java b/src/com/gitblit/GitBlitServer.java
index 2495aee..4b6df70 100644
--- a/src/com/gitblit/GitBlitServer.java
+++ b/src/com/gitblit/GitBlitServer.java
@@ -34,13 +34,7 @@
 import org.apache.log4j.PatternLayout;
 import org.apache.wicket.protocol.http.ContextParamWebApplicationFactory;
 import org.apache.wicket.protocol.http.WicketFilter;
-import org.eclipse.jetty.http.security.Constraint;
-import org.eclipse.jetty.security.ConstraintMapping;
-import org.eclipse.jetty.security.ConstraintSecurityHandler;
-import org.eclipse.jetty.security.LoginService;
-import org.eclipse.jetty.security.authentication.BasicAuthenticator;
 import org.eclipse.jetty.server.Connector;
-import org.eclipse.jetty.server.Handler;
 import org.eclipse.jetty.server.Server;
 import org.eclipse.jetty.server.bio.SocketConnector;
 import org.eclipse.jetty.server.nio.SelectChannelConnector;
@@ -53,6 +47,7 @@
 import org.eclipse.jetty.servlet.ServletHolder;
 import org.eclipse.jetty.util.thread.QueuedThreadPool;
 import org.eclipse.jetty.webapp.WebAppContext;
+import org.eclipse.jgit.http.server.GitServlet;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -234,77 +229,52 @@
 		wicketFilter.setInitParameter(ContextParamWebApplicationFactory.APP_CLASS_PARAM,
 				GitBlitWebApp.class.getName());
 		wicketFilter.setInitParameter(WicketFilter.FILTER_MAPPING_PARAM, wicketPathSpec);
-		wicketFilter.setInitParameter(WicketFilter.IGNORE_PATHS_PARAM, "git/");
+		wicketFilter.setInitParameter(WicketFilter.IGNORE_PATHS_PARAM, "git/,feed/,zip/");
 		rootContext.addFilter(wicketFilter, wicketPathSpec, FilterMapping.DEFAULT);
+
+		// JGit Filter and Servlet
+		if (settings.getBoolean(Keys.git.enableGitServlet, true)) {
+			String jgitPathSpec = Constants.GIT_SERVLET_PATH + "*";
+			rootContext.addFilter(GitFilter.class, jgitPathSpec, FilterMapping.DEFAULT);
+			ServletHolder jGitServlet = rootContext.addServlet(GitServlet.class, jgitPathSpec);
+			jGitServlet.setInitParameter("base-path", params.repositoriesFolder);
+			jGitServlet.setInitParameter("export-all",
+					settings.getBoolean(Keys.git.exportAll, true) ? "1" : "0");
+		}
+
+		// Syndication Filter and Servlet
+		String feedPathSpec = Constants.SYNDICATION_SERVLET_PATH + "*";
+		rootContext.addFilter(SyndicationFilter.class, feedPathSpec, FilterMapping.DEFAULT);
+		rootContext.addServlet(SyndicationServlet.class, feedPathSpec);
 
 		// Zip Servlet
 		rootContext.addServlet(DownloadZipServlet.class, Constants.ZIP_SERVLET_PATH + "*");
 
-		// Syndication Servlet
-		rootContext.addServlet(SyndicationServlet.class, Constants.SYNDICATION_SERVLET_PATH + "*");
-
-		// Git Servlet
-		ServletHolder gitServlet = null;
-		String gitServletPathSpec = Constants.GIT_SERVLET_PATH + "*";
-		if (settings.getBoolean(Keys.git.enableGitServlet, true)) {
-			gitServlet = rootContext.addServlet(GitBlitServlet.class, gitServletPathSpec);
-			gitServlet.setInitParameter("base-path", params.repositoriesFolder);
-			gitServlet.setInitParameter("export-all",
-					settings.getBoolean(Keys.git.exportAll, true) ? "1" : "0");
-		}
-
 		// Login Service
-		LoginService loginService = null;
 		String realmUsers = params.realmFile;
-		if (!StringUtils.isEmpty(realmUsers)) {
-			File realmFile = new File(realmUsers);
-			if (realmFile.exists()) {
-				logger.info("Setting up login service from " + realmUsers);
-				JettyLoginService jettyLoginService = new JettyLoginService(realmFile);
-				GitBlit.self().setLoginService(jettyLoginService);
-				loginService = jettyLoginService;
+		if (StringUtils.isEmpty(realmUsers)) {
+			logger.error(MessageFormat.format("PLEASE SPECIFY {0}!!", Keys.realm.realmFile));
+			return;
+		}
+		File realmFile = new File(realmUsers);
+		if (!realmFile.exists()) {
+			try {
+				realmFile.createNewFile();
+			} catch (IOException x) {
+				logger.error(MessageFormat.format("COULD NOT CREATE REALM FILE {0}!", realmUsers),
+						x);
+				return;
 			}
 		}
-
-		// Determine what handler to use
-		Handler handler;
-		if (gitServlet != null) {
-			if (loginService != null) {
-				// Authenticate Clone/Push
-				logger.info("Setting up authenticated git servlet clone/push access");
-
-				Constraint constraint = new Constraint();
-				constraint.setAuthenticate(true);
-				constraint.setRoles(new String[] { "*" });
-
-				ConstraintMapping mapping = new ConstraintMapping();
-				mapping.setPathSpec(gitServletPathSpec);
-				mapping.setConstraint(constraint);
-
-				ConstraintSecurityHandler security = new ConstraintSecurityHandler();
-				security.addConstraintMapping(mapping);
-				security.setAuthenticator(new BasicAuthenticator());
-				security.setLoginService(loginService);
-				security.setStrict(false);
-
-				security.setHandler(rootContext);
-
-				handler = security;
-			} else {
-				// Anonymous Pull/Push
-				logger.info("Setting up anonymous git servlet pull/push access");
-				handler = rootContext;
-			}
-		} else {
-			logger.info("Git servlet clone/push disabled");
-			handler = rootContext;
-		}
+		logger.info("Setting up login service from " + realmUsers);
+		FileLoginService loginService = new FileLoginService(realmFile);
+		GitBlit.self().setLoginService(loginService);
 
 		logger.info("Git repositories folder "
 				+ new File(params.repositoriesFolder).getAbsolutePath());
 
 		// Set the server's contexts
-		server.setHandler(handler);
+		server.setHandler(rootContext);
 
 		// Setup the GitBlit context
 		GitBlit gitblit = GitBlit.self();
diff --git a/src/com/gitblit/GitBlitServlet.java b/src/com/gitblit/GitBlitServlet.java
deleted file mode 100644
index a71012b..0000000
--- a/src/com/gitblit/GitBlitServlet.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright 2011 gitblit.com.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.gitblit;
-
-import java.io.IOException;
-import java.text.MessageFormat;
-
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import org.eclipse.jgit.http.server.GitServlet;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.gitblit.Constants.AccessRestrictionType;
-import com.gitblit.models.RepositoryModel;
-
-public class GitBlitServlet extends GitServlet {
-
-	private static final long serialVersionUID = 1L;
-
-	private transient Logger logger = LoggerFactory.getLogger(GitBlitServlet.class);
-
-	public GitBlitServlet() {
-		super();
-	}
-
-	@Override
-	protected void service(final HttpServletRequest req, final HttpServletResponse rsp)
-			throws ServletException, IOException {
-		// admins have full git access to all repositories
-		if (req.isUserInRole(Constants.ADMIN_ROLE)) {
-			// admins can do whatever
-			super.service(req, rsp);
-			return;
-		}
-
-		// try to intercept repository names for authenticated access
-		String url = req.getRequestURI().substring(req.getServletPath().length());
-		if (url.charAt(0) == '/' && url.length() > 1) {
-			url = url.substring(1);
-		}
-		int forwardSlash = url.indexOf('/');
-		if (forwardSlash > -1) {
-			String repository = url.substring(0, forwardSlash).toLowerCase();
-			String function = url.substring(forwardSlash + 1);
-			String query = req.getQueryString() == null ? "" : req.getQueryString();
-			RepositoryModel model = GitBlit.self().getRepositoryModel(repository);
-			if (model != null) {
-				if (model.isFrozen || model.accessRestriction.atLeast(AccessRestrictionType.PUSH)) {
-					boolean authorizedUser = req.isUserInRole(repository);
-					if (function.startsWith("git-receive-pack")
-							|| (query.indexOf("service=git-receive-pack") > -1)) {
-						// Push request
-						if (!model.isFrozen && authorizedUser) {
-							// clone-restricted or push-authorized
-							super.service(req, rsp);
-							return;
-						} else {
-							// user is unauthorized to push to this repository
-							logger.warn(MessageFormat.format(
-									"user {0} is not authorized to push to {1}", req
-											.getUserPrincipal().getName(), repository));
-							rsp.sendError(HttpServletResponse.SC_FORBIDDEN, MessageFormat.format(
-									"you are not authorized to push to {0}", repository));
-							return;
-						}
-					} else if (function.startsWith("git-upload-pack")
-							|| (query.indexOf("service=git-upload-pack") > -1)) {
-						// Clone request
-						boolean cloneRestricted = model.accessRestriction
-								.atLeast(AccessRestrictionType.CLONE);
-						if (!cloneRestricted || (cloneRestricted && authorizedUser)) {
-							// push-restricted or clone-authorized
-							super.service(req, rsp);
-							return;
-						} else {
-							// user is unauthorized to clone this repository
-							logger.warn(MessageFormat.format(
-									"user {0} is not authorized to clone {1}", req
-											.getUserPrincipal().getName(), repository));
-							rsp.sendError(HttpServletResponse.SC_FORBIDDEN, MessageFormat.format(
-									"you are not authorized to clone {0}", repository));
-							return;
-						}
-					}
-				}
-			}
-		}
-
-		// pass-through to git servlet
-		super.service(req, rsp);
-	}
-}
diff --git a/src/com/gitblit/GitFilter.java b/src/com/gitblit/GitFilter.java
new file mode 100644
index 0000000..5bd7b33
--- /dev/null
+++ b/src/com/gitblit/GitFilter.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.text.MessageFormat;
+
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.StringUtils;
+
+public class GitFilter extends AccessRestrictionFilter {
+
+	protected final String gitReceivePack = "/git-receive-pack";
+
+	protected final String gitUploadPack = "/git-upload-pack";
+
+	protected final String[] suffixes = { gitReceivePack, gitUploadPack, "/info/refs", "/HEAD",
+			"/objects" };
+
+	@Override
+	protected String extractRepositoryName(String url) {
+		String repository = url;
+		for (String urlSuffix : suffixes) {
+			if (repository.indexOf(urlSuffix) > -1) {
+				repository = repository.substring(0, repository.indexOf(urlSuffix));
+			}
+		}
+		return repository;
+	}
+
+	@Override
+	protected String getUrlRequestType(String suffix) {
+		if (!StringUtils.isEmpty(suffix)) {
+			if (suffix.startsWith(gitReceivePack)) {
+				return gitReceivePack;
+			} else if (suffix.startsWith(gitUploadPack)) {
+				return gitUploadPack;
+			} else if (suffix.contains("?service=git-receive-pack")) {
+				return gitReceivePack;
+			} else if (suffix.contains("?service=git-upload-pack")) {
+				return gitUploadPack;
+			}
+		}
+		return null;
+	}
+
+	@Override
+	protected boolean requiresAuthentication(RepositoryModel repository) {
+		return repository.accessRestriction.atLeast(AccessRestrictionType.PUSH);
+	}
+
+	@Override
+	protected boolean canAccess(RepositoryModel repository, UserModel user, String urlRequestType) {
+		if (repository.isFrozen || repository.accessRestriction.atLeast(AccessRestrictionType.PUSH)) {
+			boolean authorizedUser = user.canAccessRepository(repository.name);
+			if (urlRequestType.equals(gitReceivePack)) {
+				// Push request
+				if (!repository.isFrozen && authorizedUser) {
+					// clone-restricted or push-authorized
+					return true;
+				} else {
+					// user is unauthorized to push to this repository
+					logger.warn(MessageFormat.format("user {0} is not authorized to push to {1}",
+							user.username, repository));
+					return false;
+				}
+			} else if (urlRequestType.equals(gitUploadPack)) {
+				// Clone request
+				boolean cloneRestricted = repository.accessRestriction
+						.atLeast(AccessRestrictionType.CLONE);
+				if (!cloneRestricted || (cloneRestricted && authorizedUser)) {
+					// push-restricted or clone-authorized
+					return true;
+				} else {
+					// user is unauthorized to clone this repository
+					logger.warn(MessageFormat.format("user {0} is not authorized to clone {1}",
+							user.username, repository));
+					return false;
+				}
+			}
+		}
+		return true;
+	}
+}
diff --git a/src/com/gitblit/ServletRequestWrapper.java b/src/com/gitblit/ServletRequestWrapper.java
new file mode 100644
index 0000000..b97c395
--- /dev/null
+++ b/src/com/gitblit/ServletRequestWrapper.java
@@ -0,0 +1,311 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.Principal;
+import java.util.Enumeration;
+import java.util.Locale;
+import java.util.Map;
+
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+
+public abstract class ServletRequestWrapper implements HttpServletRequest {
+
+	protected final HttpServletRequest req;
+
+	public ServletRequestWrapper(HttpServletRequest req) {
+		this.req = req;
+	}
+
+	@Override
+	public Object getAttribute(String name) {
+		return req.getAttribute(name);
+	}
+
+	@Override
+	public Enumeration getAttributeNames() {
+		return req.getAttributeNames();
+	}
+
+	@Override
+	public String getCharacterEncoding() {
+		return req.getCharacterEncoding();
+	}
+
+	@Override
+	public void setCharacterEncoding(String env) throws UnsupportedEncodingException {
+		req.setCharacterEncoding(env);
+	}
+
+	@Override
+	public int getContentLength() {
+		return req.getContentLength();
+	}
+
+	@Override
+	public String getContentType() {
+		return req.getContentType();
+	}
+
+	@Override
+	public ServletInputStream getInputStream() throws IOException {
+		return req.getInputStream();
+	}
+
+	@Override
+	public String getParameter(String name) {
+		return req.getParameter(name);
+	}
+
+	@Override
+	public Enumeration getParameterNames() {
+		return req.getParameterNames();
+	}
+
+	@Override
+	public String[] getParameterValues(String name) {
+		return req.getParameterValues(name);
+	}
+
+	@Override
+	public Map getParameterMap() {
+		return req.getParameterMap();
+	}
+
+	@Override
+	public String getProtocol() {
+		return req.getProtocol();
+	}
+
+	@Override
+	public String getScheme() {
+		return req.getScheme();
+	}
+
+	@Override
+	public String getServerName() {
+		return req.getServerName();
+	}
+
+	@Override
+	public int getServerPort() {
+		return req.getServerPort();
+	}
+
+	@Override
+	public BufferedReader getReader() throws IOException {
+		return req.getReader();
+	}
+
+	@Override
+	public String getRemoteAddr() {
+		return req.getRemoteAddr();
+	}
+
+	@Override
+	public String getRemoteHost() {
+		return req.getRemoteHost();
+	}
+
+	@Override
+	public void setAttribute(String name, Object o) {
+		req.setAttribute(name, o);
+	}
+
+	@Override
+	public void removeAttribute(String name) {
+		req.removeAttribute(name);
+	}
+
+	@Override
+	public Locale getLocale() {
+		return req.getLocale();
+	}
+
+	@Override
+	public Enumeration getLocales() {
+		return req.getLocales();
+	}
+
+	@Override
+	public boolean isSecure() {
+		return req.isSecure();
+	}
+
+	@Override
+	public RequestDispatcher getRequestDispatcher(String path) {
+		return req.getRequestDispatcher(path);
+	}
+
+	@Override
+	@Deprecated
+	public String getRealPath(String path) {
+		return req.getRealPath(path);
+	}
+
+	@Override
+	public int getRemotePort() {
+		return req.getRemotePort();
+	}
+
+	@Override
+	public String getLocalName() {
+		return req.getLocalName();
+	}
+
+	@Override
+	public String getLocalAddr() {
+		return req.getLocalAddr();
+	}
+
+	@Override
+	public int getLocalPort() {
+		return req.getLocalPort();
+	}
+
+	@Override
+	public String getAuthType() {
+		return req.getAuthType();
+	}
+
+	@Override
+	public Cookie[] getCookies() {
+		return req.getCookies();
+	}
+
+	@Override
+	public long getDateHeader(String name) {
+		return req.getDateHeader(name);
+	}
+
+	@Override
+	public String getHeader(String name) {
+		return req.getHeader(name);
+	}
+
+	@Override
+	public Enumeration getHeaders(String name) {
+		return req.getHeaders(name);
+	}
+
+	@Override
+	public Enumeration getHeaderNames() {
+		return req.getHeaderNames();
+	}
+
+	@Override
+	public int getIntHeader(String name) {
+		return req.getIntHeader(name);
+	}
+
+	@Override
+	public String getMethod() {
+		return req.getMethod();
+	}
+
+	@Override
+	public String getPathInfo() {
+		return req.getPathInfo();
+	}
+
+	@Override
+	public String getPathTranslated() {
+		return req.getPathTranslated();
+	}
+
+	@Override
+	public String getContextPath() {
+		return req.getContextPath();
+	}
+
+	@Override
+	public String getQueryString() {
+		return req.getQueryString();
+	}
+
+	@Override
+	public String getRemoteUser() {
+		return req.getRemoteUser();
+	}
+
+	@Override
+	public boolean isUserInRole(String role) {
+		return req.isUserInRole(role);
+	}
+
+	@Override
+	public Principal getUserPrincipal() {
+		return req.getUserPrincipal();
+	}
+
+	@Override
+	public String getRequestedSessionId() {
+		return req.getRequestedSessionId();
+	}
+
+	@Override
+	public String getRequestURI() {
+		return req.getRequestURI();
+	}
+
+	@Override
+	public StringBuffer getRequestURL() {
+		return req.getRequestURL();
+	}
+
+	@Override
+	public String getServletPath() {
+		return req.getServletPath();
+	}
+
+	@Override
+	public HttpSession getSession(boolean create) {
+		return req.getSession(create);
+	}
+
+	@Override
+	public HttpSession getSession() {
+		return req.getSession();
+	}
+
+	@Override
+	public boolean isRequestedSessionIdValid() {
+		return req.isRequestedSessionIdValid();
+	}
+
+	@Override
+	public boolean isRequestedSessionIdFromCookie() {
+		return req.isRequestedSessionIdFromCookie();
+	}
+
+	@Override
+	public boolean isRequestedSessionIdFromURL() {
+		return req.isRequestedSessionIdFromURL();
+	}
+
+	@Override
+	@Deprecated
+	public boolean isRequestedSessionIdFromUrl() {
+		return req.isRequestedSessionIdFromUrl();
+	}
+}
\ No newline at end of file
diff --git a/src/com/gitblit/SyndicationFilter.java b/src/com/gitblit/SyndicationFilter.java
new file mode 100644
index 0000000..68f383b
--- /dev/null
+++ b/src/com/gitblit/SyndicationFilter.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+
+public class SyndicationFilter extends AccessRestrictionFilter {
+
+	@Override
+	protected String extractRepositoryName(String url) {
+		return url;
+	}
+
+	@Override
+	protected String getUrlRequestType(String url) {
+		return "RESTRICTED";
+	}
+
+	@Override
+	protected boolean requiresAuthentication(RepositoryModel repository) {
+		return repository.accessRestriction.atLeast(AccessRestrictionType.VIEW);
+	}
+
+	@Override
+	protected boolean canAccess(RepositoryModel repository, UserModel user, String restrictedURL) {
+		return user.canAccessRepository(repository.name);
+	}
+
+}
diff --git a/src/com/gitblit/SyndicationServlet.java b/src/com/gitblit/SyndicationServlet.java
index d2b396e..19865fe 100644
--- a/src/com/gitblit/SyndicationServlet.java
+++ b/src/com/gitblit/SyndicationServlet.java
@@ -15,6 +15,7 @@
  */
 package com.gitblit;
 
+import java.text.MessageFormat;
 import java.util.List;
 
 import javax.servlet.http.HttpServlet;
@@ -28,6 +29,7 @@
 import com.gitblit.utils.JGitUtils;
 import com.gitblit.utils.StringUtils;
 import com.gitblit.utils.SyndicationUtils;
+import com.gitblit.wicket.WicketUtils;
 
 public class SyndicationServlet extends HttpServlet {
 
@@ -36,20 +38,55 @@
 	private transient Logger logger = LoggerFactory.getLogger(SyndicationServlet.class);
 
 	public static String asLink(String baseURL, String repository, String objectId, int length) {
-		if (baseURL.charAt(baseURL.length() - 1) == '/') {
+		if (baseURL.length() > 0 && baseURL.charAt(baseURL.length() - 1) == '/') {
 			baseURL = baseURL.substring(0, baseURL.length() - 1);
 		}
-		return baseURL + Constants.SYNDICATION_SERVLET_PATH + "?r=" + repository
-				+ (objectId == null ? "" : ("&h=" + objectId)) + (length > 0 ? "&l=" + length : "");
+		StringBuilder url = new StringBuilder();
+		url.append(baseURL);
+		url.append(Constants.SYNDICATION_SERVLET_PATH);
+		url.append(repository);
+		if (!StringUtils.isEmpty(objectId) || length > 0) {
+			StringBuilder parameters = new StringBuilder("?");
+			if (StringUtils.isEmpty(objectId)) {
+				parameters.append("l=");
+				parameters.append(length);
+			} else {
+				parameters.append("h=");
+				parameters.append(objectId);
+				if (length > 0) {
+					parameters.append("&l=");
+					parameters.append(length);
+				}
+			}
+			url.append(parameters);
+		}
+		return url.toString();
+	}
+	
+	public static String getTitle(String repository, String objectId) {
+		String id = objectId;
+		if (!StringUtils.isEmpty(id)) {
+			if (id.startsWith(org.eclipse.jgit.lib.Constants.R_HEADS)) {
+				id = id.substring(org.eclipse.jgit.lib.Constants.R_HEADS.length());
+			} else if (id.startsWith(org.eclipse.jgit.lib.Constants.R_REMOTES)) {
+				id = id.substring(org.eclipse.jgit.lib.Constants.R_REMOTES.length());
+			} else if (id.startsWith(org.eclipse.jgit.lib.Constants.R_TAGS)) {
+				id = id.substring(org.eclipse.jgit.lib.Constants.R_TAGS.length());
+			}
+		}
+		return MessageFormat.format("{0} ({1})", repository, id);
 	}
 
 	private void processRequest(javax.servlet.http.HttpServletRequest request,
 			javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException,
 			java.io.IOException {
-		String hostUrl = request.getRequestURL().toString();
-		String servlet = request.getServletPath();
-		hostUrl = hostUrl.substring(0, hostUrl.indexOf(servlet));
-		String repositoryName = request.getParameter("r");
+
+		String hostURL = WicketUtils.getHostURL(request);
+		String url = request.getRequestURI().substring(request.getServletPath().length());
+		if (url.charAt(0) == '/' && url.length() > 1) {
+			url = url.substring(1);
+		}
+		String repositoryName = url;
 		String objectId = request.getParameter("h");
 		String l = request.getParameter("l");
 		int length = GitBlit.getInteger(Keys.web.syndicationEntries, 25);
@@ -62,14 +99,13 @@
 			} catch (NumberFormatException x) {
 			}
 		}
-		
-		// TODO confirm repository is accessible!!
 
 		Repository repository = GitBlit.self().getRepository(repositoryName);
 		RepositoryModel model = GitBlit.self().getRepositoryModel(repositoryName);
 		List<RevCommit> commits = JGitUtils.getRevLog(repository, objectId, 0, length);
 		try {
-			SyndicationUtils.toRSS(hostUrl, model.name + " " + objectId, model.description, model.name, commits, response.getOutputStream());
+			SyndicationUtils.toRSS(hostURL, getTitle(model.name, objectId), model.description,
+					model.name, commits, response.getOutputStream());
 		} catch (Exception e) {
 			logger.error("An error occurred during feed generation", e);
 		}
diff --git a/src/com/gitblit/models/UserModel.java b/src/com/gitblit/models/UserModel.java
index f23fd29..2964708 100644
--- a/src/com/gitblit/models/UserModel.java
+++ b/src/com/gitblit/models/UserModel.java
@@ -16,10 +16,11 @@
 package com.gitblit.models;
 
 import java.io.Serializable;
+import java.security.Principal;
 import java.util.ArrayList;
 import java.util.List;
 
-public class UserModel implements Serializable {
+public class UserModel implements Principal, Serializable {
 
 	private static final long serialVersionUID = 1L;
 
@@ -42,6 +43,11 @@
 	}
 
 	@Override
+	public String getName() {	
+		return username;
+	}
+
+	@Override
 	public String toString() {
 		return username;
 	}
diff --git a/src/com/gitblit/utils/StringUtils.java b/src/com/gitblit/utils/StringUtils.java
index fa84fe8..363efc9 100644
--- a/src/com/gitblit/utils/StringUtils.java
+++ b/src/com/gitblit/utils/StringUtils.java
@@ -16,13 +16,19 @@
 package com.gitblit.utils;
 
 import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.regex.PatternSyntaxException;
 
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jgit.util.Base64;
+
 public class StringUtils {
+
+	public static final String MD5_TYPE = "MD5:";
 
 	public static boolean isEmpty(String value) {
 		return value == null || value.trim().length() == 0;
@@ -48,6 +54,22 @@
 				retStr.append("&nbsp;");
 			} else if (changeSpace && inStr.charAt(i) == '\t') {
 				retStr.append(" &nbsp; &nbsp;");
+			} else {
+				retStr.append(inStr.charAt(i));
+			}
+			i++;
+		}
+		return retStr.toString();
+	}
+
+	public static String encodeURL(String inStr) {
+		StringBuffer retStr = new StringBuffer();
+		int i = 0;
+		while (i < inStr.length()) {
+			if (inStr.charAt(i) == '/') {
+				retStr.append("%2F");
+			} else if (inStr.charAt(i) == ' ') {
+				retStr.append("%20");
 			} else {
 				retStr.append(inStr.charAt(i));
 			}
@@ -116,18 +138,39 @@
 		try {
 			MessageDigest md = MessageDigest.getInstance("SHA-1");
 			md.update(bytes, 0, bytes.length);
-			byte[] sha1hash = md.digest();
-			StringBuilder sb = new StringBuilder(sha1hash.length * 2);
-			for (int i = 0; i < sha1hash.length; i++) {
-				if (((int) sha1hash[i] & 0xff) < 0x10) {
-					sb.append('0');
-				}
-				sb.append(Long.toString((int) sha1hash[i] & 0xff, 16));
-			}
-			return sb.toString();
+			byte[] digest = md.digest();
+			return toHex(digest);
 		} catch (NoSuchAlgorithmException t) {
 			throw new RuntimeException(t);
 		}
+	}
+
+	public static String getMD5(String string) {
+		try {
+			MessageDigest md = MessageDigest.getInstance("MD5");
+			md.reset();
+			md.update(string.getBytes("iso-8859-1"));
+			byte[] digest = md.digest();
+			return toHex(digest);
+		} catch (Exception e) {
+			Log.warn(e);
+			return null;
+		}
+	}
+
+	private static String toHex(byte[] bytes) {
+		StringBuilder sb = new StringBuilder(bytes.length * 2);
+		for (int i = 0; i < bytes.length; i++) {
+			if (((int) bytes[i] & 0xff) < 0x10) {
+				sb.append('0');
+			}
+			sb.append(Long.toString((int) bytes[i] & 0xff, 16));
+		}
+		return sb.toString();
+	}
+
+	public static String decodeBase64(String base64) {
+		return new String(Base64.decode(base64), Charset.forName("UTF-8"));
 	}
 
 	public static String getRootPath(String path) {
@@ -144,11 +187,11 @@
 		}
 		return relativePath;
 	}
-	
+
 	public static List<String> getStringsFromValue(String value) {
 		return getStringsFromValue(value, " ");
 	}
-		
+
 	public static List<String> getStringsFromValue(String value, String separator) {
 		List<String> strings = new ArrayList<String>();
 		try {
diff --git a/src/com/gitblit/utils/SyndicationUtils.java b/src/com/gitblit/utils/SyndicationUtils.java
index da937f9..5763af3 100644
--- a/src/com/gitblit/utils/SyndicationUtils.java
+++ b/src/com/gitblit/utils/SyndicationUtils.java
@@ -24,32 +24,41 @@
 
 import org.eclipse.jgit.revwalk.RevCommit;
 
+import com.gitblit.Constants;
 import com.sun.syndication.feed.synd.SyndContent;
 import com.sun.syndication.feed.synd.SyndContentImpl;
 import com.sun.syndication.feed.synd.SyndEntry;
 import com.sun.syndication.feed.synd.SyndEntryImpl;
 import com.sun.syndication.feed.synd.SyndFeed;
 import com.sun.syndication.feed.synd.SyndFeedImpl;
+import com.sun.syndication.feed.synd.SyndImageImpl;
 import com.sun.syndication.io.FeedException;
 import com.sun.syndication.io.SyndFeedOutput;
 
 public class SyndicationUtils {
 
-	public static void toRSS(String hostUrl, String title, String description, String repository, List<RevCommit> commits, OutputStream os)
-			throws IOException, FeedException {
+	public static void toRSS(String hostUrl, String title, String description, String repository,
+			List<RevCommit> commits, OutputStream os) throws IOException, FeedException {
 
 		SyndFeed feed = new SyndFeedImpl();
-		feed.setFeedType("rss_1.0");
+		feed.setFeedType("rss_2.0");
 		feed.setTitle(title);
-		feed.setLink(MessageFormat.format("{0}/summary/{1}", hostUrl, repository));
+		feed.setLink(MessageFormat.format("{0}/summary/{1}", hostUrl,
+				StringUtils.encodeURL(repository)));
 		feed.setDescription(description);
+		SyndImageImpl image = new SyndImageImpl();
+		image.setTitle(Constants.NAME);
+		image.setUrl(hostUrl + Constants.RESOURCE_PATH + "gitblt_25.png");
+		image.setLink(hostUrl);
+		feed.setImage(image);
 
 		List<SyndEntry> entries = new ArrayList<SyndEntry>();
 		for (RevCommit commit : commits) {
 			SyndEntry entry = new SyndEntryImpl();
 			entry.setTitle(commit.getShortMessage());
 			entry.setAuthor(commit.getAuthorIdent().getName());
-			entry.setLink(MessageFormat.format("{0}/commit/{1}/{2}", hostUrl, repository, commit.getName()));
+			entry.setLink(MessageFormat.format("{0}/commit/{1}/{2}", hostUrl,
+					StringUtils.encodeURL(repository), commit.getName()));
 			entry.setPublishedDate(commit.getCommitterIdent().getWhen());
 
 			SyndContent content = new SyndContentImpl();
diff --git a/src/com/gitblit/wicket/GitBlitWebApp.java b/src/com/gitblit/wicket/GitBlitWebApp.java
index 472a11d..cc54e00 100644
--- a/src/com/gitblit/wicket/GitBlitWebApp.java
+++ b/src/com/gitblit/wicket/GitBlitWebApp.java
@@ -122,7 +122,7 @@
 
 	@Override
 	public final String getConfigurationType() {
-		if (GitBlit.self().isDebugMode()) {
+		if (GitBlit.isDebugMode()) {
 			return Application.DEVELOPMENT;
 		}
 		return Application.DEPLOYMENT;
diff --git a/src/com/gitblit/wicket/WicketUtils.java b/src/com/gitblit/wicket/WicketUtils.java
index 1d2a60f..54f9648 100644
--- a/src/com/gitblit/wicket/WicketUtils.java
+++ b/src/com/gitblit/wicket/WicketUtils.java
@@ -22,11 +22,18 @@
 import java.util.List;
 import java.util.TimeZone;
 
+import javax.servlet.http.HttpServletRequest;
+
 import org.apache.wicket.Component;
 import org.apache.wicket.PageParameters;
+import org.apache.wicket.Request;
+import org.apache.wicket.behavior.HeaderContributor;
 import org.apache.wicket.behavior.SimpleAttributeModifier;
+import org.apache.wicket.markup.html.IHeaderContributor;
+import org.apache.wicket.markup.html.IHeaderResponse;
 import org.apache.wicket.markup.html.basic.Label;
 import org.apache.wicket.markup.html.image.ContextImage;
+import org.apache.wicket.protocol.http.WebRequest;
 import org.apache.wicket.resource.ContextRelativeResource;
 import org.eclipse.jgit.diff.DiffEntry.ChangeType;
 import org.eclipse.jgit.lib.Constants;
@@ -162,7 +169,7 @@
 	}
 
 	public static ContextImage newImage(String wicketId, String file, String tooltip) {
-		ContextImage img = new ContextImage(wicketId, "/com/gitblit/wicket/resources/" + file);
+		ContextImage img = new ContextImage(wicketId, com.gitblit.Constants.RESOURCE_PATH + file);
 		if (!StringUtils.isEmpty(tooltip)) {
 			setHtmlTooltip(img, tooltip);
 		}
@@ -170,7 +177,42 @@
 	}
 
 	public static ContextRelativeResource getResource(String file) {
-		return new ContextRelativeResource("/com/gitblit/wicket/resources/" + file);
+		return new ContextRelativeResource(com.gitblit.Constants.RESOURCE_PATH + file);
+	}
+
+	public static String getHostURL(Request request) {
+		HttpServletRequest req = ((WebRequest) request).getHttpServletRequest();
+		return getHostURL(req);
+	}
+
+	public static String getHostURL(HttpServletRequest request) {
+		StringBuilder sb = new StringBuilder();
+		sb.append(request.getScheme());
+		sb.append("://");
+		sb.append(request.getServerName());
+		if ((request.getScheme().equals("http") && request.getServerPort() != 80)
+				|| (request.getScheme().equals("https") && request.getServerPort() != 443)) {
+			sb.append(":" + request.getServerPort());
+		}
+		return sb.toString();
+	}
+
+	public static HeaderContributor syndicationDiscoveryLink(final String feedTitle,
+			final String url) {
+		return new HeaderContributor(new IHeaderContributor() {
+			private static final long serialVersionUID = 1L;
+
+			public void renderHead(IHeaderResponse response) {
+				String contentType = "application/rss+xml";
+
+				StringBuffer buffer = new StringBuffer();
+				buffer.append("<link rel=\"alternate\" ");
+				buffer.append("type=\"").append(contentType).append("\" ");
+				buffer.append("title=\"").append(feedTitle).append("\" ");
+				buffer.append("href=\"").append(url).append("\" />");
+				response.renderString(buffer.toString());
+			}
+		});
 	}
 
 	public static PageParameters newUsernameParameter(String username) {
diff --git a/src/com/gitblit/wicket/pages/CommitPage.java b/src/com/gitblit/wicket/pages/CommitPage.java
index 6da962e..a34917b 100644
--- a/src/com/gitblit/wicket/pages/CommitPage.java
+++ b/src/com/gitblit/wicket/pages/CommitPage.java
@@ -128,7 +128,7 @@
 						SearchType.AUTHOR));
 				item.add(WicketUtils.createTimestampLabel("authorDate", entry.notesRef
 						.getAuthorIdent().getWhen(), getTimeZone()));
-				item.add(new Label("noteContent", substituteText(entry.content))
+				item.add(new Label("noteContent", GitBlit.self().processCommitMessage(repositoryName, entry.content))
 						.setEscapeModelStrings(false));
 			}
 		};
diff --git a/src/com/gitblit/wicket/pages/EditUserPage.java b/src/com/gitblit/wicket/pages/EditUserPage.java
index eafec05..6391627 100644
--- a/src/com/gitblit/wicket/pages/EditUserPage.java
+++ b/src/com/gitblit/wicket/pages/EditUserPage.java
@@ -31,8 +31,6 @@
 import org.apache.wicket.model.Model;
 import org.apache.wicket.model.util.CollectionModel;
 import org.apache.wicket.model.util.ListModel;
-import org.eclipse.jetty.http.security.Credential.Crypt;
-import org.eclipse.jetty.http.security.Credential.MD5;
 
 import com.gitblit.Constants.AccessRestrictionType;
 import com.gitblit.GitBlit;
@@ -114,8 +112,7 @@
 					return;
 				}
 				String password = userModel.password;
-				if (!password.toUpperCase().startsWith(Crypt.__TYPE)
-						&& !password.toUpperCase().startsWith(MD5.__TYPE)) {
+				if (!password.toUpperCase().startsWith(StringUtils.MD5_TYPE)) {
 					// This is a plain text password.
 					// Check length.
 					int minLength = GitBlit.getInteger(Keys.realm.minPasswordLength, 5);
@@ -133,7 +130,7 @@
 					String type = GitBlit.getString(Keys.realm.passwordStorage, "md5");
 					if (type.equalsIgnoreCase("md5")) {
 						// store MD5 digest of password
-						userModel.password = MD5.digest(userModel.password);
+						userModel.password = StringUtils.MD5_TYPE + StringUtils.getMD5(userModel.password);
 					}
 				}
 
diff --git a/src/com/gitblit/wicket/pages/LogPage.java b/src/com/gitblit/wicket/pages/LogPage.java
index 35f8a73..2cd787c 100644
--- a/src/com/gitblit/wicket/pages/LogPage.java
+++ b/src/com/gitblit/wicket/pages/LogPage.java
@@ -26,6 +26,8 @@
 	public LogPage(PageParameters params) {
 		super(params);
 
+		addSyndicationDiscoveryLink();
+		
 		int pageNumber = WicketUtils.getPage(params);
 		int prevPage = Math.max(0, pageNumber - 1);
 		int nextPage = pageNumber + 1;
diff --git a/src/com/gitblit/wicket/pages/RepositoryPage.java b/src/com/gitblit/wicket/pages/RepositoryPage.java
index c3a6b03..cf14ee1 100644
--- a/src/com/gitblit/wicket/pages/RepositoryPage.java
+++ b/src/com/gitblit/wicket/pages/RepositoryPage.java
@@ -21,7 +21,6 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
 
 import org.apache.wicket.Component;
 import org.apache.wicket.PageParameters;
@@ -159,7 +158,7 @@
 			}
 		};
 		add(extrasView);
-		
+
 		add(new ExternalLink("syndication", SyndicationServlet.asLink(getRequest()
 				.getRelativePathPrefixToContextRoot(), repositoryName, null, 0)));
 
@@ -187,6 +186,12 @@
 				break;
 			}
 		}
+	}
+
+	protected void addSyndicationDiscoveryLink() {
+		add(WicketUtils.syndicationDiscoveryLink(SyndicationServlet.getTitle(repositoryName,
+				objectId), SyndicationServlet.asLink(getRequest()
+				.getRelativePathPrefixToContextRoot(), repositoryName, objectId, 0)));
 	}
 
 	protected Repository getRepository() {
@@ -234,46 +239,11 @@
 	protected void addFullText(String wicketId, String text, boolean substituteRegex) {
 		String html;
 		if (substituteRegex) {
-			html = substituteText(text);
+			html = GitBlit.self().processCommitMessage(repositoryName, text);
 		} else {
 			html = StringUtils.breakLinesForHtml(text);
 		}
 		add(new Label(wicketId, html).setEscapeModelStrings(false));
-	}
-
-	protected String substituteText(String text) {
-		String html = StringUtils.breakLinesForHtml(text);
-		Map<String, String> map = new HashMap<String, String>();
-		// global regex keys
-		if (GitBlit.getBoolean(Keys.regex.global, false)) {
-			for (String key : GitBlit.getAllKeys(Keys.regex.global)) {
-				if (!key.equals(Keys.regex.global)) {
-					String subKey = key.substring(key.lastIndexOf('.') + 1);
-					map.put(subKey, GitBlit.getString(key, ""));
-				}
-			}
-		}
-
-		// repository-specific regex keys
-		List<String> keys = GitBlit.getAllKeys(Keys.regex._ROOT + "."
-				+ repositoryName.toLowerCase());
-		for (String key : keys) {
-			String subKey = key.substring(key.lastIndexOf('.') + 1);
-			map.put(subKey, GitBlit.getString(key, ""));
-		}
-
-		for (Entry<String, String> entry : map.entrySet()) {
-			String definition = entry.getValue().trim();
-			String[] chunks = definition.split("!!!");
-			if (chunks.length == 2) {
-				html = html.replaceAll(chunks[0], chunks[1]);
-			} else {
-				logger.warn(entry.getKey()
-						+ " improperly formatted.  Use !!! to separate match from replacement: "
-						+ definition);
-			}
-		}
-		return html;
 	}
 
 	protected abstract String getPageName();
diff --git a/src/com/gitblit/wicket/pages/SummaryPage.java b/src/com/gitblit/wicket/pages/SummaryPage.java
index e85901a..0d0db86 100644
--- a/src/com/gitblit/wicket/pages/SummaryPage.java
+++ b/src/com/gitblit/wicket/pages/SummaryPage.java
@@ -22,12 +22,9 @@
 import java.util.ArrayList;
 import java.util.List;
 
-import javax.servlet.http.HttpServletRequest;
-
 import org.apache.wicket.PageParameters;
 import org.apache.wicket.markup.html.basic.Label;
 import org.apache.wicket.markup.html.link.BookmarkablePageLink;
-import org.apache.wicket.protocol.http.WebRequest;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.wicketstuff.googlecharts.Chart;
@@ -81,6 +78,8 @@
 			metrics = MetricUtils.getDateMetrics(r, null, true, null);
 			metricsTotal = metrics.remove(0);
 		}
+		
+		addSyndicationDiscoveryLink();
 
 		// repository description
 		add(new Label("repositoryDescription", getRepositoryModel().description));
@@ -121,17 +120,8 @@
 			default:
 				add(WicketUtils.newClearPixel("accessRestrictionIcon").setVisible(false));
 			}
-
-			HttpServletRequest req = ((WebRequest) getRequestCycle().getRequest())
-					.getHttpServletRequest();
 			StringBuilder sb = new StringBuilder();
-			sb.append(req.getScheme());
-			sb.append("://");
-			sb.append(req.getServerName());
-			if ((req.getScheme().equals("http") && req.getServerPort() != 80)
-					|| (req.getScheme().equals("https") && req.getServerPort() != 443)) {
-				sb.append(":" + req.getServerPort());
-			}
+			sb.append(WicketUtils.getHostURL(getRequestCycle().getRequest()));					
 			sb.append(Constants.GIT_SERVLET_PATH);
 			sb.append(repositoryName);
 			repositoryUrls.add(sb.toString());
diff --git a/src/com/gitblit/wicket/panels/RepositoriesPanel.html b/src/com/gitblit/wicket/panels/RepositoriesPanel.html
index a599d22..1e609e1 100644
--- a/src/com/gitblit/wicket/panels/RepositoriesPanel.html
+++ b/src/com/gitblit/wicket/panels/RepositoriesPanel.html
@@ -50,7 +50,7 @@
 			<th wicket:id="orderByOwner"><wicket:message key="gb.owner">Owner</wicket:message></th>
 			<th></th>
 			<th wicket:id="orderByDate"><wicket:message key="gb.lastChange">Last Change</wicket:message></th>
-			<th clas="right"></th>
+			<th class="right"></th>
 		</tr>
 	</wicket:fragment>
 	
@@ -80,7 +80,12 @@
         <td class="author"><span wicket:id="repositoryOwner">[repository owner]</span></td>
         <td style="text-align: right;padding-right:10px;"><img class="inlineIcon" wicket:id="ticketsIcon" /><img class="inlineIcon" wicket:id="docsIcon" /><img class="inlineIcon" wicket:id="frozenIcon" /><img class="inlineIcon" wicket:id="accessRestrictionIcon" /></td>
         <td><span wicket:id="repositoryLastChange">[last change]</span></td>
-        <td class="rightAlign"><span wicket:id="repositoryLinks"></span></td>
+        <td class="rightAlign">
+        	<span wicket:id="repositoryLinks"></span>
+			<a style="text-decoration: none;" wicket:id="syndication">
+				<img style="border:0px;vertical-align:middle;" src="/com/gitblit/wicket/resources/feed_16x16.png"></img>
+			</a>
+		</td>				
 	</wicket:fragment>
 	
 </wicket:panel>
diff --git a/src/com/gitblit/wicket/panels/RepositoriesPanel.java b/src/com/gitblit/wicket/panels/RepositoriesPanel.java
index a0c9e13..c744148 100644
--- a/src/com/gitblit/wicket/panels/RepositoriesPanel.java
+++ b/src/com/gitblit/wicket/panels/RepositoriesPanel.java
@@ -31,6 +31,7 @@
 import org.apache.wicket.extensions.markup.html.repeater.util.SortableDataProvider;
 import org.apache.wicket.markup.html.basic.Label;
 import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.markup.html.link.ExternalLink;
 import org.apache.wicket.markup.html.link.Link;
 import org.apache.wicket.markup.html.panel.Fragment;
 import org.apache.wicket.markup.repeater.Item;
@@ -43,6 +44,7 @@
 import com.gitblit.Constants.AccessRestrictionType;
 import com.gitblit.GitBlit;
 import com.gitblit.Keys;
+import com.gitblit.SyndicationServlet;
 import com.gitblit.models.RepositoryModel;
 import com.gitblit.models.UserModel;
 import com.gitblit.utils.StringUtils;
@@ -215,6 +217,8 @@
 				} else {
 					row.add(new Label("repositoryLinks"));
 				}
+				row.add(new ExternalLink("syndication", SyndicationServlet.asLink(getRequest()
+						.getRelativePathPrefixToContextRoot(), entry.name, null, 0)));
 				WicketUtils.setAlternatingBackground(item, counter);
 				counter++;
 			}
diff --git a/tests/com/gitblit/tests/GitBlitSuite.java b/tests/com/gitblit/tests/GitBlitSuite.java
index e13e1bb..c9e383e 100644
--- a/tests/com/gitblit/tests/GitBlitSuite.java
+++ b/tests/com/gitblit/tests/GitBlitSuite.java
@@ -24,10 +24,10 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.storage.file.FileRepository;
 
+import com.gitblit.FileLoginService;
 import com.gitblit.FileSettings;
 import com.gitblit.GitBlit;
 import com.gitblit.GitBlitException;
-import com.gitblit.JettyLoginService;
 import com.gitblit.models.RepositoryModel;
 import com.gitblit.utils.JGitUtils;
 
@@ -72,8 +72,7 @@
 	protected void setUp() throws Exception {
 		FileSettings settings = new FileSettings("distrib/gitblit.properties");
 		GitBlit.self().configureContext(settings);
-		JettyLoginService loginService = new JettyLoginService(new File("distrib/users.properties"));
-		loginService.loadUsers();
+		FileLoginService loginService = new FileLoginService(new File("distrib/users.properties"));
 		GitBlit.self().setLoginService(loginService);
 
 		if (REPOSITORIES.exists() || REPOSITORIES.mkdirs()) {

--
Gitblit v1.9.1