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