James Moger
2011-06-14 8c9a2037b5c0fed881a3ad6dd9cff364eed603d9
Added AccessRestrictionFilter and simplified authentication.

Replaced servlet container basic authentication with a custom servlet
filter which performs the same function. The advantage to this is
that the servlet container is now divorced from the webapp.

The login service (realm) also simplified a great deal and removes its
Jetty dependencies.

Additionally, the basic authorization pop-up will be displayed as
needed based on the repository's access restriction. This was
necessary for view-restricted repositories with the RSS feature. Its
also necessary for completely open repositories as before it would
prompt for credentials.

Improved feed syndication feature.
4 files added
1 files renamed
20 files modified
1 files deleted
1440 ■■■■ changed files
docs/00_index.mkd 3 ●●●● patch | view | raw | blame | history
src/com/gitblit/AccessRestrictionFilter.java 240 ●●●●● patch | view | raw | blame | history
src/com/gitblit/Constants.java 2 ●●●●● patch | view | raw | blame | history
src/com/gitblit/DownloadZipServlet.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/FileLoginService.java 202 ●●●● patch | view | raw | blame | history
src/com/gitblit/FileSettings.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/GitBlit.java 45 ●●●●● patch | view | raw | blame | history
src/com/gitblit/GitBlitServer.java 96 ●●●●● patch | view | raw | blame | history
src/com/gitblit/GitBlitServlet.java 108 ●●●●● patch | view | raw | blame | history
src/com/gitblit/GitFilter.java 98 ●●●●● patch | view | raw | blame | history
src/com/gitblit/ServletRequestWrapper.java 311 ●●●●● patch | view | raw | blame | history
src/com/gitblit/SyndicationFilter.java 44 ●●●●● patch | view | raw | blame | history
src/com/gitblit/SyndicationServlet.java 56 ●●●● patch | view | raw | blame | history
src/com/gitblit/models/UserModel.java 8 ●●●● patch | view | raw | blame | history
src/com/gitblit/utils/StringUtils.java 65 ●●●● patch | view | raw | blame | history
src/com/gitblit/utils/SyndicationUtils.java 19 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/GitBlitWebApp.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/WicketUtils.java 46 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/CommitPage.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditUserPage.java 7 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/LogPage.java 2 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/RepositoryPage.java 46 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/SummaryPage.java 16 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/RepositoriesPanel.html 9 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/RepositoriesPanel.java 4 ●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/GitBlitSuite.java 5 ●●●●● patch | view | raw | blame | history
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
src/com/gitblit/AccessRestrictionFilter.java
New file
@@ -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;
        }
    }
}
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 = "***********************************************************";
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
src/com/gitblit/FileLoginService.java
File was renamed from src/com/gitblit/JettyLoginService.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;
    }
}
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();
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);
        }
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();
src/com/gitblit/GitBlitServlet.java
File was deleted
src/com/gitblit/GitFilter.java
New file
@@ -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;
    }
}
src/com/gitblit/ServletRequestWrapper.java
New file
@@ -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();
    }
}
src/com/gitblit/SyndicationFilter.java
New file
@@ -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);
    }
}
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);
        }
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;
    }
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 {
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();
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;
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) {
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));
            }
        };
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);
                    }
                }
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;
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();
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());
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>
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++;
            }
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()) {