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