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(" "); } else if (changeSpace && inStr.charAt(i) == '\t') { retStr.append(" "); + } 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