James Moger
2015-12-09 afc9deedc7da2b5a3d183941d63e13bfbfb7c78a
commit | author | age
ca31f5 1 /*
JM 2  * Copyright 2014 gitblit.com.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.gitblit.servlet;
17
18 import java.io.ByteArrayInputStream;
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.io.UnsupportedEncodingException;
22 import java.net.URLEncoder;
23 import java.text.MessageFormat;
24 import java.text.ParseException;
25 import java.util.ArrayList;
26 import java.util.Date;
5cc0a6 27 import java.util.HashMap;
ca31f5 28 import java.util.List;
JM 29 import java.util.Map;
30 import java.util.TreeMap;
31
32 import javax.servlet.ServletContext;
33 import javax.servlet.ServletException;
1b34b0 34 import javax.servlet.http.HttpServlet;
ca31f5 35 import javax.servlet.http.HttpServletRequest;
JM 36 import javax.servlet.http.HttpServletResponse;
37
38 import org.apache.tika.Tika;
39 import org.eclipse.jgit.lib.FileMode;
553e0f 40 import org.eclipse.jgit.lib.MutableObjectId;
JM 41 import org.eclipse.jgit.lib.ObjectLoader;
42 import org.eclipse.jgit.lib.ObjectReader;
ca31f5 43 import org.eclipse.jgit.lib.Repository;
JM 44 import org.eclipse.jgit.revwalk.RevCommit;
553e0f 45 import org.eclipse.jgit.revwalk.RevWalk;
JM 46 import org.eclipse.jgit.treewalk.TreeWalk;
47 import org.eclipse.jgit.treewalk.filter.PathFilter;
ca31f5 48 import org.slf4j.Logger;
JM 49 import org.slf4j.LoggerFactory;
50
51 import com.gitblit.Constants;
1946fe 52 import com.gitblit.Keys;
ca31f5 53 import com.gitblit.manager.IRepositoryManager;
1946fe 54 import com.gitblit.manager.IRuntimeManager;
ca31f5 55 import com.gitblit.models.PathModel;
JM 56 import com.gitblit.utils.ByteFormat;
57 import com.gitblit.utils.JGitUtils;
58 import com.gitblit.utils.MarkdownUtils;
59 import com.gitblit.utils.StringUtils;
c8b728 60 import com.google.inject.Inject;
JM 61 import com.google.inject.Singleton;
ca31f5 62
JM 63 /**
64  * Serves the content of a branch.
65  *
66  * @author James Moger
67  *
68  */
1b34b0 69 @Singleton
JM 70 public class RawServlet extends HttpServlet {
ca31f5 71
JM 72     private static final long serialVersionUID = 1L;
73
ff17f7 74     private transient Logger logger = LoggerFactory.getLogger(RawServlet.class);
ca31f5 75
1b34b0 76     private final IRuntimeManager runtimeManager;
1946fe 77
1b34b0 78     private final IRepositoryManager repositoryManager;
ca31f5 79
1b34b0 80     @Inject
JM 81     public RawServlet(
82             IRuntimeManager runtimeManager,
83             IRepositoryManager repositoryManager) {
84
85         this.runtimeManager = runtimeManager;
86         this.repositoryManager = repositoryManager;
ca31f5 87     }
JM 88
89     /**
90      * Returns an url to this servlet for the specified parameters.
91      *
92      * @param baseURL
93      * @param repository
94      * @param branch
95      * @param path
96      * @return an url
97      */
98     public static String asLink(String baseURL, String repository, String branch, String path) {
99         if (baseURL.length() > 0 && baseURL.charAt(baseURL.length() - 1) == '/') {
100             baseURL = baseURL.substring(0, baseURL.length() - 1);
101         }
477412 102
58562a 103         char fsc = '!';
JM 104         char c = GitblitContext.getManager(IRuntimeManager.class).getSettings().getChar(Keys.web.forwardSlashCharacter, '/');
105         if (c != '/') {
106             fsc = c;
107         }
477412 108         if (branch != null) {
846ee5 109             branch = Repository.shortenRefName(branch).replace('/', fsc);
477412 110         }
JM 111
9ceddb 112         String encodedPath = path == null ? "" : path.replace('/', fsc);
JM 113         return baseURL + Constants.RAW_PATH + repository + "/" + (branch == null ? "" : (branch + "/" + encodedPath));
ca31f5 114     }
JM 115
116     protected String getBranch(String repository, HttpServletRequest request) {
117         String pi = request.getPathInfo();
118         String branch = pi.substring(pi.indexOf(repository) + repository.length() + 1);
119         int fs = branch.indexOf('/');
120         if (fs > -1) {
121             branch = branch.substring(0, fs);
122         }
477412 123         char c = runtimeManager.getSettings().getChar(Keys.web.forwardSlashCharacter, '/');
JM 124         return branch.replace('!', '/').replace(c, '/');
ca31f5 125     }
JM 126
127     protected String getPath(String repository, String branch, HttpServletRequest request) {
128         String base = repository + "/" + branch;
129         String pi = request.getPathInfo().substring(1);
130         if (pi.equals(base)) {
131             return "";
132         }
133         String path = pi.substring(pi.indexOf(base) + base.length() + 1);
134         if (path.endsWith("/")) {
135             path = path.substring(0, path.length() - 1);
136         }
f010ef 137         char c = runtimeManager.getSettings().getChar(Keys.web.forwardSlashCharacter, '/');
JM 138         return path.replace('!', '/').replace(c, '/');
ca31f5 139     }
JM 140
141     protected boolean renderIndex() {
142         return false;
143     }
144
145     /**
146      * Retrieves the specified resource from the specified branch of the
147      * repository.
148      *
149      * @param request
150      * @param response
151      * @throws javax.servlet.ServletException
152      * @throws java.io.IOException
153      */
154     private void processRequest(HttpServletRequest request, HttpServletResponse response)
155             throws ServletException, IOException {
156         String path = request.getPathInfo();
157         if (path.toLowerCase().endsWith(".git")) {
158             // forward to url with trailing /
159             // this is important for relative pages links
160             response.sendRedirect(request.getServletPath() + path + "/");
161             return;
162         }
163         if (path.charAt(0) == '/') {
164             // strip leading /
165             path = path.substring(1);
166         }
167
168         // determine repository and resource from url
2b1e02 169         String repository = path;
ca31f5 170         Repository r = null;
2b1e02 171         int terminator = repository.length();
P 172         do {
173             repository = repository.substring(0, terminator);
ca31f5 174             r = repositoryManager.getRepository(repository, false);
2b1e02 175             terminator = repository.lastIndexOf('/');
P 176         } while (r == null && terminator > -1 );
ca31f5 177
JM 178         ServletContext context = request.getSession().getServletContext();
179
180         try {
181             if (r == null) {
182                 // repository not found!
183                 String mkd = MessageFormat.format(
184                         "# Error\nSorry, no valid **repository** specified in this url: {0}!",
185                         path);
186                 error(response, mkd);
187                 return;
188             }
189
190             // identify the branch
191             String branch = getBranch(repository, request);
192             if (StringUtils.isEmpty(branch)) {
193                 branch = r.getBranch();
194                 if (branch == null) {
195                     // no branches found!  empty?
196                     String mkd = MessageFormat.format(
197                             "# Error\nSorry, no valid **branch** specified in this url: {0}!",
198                             path);
199                     error(response, mkd);
200                 } else {
201                     // redirect to default branch
202                     String base = request.getRequestURI();
203                     String url = base + branch + "/";
204                     response.sendRedirect(url);
205                 }
206                 return;
207             }
208
209             // identify the requested path
210             String requestedPath = getPath(repository, branch, request);
211
212             // identify the commit
213             RevCommit commit = JGitUtils.getCommit(r, branch);
214             if (commit == null) {
215                 // branch not found!
216                 String mkd = MessageFormat.format(
217                         "# Error\nSorry, the repository {0} does not have a **{1}** branch!",
218                         repository, branch);
219                 error(response, mkd);
220                 return;
221             }
222
5cc0a6 223             Map<String, String> quickContentTypes = new HashMap<>();
JM 224             quickContentTypes.put("html", "text/html");
225             quickContentTypes.put("htm", "text/html");
226             quickContentTypes.put("xml", "application/xml");
227             quickContentTypes.put("json", "application/json");
ca31f5 228
JM 229             List<PathModel> pathEntries = JGitUtils.getFilesInPath(r, requestedPath, commit);
230             if (pathEntries.isEmpty()) {
231                 // requested a specific resource
1946fe 232                 String file = StringUtils.getLastPathElement(requestedPath);
ca31f5 233                 try {
b4fbe4 234
JM 235                     String ext = StringUtils.getFileExtension(file).toLowerCase();
68100a 236                     // We can't parse out an extension for classic "dotfiles", so make a general assumption that
JJ 237                     // they're text files to allow presenting them in browser instead of only for download.
238                     //
239                     // However, that only holds for files with no other extension included, for files that happen
240                     // to start with a dot but also include an extension, process the extension normally.
241                     // This logic covers .gitattributes, .gitignore, .zshrc, etc., but does not cover .mongorc.js, .zshrc.bak
242                     boolean isExtensionlessDotfile = file.charAt(0) == '.' && (file.length() == 1 || file.indexOf('.',  1) < 0);
243                     String contentType = isExtensionlessDotfile ? "text/plain" : quickContentTypes.get(ext);
5cc0a6 244
JM 245                     if (contentType == null) {
246                         List<String> exts = runtimeManager.getSettings().getStrings(Keys.web.prettyPrintExtensions);
247                         if (exts.contains(ext)) {
248                             // extension is a registered text type for pretty printing
249                             contentType = "text/plain";
250                         } else {
251                             // query Tika for the content type
252                             Tika tika = new Tika();
253                             contentType = tika.detect(file);
254                         }
b4fbe4 255                     }
ca31f5 256
JM 257                     if (contentType == null) {
258                         // ask the container for the content type
259                         contentType = context.getMimeType(requestedPath);
260
261                         if (contentType == null) {
262                             // still unknown content type, assume binary
263                             contentType = "application/octet-stream";
264                         }
265                     }
266
c8b728 267                     if (isTextType(contentType) || isTextDataType(contentType)) {
ca31f5 268
1946fe 269                         // load, interpret, and serve text content as UTF-8
JM 270                         String [] encodings = runtimeManager.getSettings().getStrings(Keys.web.blobEncodings).toArray(new String[0]);
271                         String content = JGitUtils.getStringContent(r, commit.getTree(), requestedPath, encodings);
498cdc 272                         if (content == null) {
JM 273                             logger.error("RawServlet Failed to load {} {} {}", repository, commit.getName(), path);
846ee5 274                             notFound(response, requestedPath, branch);
498cdc 275                             return;
JM 276                         }
1946fe 277
JM 278                         byte [] bytes = content.getBytes(Constants.ENCODING);
846ee5 279                         setContentType(response, contentType);
1946fe 280                         response.setContentLength(bytes.length);
JM 281                         ByteArrayInputStream is = new ByteArrayInputStream(bytes);
282                         sendContent(response, JGitUtils.getCommitDate(commit), is);
283
ca31f5 284                     } else {
1946fe 285                         // stream binary content directly from the repository
846ee5 286                         if (!streamFromRepo(request, response, r, commit, requestedPath)) {
JM 287                             logger.error("RawServlet Failed to load {} {} {}", repository, commit.getName(), path);
288                             notFound(response, requestedPath, branch);
289                         }
1946fe 290                     }
ca31f5 291                     return;
JM 292                 } catch (Exception e) {
293                     logger.error(null, e);
294                 }
295             } else {
296                 // path request
297                 if (!request.getPathInfo().endsWith("/")) {
298                     // redirect to trailing '/' url
299                     response.sendRedirect(request.getServletPath() + request.getPathInfo() + "/");
300                     return;
301                 }
302
303                 if (renderIndex()) {
304                     // locate and render an index file
305                     Map<String, String> names = new TreeMap<String, String>();
306                     for (PathModel entry : pathEntries) {
307                         names.put(entry.name.toLowerCase(), entry.name);
308                     }
309
310                     List<String> extensions = new ArrayList<String>();
311                     extensions.add("html");
312                     extensions.add("htm");
313
314                     String content = null;
315                     for (String ext : extensions) {
316                         String key = "index." + ext;
317
318                         if (names.containsKey(key)) {
319                             String fileName = names.get(key);
320                             String fullPath = fileName;
321                             if (!requestedPath.isEmpty()) {
322                                 fullPath = requestedPath + "/" + fileName;
323                             }
324
1946fe 325                             String [] encodings = runtimeManager.getSettings().getStrings(Keys.web.blobEncodings).toArray(new String[0]);
JM 326                             String stringContent = JGitUtils.getStringContent(r, commit.getTree(), fullPath, encodings);
ca31f5 327                             if (stringContent == null) {
JM 328                                 continue;
329                             }
330                             content = stringContent;
331                             requestedPath = fullPath;
332                             break;
333                         }
334                     }
335
336                     response.setContentType("text/html; charset=" + Constants.ENCODING);
337                     byte [] bytes = content.getBytes(Constants.ENCODING);
1946fe 338                     response.setContentLength(bytes.length);
ca31f5 339
JM 340                     ByteArrayInputStream is = new ByteArrayInputStream(bytes);
341                     sendContent(response, JGitUtils.getCommitDate(commit), is);
342                     return;
343                 }
344             }
345
346             // no content, document list or 404 page
347             if (pathEntries.isEmpty()) {
348                 // default 404 page
846ee5 349                 notFound(response, requestedPath, branch);
ff17f7 350                 return;
ca31f5 351             } else {
JM 352                 //
353                 // directory list
354                 //
355                 response.setContentType("text/html");
356                 response.getWriter().append("<style>table th, table td { min-width: 150px; text-align: left; }</style>");
357                 response.getWriter().append("<table>");
358                 response.getWriter().append("<thead><tr><th>path</th><th>mode</th><th>size</th></tr>");
359                 response.getWriter().append("</thead>");
360                 response.getWriter().append("<tbody>");
361                 String pattern = "<tr><td><a href=\"{0}/{1}\">{1}</a></td><td>{2}</td><td>{3}</td></tr>";
362                 final ByteFormat byteFormat = new ByteFormat();
363                 if (!pathEntries.isEmpty()) {
364                     if (pathEntries.get(0).path.indexOf('/') > -1) {
365                         // we are in a subdirectory, add parent directory link
366                         String pp = URLEncoder.encode(requestedPath, Constants.ENCODING);
367                         pathEntries.add(0, new PathModel("..", pp + "/..", 0, FileMode.TREE.getBits(), null, null));
368                     }
369                 }
370
371                 String basePath = request.getServletPath() + request.getPathInfo();
372                 if (basePath.charAt(basePath.length() - 1) == '/') {
373                     // strip trailing slash
374                     basePath = basePath.substring(0, basePath.length() - 1);
375                 }
376                 for (PathModel entry : pathEntries) {
377                     String pp = URLEncoder.encode(entry.name, Constants.ENCODING);
378                     response.getWriter().append(MessageFormat.format(pattern, basePath, pp,
ff17f7 379                             JGitUtils.getPermissionsFromMode(entry.mode),
JM 380                             entry.isFile() ? byteFormat.format(entry.size) : ""));
ca31f5 381                 }
JM 382                 response.getWriter().append("</tbody>");
383                 response.getWriter().append("</table>");
384             }
385         } catch (Throwable t) {
386             logger.error("Failed to write page to client", t);
387         } finally {
388             r.close();
389         }
390     }
391
1946fe 392     protected boolean isTextType(String contentType) {
JM 393         if (contentType.startsWith("text/")
394                 || "application/json".equals(contentType)
395                 || "application/xml".equals(contentType)) {
396             return true;
397         }
398         return false;
399     }
400
c8b728 401     protected boolean isTextDataType(String contentType) {
JM 402         if ("image/svg+xml".equals(contentType)) {
403             return true;
404         }
405         return false;
406     }
407
1946fe 408     /**
JM 409      * Override all text types to be plain text.
410      *
411      * @param response
412      * @param contentType
413      */
414     protected void setContentType(HttpServletResponse response, String contentType) {
415         if (isTextType(contentType)) {
416             response.setContentType("text/plain");
417         } else {
418             response.setContentType(contentType);
419         }
420     }
421
846ee5 422     protected boolean streamFromRepo(HttpServletRequest request, HttpServletResponse response, Repository repository,
553e0f 423             RevCommit commit, String requestedPath) throws IOException {
JM 424
846ee5 425         boolean served = false;
553e0f 426         RevWalk rw = new RevWalk(repository);
JM 427         TreeWalk tw = new TreeWalk(repository);
428         try {
429             tw.reset();
430             tw.addTree(commit.getTree());
431             PathFilter f = PathFilter.create(requestedPath);
432             tw.setFilter(f);
433             tw.setRecursive(true);
434             MutableObjectId id = new MutableObjectId();
435             ObjectReader reader = tw.getObjectReader();
436             while (tw.next()) {
437                 FileMode mode = tw.getFileMode(0);
438                 if (mode == FileMode.GITLINK || mode == FileMode.TREE) {
439                     continue;
440                 }
441                 tw.getObjectId(id, 0);
442
846ee5 443                 String filename = StringUtils.getLastPathElement(requestedPath);
JM 444                 try {
445                     String userAgent = request.getHeader("User-Agent");
446                     if (userAgent != null && userAgent.indexOf("MSIE 5.5") > -1) {
447                           response.setHeader("Content-Disposition", "filename=\""
448                                   +  URLEncoder.encode(filename, Constants.ENCODING) + "\"");
449                     } else if (userAgent != null && userAgent.indexOf("MSIE") > -1) {
450                           response.setHeader("Content-Disposition", "attachment; filename=\""
451                                   +  URLEncoder.encode(filename, Constants.ENCODING) + "\"");
452                     } else {
453                             response.setHeader("Content-Disposition", "attachment; filename=\""
454                                   + new String(filename.getBytes(Constants.ENCODING), "latin1") + "\"");
455                     }
456                 }
457                 catch (UnsupportedEncodingException e) {
458                     response.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
459                 }
460
553e0f 461                 long len = reader.getObjectSize(id, org.eclipse.jgit.lib.Constants.OBJ_BLOB);
846ee5 462                 setContentType(response, "application/octet-stream");
553e0f 463                 response.setIntHeader("Content-Length", (int) len);
JM 464                 ObjectLoader ldr = repository.open(id);
465                 ldr.copyTo(response.getOutputStream());
846ee5 466                 served = true;
553e0f 467             }
JM 468         } finally {
a1cee6 469             tw.close();
553e0f 470             rw.dispose();
JM 471         }
472
473         response.flushBuffer();
846ee5 474         return served;
553e0f 475     }
JM 476
818973 477     protected void sendContent(HttpServletResponse response, Date date, InputStream is) throws ServletException, IOException {
JM 478
ca31f5 479         try {
JM 480             byte[] tmp = new byte[8192];
481             int len = 0;
482             while ((len = is.read(tmp)) > -1) {
483                 response.getOutputStream().write(tmp, 0, len);
484             }
485         } finally {
486             is.close();
487         }
488         response.flushBuffer();
489     }
490
846ee5 491     protected void notFound(HttpServletResponse response, String requestedPath, String branch)
JM 492             throws ParseException, ServletException, IOException {
493         String str = MessageFormat.format(
494                 "# Error\nSorry, the requested resource **{0}** was not found in **{1}**.",
495                 requestedPath, branch);
496         response.setStatus(HttpServletResponse.SC_NOT_FOUND);
497         error(response, str);
498     }
499
ca31f5 500     private void error(HttpServletResponse response, String mkd) throws ServletException,
JM 501             IOException, ParseException {
502         String content = MarkdownUtils.transformMarkdown(mkd);
503         response.setContentType("text/html; charset=" + Constants.ENCODING);
504         response.getWriter().write(content);
505     }
506
507     @Override
508     protected void doPost(HttpServletRequest request, HttpServletResponse response)
509             throws ServletException, IOException {
510         processRequest(request, response);
511     }
512
513     @Override
514     protected void doGet(HttpServletRequest request, HttpServletResponse response)
515             throws ServletException, IOException {
516         processRequest(request, response);
517     }
518 }