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