James Moger
2015-11-22 ed552ba47c02779c270ffd62841d6d1048dade70
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
169         String repository = "";
170         Repository r = null;
171         int offset = 0;
172         while (r == null) {
173             int slash = path.indexOf('/', offset);
174             if (slash == -1) {
175                 repository = path;
176             } else {
177                 repository = path.substring(0, slash);
178             }
e7e8bd 179             offset = ( slash + 1 );
ca31f5 180             r = repositoryManager.getRepository(repository, false);
JM 181             if (repository.equals(path)) {
182                 // either only repository in url or no repository found
183                 break;
184             }
185         }
186
187         ServletContext context = request.getSession().getServletContext();
188
189         try {
190             if (r == null) {
191                 // repository not found!
192                 String mkd = MessageFormat.format(
193                         "# Error\nSorry, no valid **repository** specified in this url: {0}!",
194                         path);
195                 error(response, mkd);
196                 return;
197             }
198
199             // identify the branch
200             String branch = getBranch(repository, request);
201             if (StringUtils.isEmpty(branch)) {
202                 branch = r.getBranch();
203                 if (branch == null) {
204                     // no branches found!  empty?
205                     String mkd = MessageFormat.format(
206                             "# Error\nSorry, no valid **branch** specified in this url: {0}!",
207                             path);
208                     error(response, mkd);
209                 } else {
210                     // redirect to default branch
211                     String base = request.getRequestURI();
212                     String url = base + branch + "/";
213                     response.sendRedirect(url);
214                 }
215                 return;
216             }
217
218             // identify the requested path
219             String requestedPath = getPath(repository, branch, request);
220
221             // identify the commit
222             RevCommit commit = JGitUtils.getCommit(r, branch);
223             if (commit == null) {
224                 // branch not found!
225                 String mkd = MessageFormat.format(
226                         "# Error\nSorry, the repository {0} does not have a **{1}** branch!",
227                         repository, branch);
228                 error(response, mkd);
229                 return;
230             }
231
5cc0a6 232             Map<String, String> quickContentTypes = new HashMap<>();
JM 233             quickContentTypes.put("html", "text/html");
234             quickContentTypes.put("htm", "text/html");
235             quickContentTypes.put("xml", "application/xml");
236             quickContentTypes.put("json", "application/json");
ca31f5 237
JM 238             List<PathModel> pathEntries = JGitUtils.getFilesInPath(r, requestedPath, commit);
239             if (pathEntries.isEmpty()) {
240                 // requested a specific resource
1946fe 241                 String file = StringUtils.getLastPathElement(requestedPath);
ca31f5 242                 try {
b4fbe4 243
JM 244                     String ext = StringUtils.getFileExtension(file).toLowerCase();
5cc0a6 245                     String contentType = quickContentTypes.get(ext);
JM 246
247                     if (contentType == null) {
248                         List<String> exts = runtimeManager.getSettings().getStrings(Keys.web.prettyPrintExtensions);
249                         if (exts.contains(ext)) {
250                             // extension is a registered text type for pretty printing
251                             contentType = "text/plain";
252                         } else {
253                             // query Tika for the content type
254                             Tika tika = new Tika();
255                             contentType = tika.detect(file);
256                         }
b4fbe4 257                     }
ca31f5 258
JM 259                     if (contentType == null) {
260                         // ask the container for the content type
261                         contentType = context.getMimeType(requestedPath);
262
263                         if (contentType == null) {
264                             // still unknown content type, assume binary
265                             contentType = "application/octet-stream";
266                         }
267                     }
268
c8b728 269                     if (isTextType(contentType) || isTextDataType(contentType)) {
ca31f5 270
1946fe 271                         // load, interpret, and serve text content as UTF-8
JM 272                         String [] encodings = runtimeManager.getSettings().getStrings(Keys.web.blobEncodings).toArray(new String[0]);
273                         String content = JGitUtils.getStringContent(r, commit.getTree(), requestedPath, encodings);
498cdc 274                         if (content == null) {
JM 275                             logger.error("RawServlet Failed to load {} {} {}", repository, commit.getName(), path);
846ee5 276                             notFound(response, requestedPath, branch);
498cdc 277                             return;
JM 278                         }
1946fe 279
JM 280                         byte [] bytes = content.getBytes(Constants.ENCODING);
846ee5 281                         setContentType(response, contentType);
1946fe 282                         response.setContentLength(bytes.length);
JM 283                         ByteArrayInputStream is = new ByteArrayInputStream(bytes);
284                         sendContent(response, JGitUtils.getCommitDate(commit), is);
285
ca31f5 286                     } else {
1946fe 287                         // stream binary content directly from the repository
846ee5 288                         if (!streamFromRepo(request, response, r, commit, requestedPath)) {
JM 289                             logger.error("RawServlet Failed to load {} {} {}", repository, commit.getName(), path);
290                             notFound(response, requestedPath, branch);
291                         }
1946fe 292                     }
ca31f5 293                     return;
JM 294                 } catch (Exception e) {
295                     logger.error(null, e);
296                 }
297             } else {
298                 // path request
299                 if (!request.getPathInfo().endsWith("/")) {
300                     // redirect to trailing '/' url
301                     response.sendRedirect(request.getServletPath() + request.getPathInfo() + "/");
302                     return;
303                 }
304
305                 if (renderIndex()) {
306                     // locate and render an index file
307                     Map<String, String> names = new TreeMap<String, String>();
308                     for (PathModel entry : pathEntries) {
309                         names.put(entry.name.toLowerCase(), entry.name);
310                     }
311
312                     List<String> extensions = new ArrayList<String>();
313                     extensions.add("html");
314                     extensions.add("htm");
315
316                     String content = null;
317                     for (String ext : extensions) {
318                         String key = "index." + ext;
319
320                         if (names.containsKey(key)) {
321                             String fileName = names.get(key);
322                             String fullPath = fileName;
323                             if (!requestedPath.isEmpty()) {
324                                 fullPath = requestedPath + "/" + fileName;
325                             }
326
1946fe 327                             String [] encodings = runtimeManager.getSettings().getStrings(Keys.web.blobEncodings).toArray(new String[0]);
JM 328                             String stringContent = JGitUtils.getStringContent(r, commit.getTree(), fullPath, encodings);
ca31f5 329                             if (stringContent == null) {
JM 330                                 continue;
331                             }
332                             content = stringContent;
333                             requestedPath = fullPath;
334                             break;
335                         }
336                     }
337
338                     response.setContentType("text/html; charset=" + Constants.ENCODING);
339                     byte [] bytes = content.getBytes(Constants.ENCODING);
1946fe 340                     response.setContentLength(bytes.length);
ca31f5 341
JM 342                     ByteArrayInputStream is = new ByteArrayInputStream(bytes);
343                     sendContent(response, JGitUtils.getCommitDate(commit), is);
344                     return;
345                 }
346             }
347
348             // no content, document list or 404 page
349             if (pathEntries.isEmpty()) {
350                 // default 404 page
846ee5 351                 notFound(response, requestedPath, branch);
ff17f7 352                 return;
ca31f5 353             } else {
JM 354                 //
355                 // directory list
356                 //
357                 response.setContentType("text/html");
358                 response.getWriter().append("<style>table th, table td { min-width: 150px; text-align: left; }</style>");
359                 response.getWriter().append("<table>");
360                 response.getWriter().append("<thead><tr><th>path</th><th>mode</th><th>size</th></tr>");
361                 response.getWriter().append("</thead>");
362                 response.getWriter().append("<tbody>");
363                 String pattern = "<tr><td><a href=\"{0}/{1}\">{1}</a></td><td>{2}</td><td>{3}</td></tr>";
364                 final ByteFormat byteFormat = new ByteFormat();
365                 if (!pathEntries.isEmpty()) {
366                     if (pathEntries.get(0).path.indexOf('/') > -1) {
367                         // we are in a subdirectory, add parent directory link
368                         String pp = URLEncoder.encode(requestedPath, Constants.ENCODING);
369                         pathEntries.add(0, new PathModel("..", pp + "/..", 0, FileMode.TREE.getBits(), null, null));
370                     }
371                 }
372
373                 String basePath = request.getServletPath() + request.getPathInfo();
374                 if (basePath.charAt(basePath.length() - 1) == '/') {
375                     // strip trailing slash
376                     basePath = basePath.substring(0, basePath.length() - 1);
377                 }
378                 for (PathModel entry : pathEntries) {
379                     String pp = URLEncoder.encode(entry.name, Constants.ENCODING);
380                     response.getWriter().append(MessageFormat.format(pattern, basePath, pp,
ff17f7 381                             JGitUtils.getPermissionsFromMode(entry.mode),
JM 382                             entry.isFile() ? byteFormat.format(entry.size) : ""));
ca31f5 383                 }
JM 384                 response.getWriter().append("</tbody>");
385                 response.getWriter().append("</table>");
386             }
387         } catch (Throwable t) {
388             logger.error("Failed to write page to client", t);
389         } finally {
390             r.close();
391         }
392     }
393
1946fe 394     protected boolean isTextType(String contentType) {
JM 395         if (contentType.startsWith("text/")
396                 || "application/json".equals(contentType)
397                 || "application/xml".equals(contentType)) {
398             return true;
399         }
400         return false;
401     }
402
c8b728 403     protected boolean isTextDataType(String contentType) {
JM 404         if ("image/svg+xml".equals(contentType)) {
405             return true;
406         }
407         return false;
408     }
409
1946fe 410     /**
JM 411      * Override all text types to be plain text.
412      *
413      * @param response
414      * @param contentType
415      */
416     protected void setContentType(HttpServletResponse response, String contentType) {
417         if (isTextType(contentType)) {
418             response.setContentType("text/plain");
419         } else {
420             response.setContentType(contentType);
421         }
422     }
423
846ee5 424     protected boolean streamFromRepo(HttpServletRequest request, HttpServletResponse response, Repository repository,
553e0f 425             RevCommit commit, String requestedPath) throws IOException {
JM 426
846ee5 427         boolean served = false;
553e0f 428         RevWalk rw = new RevWalk(repository);
JM 429         TreeWalk tw = new TreeWalk(repository);
430         try {
431             tw.reset();
432             tw.addTree(commit.getTree());
433             PathFilter f = PathFilter.create(requestedPath);
434             tw.setFilter(f);
435             tw.setRecursive(true);
436             MutableObjectId id = new MutableObjectId();
437             ObjectReader reader = tw.getObjectReader();
438             while (tw.next()) {
439                 FileMode mode = tw.getFileMode(0);
440                 if (mode == FileMode.GITLINK || mode == FileMode.TREE) {
441                     continue;
442                 }
443                 tw.getObjectId(id, 0);
444
846ee5 445                 String filename = StringUtils.getLastPathElement(requestedPath);
JM 446                 try {
447                     String userAgent = request.getHeader("User-Agent");
448                     if (userAgent != null && userAgent.indexOf("MSIE 5.5") > -1) {
449                           response.setHeader("Content-Disposition", "filename=\""
450                                   +  URLEncoder.encode(filename, Constants.ENCODING) + "\"");
451                     } else if (userAgent != null && userAgent.indexOf("MSIE") > -1) {
452                           response.setHeader("Content-Disposition", "attachment; filename=\""
453                                   +  URLEncoder.encode(filename, Constants.ENCODING) + "\"");
454                     } else {
455                             response.setHeader("Content-Disposition", "attachment; filename=\""
456                                   + new String(filename.getBytes(Constants.ENCODING), "latin1") + "\"");
457                     }
458                 }
459                 catch (UnsupportedEncodingException e) {
460                     response.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
461                 }
462
553e0f 463                 long len = reader.getObjectSize(id, org.eclipse.jgit.lib.Constants.OBJ_BLOB);
846ee5 464                 setContentType(response, "application/octet-stream");
553e0f 465                 response.setIntHeader("Content-Length", (int) len);
JM 466                 ObjectLoader ldr = repository.open(id);
467                 ldr.copyTo(response.getOutputStream());
846ee5 468                 served = true;
553e0f 469             }
JM 470         } finally {
a1cee6 471             tw.close();
553e0f 472             rw.dispose();
JM 473         }
474
475         response.flushBuffer();
846ee5 476         return served;
553e0f 477     }
JM 478
818973 479     protected void sendContent(HttpServletResponse response, Date date, InputStream is) throws ServletException, IOException {
JM 480
ca31f5 481         try {
JM 482             byte[] tmp = new byte[8192];
483             int len = 0;
484             while ((len = is.read(tmp)) > -1) {
485                 response.getOutputStream().write(tmp, 0, len);
486             }
487         } finally {
488             is.close();
489         }
490         response.flushBuffer();
491     }
492
846ee5 493     protected void notFound(HttpServletResponse response, String requestedPath, String branch)
JM 494             throws ParseException, ServletException, IOException {
495         String str = MessageFormat.format(
496                 "# Error\nSorry, the requested resource **{0}** was not found in **{1}**.",
497                 requestedPath, branch);
498         response.setStatus(HttpServletResponse.SC_NOT_FOUND);
499         error(response, str);
500     }
501
ca31f5 502     private void error(HttpServletResponse response, String mkd) throws ServletException,
JM 503             IOException, ParseException {
504         String content = MarkdownUtils.transformMarkdown(mkd);
505         response.setContentType("text/html; charset=" + Constants.ENCODING);
506         response.getWriter().write(content);
507     }
508
509     @Override
510     protected void doPost(HttpServletRequest request, HttpServletResponse response)
511             throws ServletException, IOException {
512         processRequest(request, response);
513     }
514
515     @Override
516     protected void doGet(HttpServletRequest request, HttpServletResponse response)
517             throws ServletException, IOException {
518         processRequest(request, response);
519     }
520 }