/* * Copyright 2014 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.servlet; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.text.MessageFormat; import java.text.ParseException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.tika.Tika; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.MutableObjectId; import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.PathFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.gitblit.Constants; import com.gitblit.Keys; import com.gitblit.manager.IRepositoryManager; import com.gitblit.manager.IRuntimeManager; import com.gitblit.models.PathModel; import com.gitblit.utils.ByteFormat; import com.gitblit.utils.JGitUtils; import com.gitblit.utils.MarkdownUtils; import com.gitblit.utils.StringUtils; import com.google.inject.Inject; import com.google.inject.Singleton; /** * Serves the content of a branch. * * @author James Moger * */ @Singleton public class RawServlet extends HttpServlet { private static final long serialVersionUID = 1L; private transient Logger logger = LoggerFactory.getLogger(RawServlet.class); private final IRuntimeManager runtimeManager; private final IRepositoryManager repositoryManager; @Inject public RawServlet( IRuntimeManager runtimeManager, IRepositoryManager repositoryManager) { this.runtimeManager = runtimeManager; this.repositoryManager = repositoryManager; } /** * Returns an url to this servlet for the specified parameters. * * @param baseURL * @param repository * @param branch * @param path * @return an url */ public static String asLink(String baseURL, String repository, String branch, String path) { if (baseURL.length() > 0 && baseURL.charAt(baseURL.length() - 1) == '/') { baseURL = baseURL.substring(0, baseURL.length() - 1); } char fsc = '!'; char c = GitblitContext.getManager(IRuntimeManager.class).getSettings().getChar(Keys.web.forwardSlashCharacter, '/'); if (c != '/') { fsc = c; } if (branch != null) { branch = Repository.shortenRefName(branch).replace('/', fsc); } String encodedPath = path == null ? "" : path.replace('/', fsc); return baseURL + Constants.RAW_PATH + repository + "/" + (branch == null ? "" : (branch + "/" + encodedPath)); } protected String getBranch(String repository, HttpServletRequest request) { String pi = request.getPathInfo(); String branch = pi.substring(pi.indexOf(repository) + repository.length() + 1); int fs = branch.indexOf('/'); if (fs > -1) { branch = branch.substring(0, fs); } char c = runtimeManager.getSettings().getChar(Keys.web.forwardSlashCharacter, '/'); return branch.replace('!', '/').replace(c, '/'); } protected String getPath(String repository, String branch, HttpServletRequest request) { String base = repository + "/" + branch; String pi = request.getPathInfo().substring(1); if (pi.equals(base)) { return ""; } String path = pi.substring(pi.indexOf(base) + base.length() + 1); if (path.endsWith("/")) { path = path.substring(0, path.length() - 1); } char c = runtimeManager.getSettings().getChar(Keys.web.forwardSlashCharacter, '/'); return path.replace('!', '/').replace(c, '/'); } protected boolean renderIndex() { return false; } /** * Retrieves the specified resource from the specified branch of the * repository. * * @param request * @param response * @throws javax.servlet.ServletException * @throws java.io.IOException */ private void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String path = request.getPathInfo(); if (path.toLowerCase().endsWith(".git")) { // forward to url with trailing / // this is important for relative pages links response.sendRedirect(request.getServletPath() + path + "/"); return; } if (path.charAt(0) == '/') { // strip leading / path = path.substring(1); } // determine repository and resource from url String repository = ""; Repository r = null; int offset = 0; while (r == null) { int slash = path.indexOf('/', offset); if (slash == -1) { repository = path; } else { repository = path.substring(0, slash); } offset = ( slash + 1 ); r = repositoryManager.getRepository(repository, false); if (repository.equals(path)) { // either only repository in url or no repository found break; } } ServletContext context = request.getSession().getServletContext(); try { if (r == null) { // repository not found! String mkd = MessageFormat.format( "# Error\nSorry, no valid **repository** specified in this url: {0}!", path); error(response, mkd); return; } // identify the branch String branch = getBranch(repository, request); if (StringUtils.isEmpty(branch)) { branch = r.getBranch(); if (branch == null) { // no branches found! empty? String mkd = MessageFormat.format( "# Error\nSorry, no valid **branch** specified in this url: {0}!", path); error(response, mkd); } else { // redirect to default branch String base = request.getRequestURI(); String url = base + branch + "/"; response.sendRedirect(url); } return; } // identify the requested path String requestedPath = getPath(repository, branch, request); // identify the commit RevCommit commit = JGitUtils.getCommit(r, branch); if (commit == null) { // branch not found! String mkd = MessageFormat.format( "# Error\nSorry, the repository {0} does not have a **{1}** branch!", repository, branch); error(response, mkd); return; } Map quickContentTypes = new HashMap<>(); quickContentTypes.put("html", "text/html"); quickContentTypes.put("htm", "text/html"); quickContentTypes.put("xml", "application/xml"); quickContentTypes.put("json", "application/json"); List pathEntries = JGitUtils.getFilesInPath(r, requestedPath, commit); if (pathEntries.isEmpty()) { // requested a specific resource String file = StringUtils.getLastPathElement(requestedPath); try { String ext = StringUtils.getFileExtension(file).toLowerCase(); String contentType = quickContentTypes.get(ext); if (contentType == null) { List exts = runtimeManager.getSettings().getStrings(Keys.web.prettyPrintExtensions); if (exts.contains(ext)) { // extension is a registered text type for pretty printing contentType = "text/plain"; } else { // query Tika for the content type Tika tika = new Tika(); contentType = tika.detect(file); } } if (contentType == null) { // ask the container for the content type contentType = context.getMimeType(requestedPath); if (contentType == null) { // still unknown content type, assume binary contentType = "application/octet-stream"; } } if (isTextType(contentType) || isTextDataType(contentType)) { // load, interpret, and serve text content as UTF-8 String [] encodings = runtimeManager.getSettings().getStrings(Keys.web.blobEncodings).toArray(new String[0]); String content = JGitUtils.getStringContent(r, commit.getTree(), requestedPath, encodings); if (content == null) { logger.error("RawServlet Failed to load {} {} {}", repository, commit.getName(), path); notFound(response, requestedPath, branch); return; } byte [] bytes = content.getBytes(Constants.ENCODING); setContentType(response, contentType); response.setContentLength(bytes.length); ByteArrayInputStream is = new ByteArrayInputStream(bytes); sendContent(response, JGitUtils.getCommitDate(commit), is); } else { // stream binary content directly from the repository if (!streamFromRepo(request, response, r, commit, requestedPath)) { logger.error("RawServlet Failed to load {} {} {}", repository, commit.getName(), path); notFound(response, requestedPath, branch); } } return; } catch (Exception e) { logger.error(null, e); } } else { // path request if (!request.getPathInfo().endsWith("/")) { // redirect to trailing '/' url response.sendRedirect(request.getServletPath() + request.getPathInfo() + "/"); return; } if (renderIndex()) { // locate and render an index file Map names = new TreeMap(); for (PathModel entry : pathEntries) { names.put(entry.name.toLowerCase(), entry.name); } List extensions = new ArrayList(); extensions.add("html"); extensions.add("htm"); String content = null; for (String ext : extensions) { String key = "index." + ext; if (names.containsKey(key)) { String fileName = names.get(key); String fullPath = fileName; if (!requestedPath.isEmpty()) { fullPath = requestedPath + "/" + fileName; } String [] encodings = runtimeManager.getSettings().getStrings(Keys.web.blobEncodings).toArray(new String[0]); String stringContent = JGitUtils.getStringContent(r, commit.getTree(), fullPath, encodings); if (stringContent == null) { continue; } content = stringContent; requestedPath = fullPath; break; } } response.setContentType("text/html; charset=" + Constants.ENCODING); byte [] bytes = content.getBytes(Constants.ENCODING); response.setContentLength(bytes.length); ByteArrayInputStream is = new ByteArrayInputStream(bytes); sendContent(response, JGitUtils.getCommitDate(commit), is); return; } } // no content, document list or 404 page if (pathEntries.isEmpty()) { // default 404 page notFound(response, requestedPath, branch); return; } else { // // directory list // response.setContentType("text/html"); response.getWriter().append(""); response.getWriter().append(""); response.getWriter().append(""); response.getWriter().append(""); response.getWriter().append(""); String pattern = ""; final ByteFormat byteFormat = new ByteFormat(); if (!pathEntries.isEmpty()) { if (pathEntries.get(0).path.indexOf('/') > -1) { // we are in a subdirectory, add parent directory link String pp = URLEncoder.encode(requestedPath, Constants.ENCODING); pathEntries.add(0, new PathModel("..", pp + "/..", 0, FileMode.TREE.getBits(), null, null)); } } String basePath = request.getServletPath() + request.getPathInfo(); if (basePath.charAt(basePath.length() - 1) == '/') { // strip trailing slash basePath = basePath.substring(0, basePath.length() - 1); } for (PathModel entry : pathEntries) { String pp = URLEncoder.encode(entry.name, Constants.ENCODING); response.getWriter().append(MessageFormat.format(pattern, basePath, pp, JGitUtils.getPermissionsFromMode(entry.mode), entry.isFile() ? byteFormat.format(entry.size) : "")); } response.getWriter().append(""); response.getWriter().append("
pathmodesize
{1}{2}{3}
"); } } catch (Throwable t) { logger.error("Failed to write page to client", t); } finally { r.close(); } } protected boolean isTextType(String contentType) { if (contentType.startsWith("text/") || "application/json".equals(contentType) || "application/xml".equals(contentType)) { return true; } return false; } protected boolean isTextDataType(String contentType) { if ("image/svg+xml".equals(contentType)) { return true; } return false; } /** * Override all text types to be plain text. * * @param response * @param contentType */ protected void setContentType(HttpServletResponse response, String contentType) { if (isTextType(contentType)) { response.setContentType("text/plain"); } else { response.setContentType(contentType); } } protected boolean streamFromRepo(HttpServletRequest request, HttpServletResponse response, Repository repository, RevCommit commit, String requestedPath) throws IOException { boolean served = false; RevWalk rw = new RevWalk(repository); TreeWalk tw = new TreeWalk(repository); try { tw.reset(); tw.addTree(commit.getTree()); PathFilter f = PathFilter.create(requestedPath); tw.setFilter(f); tw.setRecursive(true); MutableObjectId id = new MutableObjectId(); ObjectReader reader = tw.getObjectReader(); while (tw.next()) { FileMode mode = tw.getFileMode(0); if (mode == FileMode.GITLINK || mode == FileMode.TREE) { continue; } tw.getObjectId(id, 0); String filename = StringUtils.getLastPathElement(requestedPath); try { String userAgent = request.getHeader("User-Agent"); if (userAgent != null && userAgent.indexOf("MSIE 5.5") > -1) { response.setHeader("Content-Disposition", "filename=\"" + URLEncoder.encode(filename, Constants.ENCODING) + "\""); } else if (userAgent != null && userAgent.indexOf("MSIE") > -1) { response.setHeader("Content-Disposition", "attachment; filename=\"" + URLEncoder.encode(filename, Constants.ENCODING) + "\""); } else { response.setHeader("Content-Disposition", "attachment; filename=\"" + new String(filename.getBytes(Constants.ENCODING), "latin1") + "\""); } } catch (UnsupportedEncodingException e) { response.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\""); } long len = reader.getObjectSize(id, org.eclipse.jgit.lib.Constants.OBJ_BLOB); setContentType(response, "application/octet-stream"); response.setIntHeader("Content-Length", (int) len); ObjectLoader ldr = repository.open(id); ldr.copyTo(response.getOutputStream()); served = true; } } finally { tw.close(); rw.dispose(); } response.flushBuffer(); return served; } protected void sendContent(HttpServletResponse response, Date date, InputStream is) throws ServletException, IOException { try { byte[] tmp = new byte[8192]; int len = 0; while ((len = is.read(tmp)) > -1) { response.getOutputStream().write(tmp, 0, len); } } finally { is.close(); } response.flushBuffer(); } protected void notFound(HttpServletResponse response, String requestedPath, String branch) throws ParseException, ServletException, IOException { String str = MessageFormat.format( "# Error\nSorry, the requested resource **{0}** was not found in **{1}**.", requestedPath, branch); response.setStatus(HttpServletResponse.SC_NOT_FOUND); error(response, str); } private void error(HttpServletResponse response, String mkd) throws ServletException, IOException, ParseException { String content = MarkdownUtils.transformMarkdown(mkd); response.setContentType("text/html; charset=" + Constants.ENCODING); response.getWriter().write(content); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { processRequest(request, response); } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { processRequest(request, response); } }