James Moger
2016-01-25 252dc07d7f85cc344b5919bb7c6166ef84b2102e
commit | author | age
bd0e83 1 /*
PM 2  * Copyright 2015 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.BufferedReader;
19 import java.io.IOException;
20 import java.io.Serializable;
21 import java.text.MessageFormat;
22 import java.util.ArrayList;
23 import java.util.HashMap;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.regex.Matcher;
27 import java.util.regex.Pattern;
28
29 import com.google.inject.Inject;
30 import com.google.inject.Singleton;
31
32 import javax.servlet.ServletException;
33 import javax.servlet.http.HttpServlet;
34 import javax.servlet.http.HttpServletRequest;
35 import javax.servlet.http.HttpServletResponse;
36
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
39
40 import com.gitblit.Constants;
41 import com.gitblit.IStoredSettings;
42 import com.gitblit.models.FilestoreModel;
43 import com.gitblit.models.RepositoryModel;
44 import com.gitblit.models.FilestoreModel.Status;
45 import com.gitblit.manager.FilestoreManager;
46 import com.gitblit.manager.IGitblit;
47 import com.gitblit.models.UserModel;
48 import com.gitblit.utils.JsonUtils;
49
50
51 /**
52  * Handles large file storage as per the Git LFS v1 Batch API
53  * 
54  * Further details can be found at https://github.com/github/git-lfs
55  * 
56  * @author Paul Martin
57  */
58 @Singleton
59 public class FilestoreServlet extends HttpServlet {
60
61     private static final long serialVersionUID = 1L;
62     public static final int PROTOCOL_VERSION = 1;
63     
64     public static final String GIT_LFS_META_MIME = "application/vnd.git-lfs+json";
65     
da29a4 66     public static final String REGEX_PATH = "^(.*?)/(r)/(.*?)/info/lfs/objects/(batch|" + Constants.REGEX_SHA256 + ")";
bd0e83 67     public static final int REGEX_GROUP_BASE_URI = 1;
PM 68     public static final int REGEX_GROUP_PREFIX = 2;
69     public static final int REGEX_GROUP_REPOSITORY = 3;
70     public static final int REGEX_GROUP_ENDPOINT = 4;
71     
72     protected final Logger logger;
73     
74     private static IGitblit gitblit;
75
76     @Inject
77     public FilestoreServlet(IStoredSettings settings, IGitblit gitblit) {
78         
79         super();
80         logger = LoggerFactory.getLogger(getClass());
81         
82         FilestoreServlet.gitblit = gitblit;
83     }
84
85         
86     /**
87      * Handles batch upload request (metadata)
88      *
89      * @param request
90      * @param response
91      * @throws javax.servlet.ServletException
92      * @throws java.io.IOException
93      */
94     @Override
95     protected void doPost(HttpServletRequest request, 
96             HttpServletResponse response) throws ServletException ,IOException {
97         
98         UrlInfo info = getInfoFromRequest(request);
99         if (info == null) {
100             sendError(response, HttpServletResponse.SC_NOT_FOUND);
101             return;
102         }
103
104         //Post is for batch operations so no oid should be defined
105         if (info.oid != null) {
106             sendError(response, HttpServletResponse.SC_BAD_REQUEST);
107             return;
108         }
109         
110         IGitLFS.Batch batch = deserialize(request, response, IGitLFS.Batch.class);
111         
112         if (batch == null) { 
113             sendError(response, HttpServletResponse.SC_BAD_REQUEST);
114             return;
115         }
116
117         UserModel user = getUserOrAnonymous(request);
118         
119         IGitLFS.BatchResponse batchResponse = new IGitLFS.BatchResponse();
120         
121         if (batch.operation.equalsIgnoreCase("upload")) {
122             for (IGitLFS.Request item : batch.objects) {
123                 
124                 Status state = gitblit.addObject(item.oid, item.size, user, info.repository);
125
126                 batchResponse.objects.add(getResponseForUpload(info.baseUrl, item.oid, item.size, user.getName(), info.repository.name, state));
127             }
128         } else if (batch.operation.equalsIgnoreCase("download")) {
129             for (IGitLFS.Request item : batch.objects) {
130                 
131                 Status state = gitblit.downloadBlob(item.oid, user, info.repository, null);
132                 batchResponse.objects.add(getResponseForDownload(info.baseUrl, item.oid, item.size, user.getName(), info.repository.name, state));
133             }
134         } else {
135             sendError(response, HttpServletResponse.SC_NOT_IMPLEMENTED);
136             return;
137         }
138         
139         response.setStatus(HttpServletResponse.SC_OK);
140         serialize(response, batchResponse);
141     }
142     
143     /**
144      * Handles the actual upload (BLOB)
145      * 
146      * @param request
147      * @param response
148      * @throws javax.servlet.ServletException
149      * @throws java.io.IOException
150      */
151     @Override
152     protected void doPut(HttpServletRequest request, 
153             HttpServletResponse response) throws ServletException ,IOException {
154         
155         UrlInfo info = getInfoFromRequest(request);
156         
157         if (info == null) {
158             sendError(response, HttpServletResponse.SC_NOT_FOUND);
159             return;
160         }
161
162         //Put is a singular operation so must have oid
163         if (info.oid == null) {
164             sendError(response, HttpServletResponse.SC_BAD_REQUEST);
165             return;
166         }
167         
168         UserModel user = getUserOrAnonymous(request);
169         long size = FilestoreManager.UNDEFINED_SIZE;
170         
171         
172         
173         FilestoreModel.Status status = gitblit.uploadBlob(info.oid, size, user, info.repository, request.getInputStream());
174         IGitLFS.Response responseObject = getResponseForUpload(info.baseUrl, info.oid, size, user.getName(), info.repository.name, status);
175         
176         logger.info(MessageFormat.format("FILESTORE-AUDIT {0}:{4} {1} {2}@{3}", 
177                 "PUT", info.oid, user.getName(), info.repository.name, status.toString() ));
178         
179         if (responseObject.error == null) {
180             response.setStatus(responseObject.successCode);
181         } else {
182             serialize(response, responseObject.error);
183         }
184     };
185     
186     /**
187      * Handles a download
188      * Treated as hypermedia request if accept header contains Git-LFS MIME
189      * otherwise treated as a download of the blob
190      * @param request
191      * @param response
192      * @throws javax.servlet.ServletException
193      * @throws java.io.IOException
194      */
195     @Override
196     protected void doGet(HttpServletRequest request, 
197             HttpServletResponse response) throws ServletException ,IOException {
198         
199         UrlInfo info = getInfoFromRequest(request);
200         
201         if (info == null || info.oid == null) {
202             sendError(response, HttpServletResponse.SC_NOT_FOUND);
203             return;
204         }
205         
206         UserModel user = getUserOrAnonymous(request);
207         
208         FilestoreModel model = gitblit.getObject(info.oid, user, info.repository);
209         long size = FilestoreManager.UNDEFINED_SIZE;
210         
211         boolean isMetaRequest = AccessRestrictionFilter.hasContentInRequestHeader(request, "Accept", GIT_LFS_META_MIME);
212         FilestoreModel.Status status = Status.Unavailable;
213         
214         if (model != null) {
215             size = model.getSize();
216             status = model.getStatus();
217         }
218         
219         if (!isMetaRequest) {
220             status = gitblit.downloadBlob(info.oid, user, info.repository, response.getOutputStream());
221             
222             logger.info(MessageFormat.format("FILESTORE-AUDIT {0}:{4} {1} {2}@{3}", 
223                     "GET", info.oid, user.getName(), info.repository.name, status.toString() ));
224         }
225         
226         if (status == Status.Error_Unexpected_Stream_End) {
227             return;
228         }
229
230         IGitLFS.Response responseObject = getResponseForDownload(info.baseUrl, 
231                 info.oid, size, user.getName(), info.repository.name, status);
232         
233         if (responseObject.error == null) {
234             response.setStatus(responseObject.successCode);
235             
236             if (isMetaRequest) {
237                 serialize(response, responseObject);
238             }
239         } else {
240             response.setStatus(responseObject.error.code);
46f33f 241             
PM 242             if (isMetaRequest) {
243                 serialize(response, responseObject.error);
244             }
bd0e83 245         }
PM 246     };
247     
248     private void sendError(HttpServletResponse response, int code) throws IOException {
249         
250         String msg = "";
251         
252         switch (code)
253         {
254             case HttpServletResponse.SC_NOT_FOUND: msg = "Not Found"; break;
255             case HttpServletResponse.SC_NOT_IMPLEMENTED: msg = "Not Implemented"; break;
256             case HttpServletResponse.SC_BAD_REQUEST: msg = "Malformed Git-LFS request"; break;
257             
258             default: msg = "Unknown Error";
259         }
260         
261         response.setStatus(code);
262         serialize(response, new IGitLFS.ObjectError(code, msg));
263     }
264     
265     @SuppressWarnings("incomplete-switch")
266     private IGitLFS.Response getResponseForUpload(String baseUrl, String oid, long size, String user, String repo, FilestoreModel.Status state) {
267
268         switch (state) {
269             case AuthenticationRequired:
270                 return new IGitLFS.Response(oid, size, 401, MessageFormat.format("Authentication required to write to repository {0}", repo));
271             case Error_Unauthorized: 
272                 return new IGitLFS.Response(oid, size, 403, MessageFormat.format("User {0}, does not have write permissions to repository {1}", user, repo));
273             case Error_Exceeds_Size_Limit: 
274                 return new IGitLFS.Response(oid, size, 509, MessageFormat.format("Object is larger than allowed limit of {1}",  gitblit.getMaxUploadSize()));
275             case Error_Hash_Mismatch: 
276                 return new IGitLFS.Response(oid, size, 422, "Hash mismatch");
277             case Error_Invalid_Oid: 
278                 return new IGitLFS.Response(oid, size, 422, MessageFormat.format("{0} is not a valid oid", oid));
279             case Error_Invalid_Size: 
280                 return new IGitLFS.Response(oid, size, 422, MessageFormat.format("{0} is not a valid size", size));
281             case Error_Size_Mismatch: 
282                 return new IGitLFS.Response(oid, size, 422, "Object size mismatch");
283             case Deleted: 
284                 return new IGitLFS.Response(oid, size, 410, "Object was deleted : ".concat("TBD Reason") );
285             case Upload_In_Progress:
286                 return new IGitLFS.Response(oid, size, 503, "File currently being uploaded by another user");
287             case Unavailable: 
288                 return new IGitLFS.Response(oid, size, 404, MessageFormat.format("Repository {0}, does not exist for user {1}", repo, user));
289             case Upload_Pending: 
290                 return new IGitLFS.Response(oid, size, 202, "upload", getObjectUri(baseUrl, repo, oid) );
291             case Available: 
292                 return new IGitLFS.Response(oid, size, 200, "upload", getObjectUri(baseUrl, repo, oid) );
293         }
294         
295         return new IGitLFS.Response(oid, size, 500, "Unknown Error");
296     }
297
298     @SuppressWarnings("incomplete-switch")
299     private IGitLFS.Response getResponseForDownload(String baseUrl, String oid, long size, String user, String repo, FilestoreModel.Status state) {
300
301         switch (state) {
302             case Error_Unauthorized: 
303                 return new IGitLFS.Response(oid, size, 403, MessageFormat.format("User {0}, does not have read permissions to repository {1}", user, repo));
304             case Error_Invalid_Oid: 
305                 return new IGitLFS.Response(oid, size, 422, MessageFormat.format("{0} is not a valid oid", oid));
306             case Error_Unknown:
307                 return new IGitLFS.Response(oid, size, 500, "Unknown Error");
308             case Deleted: 
309                 return new IGitLFS.Response(oid, size, 410, "Object was deleted : ".concat("TBD Reason") );
310             case Available: 
311                 return new IGitLFS.Response(oid, size, 200, "download", getObjectUri(baseUrl, repo, oid) );
312         }
313         
314         return new IGitLFS.Response(oid, size, 404, "Object not available");
315     }
316
317     
318     private String getObjectUri(String baseUrl, String repo, String oid) {
319         return baseUrl + "/" + repo + "/" + Constants.R_LFS + "objects/" + oid;
320     }
321     
322     
323     protected void serialize(HttpServletResponse response, Object o) throws IOException {
324         if (o != null) {
325             // Send JSON response
326             String json = JsonUtils.toJsonString(o);
327             response.setCharacterEncoding(Constants.ENCODING);
328             response.setContentType(GIT_LFS_META_MIME);
329             response.getWriter().append(json);
330         }
331     }
332     
333     protected <X> X deserialize(HttpServletRequest request, HttpServletResponse response,
334             Class<X> clazz) {
335         
336         String json = "";
337         try {
338             
339             json = readJson(request, response);
340             
341             return JsonUtils.fromJsonString(json.toString(), clazz);
342             
343         } catch (Exception e) {
344             //Intentional silent fail
345         }
346         
347         return null;
348     }
349     
350     private String readJson(HttpServletRequest request, HttpServletResponse response)
351             throws IOException {
352         BufferedReader reader = request.getReader();
353         StringBuilder json = new StringBuilder();
354         String line = null;
355         while ((line = reader.readLine()) != null) {
356             json.append(line);
357         }
358         reader.close();
359
360         if (json.length() == 0) {
361             logger.error(MessageFormat.format("Failed to receive json data from {0}",
362                     request.getRemoteAddr()));
363             response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
364             return null;
365         }
366         return json.toString();
367     }
368     
369     private UserModel getUserOrAnonymous(HttpServletRequest r) {
370         UserModel user = (UserModel) r.getUserPrincipal();
371         if (user != null) { return user; }
372         return UserModel.ANONYMOUS;
373     }
374     
375     private static class UrlInfo {
376         public RepositoryModel repository;
377         public String oid;
378         public String baseUrl;
379         
380         public UrlInfo(RepositoryModel repo, String oid, String baseUrl) {
381             this.repository = repo;
382             this.oid = oid;
383             this.baseUrl = baseUrl;
384         }
385     }
386     
387     public static UrlInfo getInfoFromRequest(HttpServletRequest httpRequest) {
388         
389         String url = httpRequest.getRequestURL().toString();
390         Pattern p = Pattern.compile(REGEX_PATH);
391         Matcher m = p.matcher(url);
392         
393         
394         if (m.find()) {
395             RepositoryModel repo = gitblit.getRepositoryModel(m.group(REGEX_GROUP_REPOSITORY));
396             String baseUrl = m.group(REGEX_GROUP_BASE_URI) + "/" + m.group(REGEX_GROUP_PREFIX);
397             
398             if (m.group(REGEX_GROUP_ENDPOINT).equals("batch")) {
399                 return new UrlInfo(repo, null, baseUrl);
400             } else {
401                 return new UrlInfo(repo, m.group(REGEX_GROUP_ENDPOINT), baseUrl);
402             }
403         }
404         
405         return null;
406     }
407     
408     
409     public interface IGitLFS {
410     
411         @SuppressWarnings("serial")
412         public class Request implements Serializable
413         {
414             public String oid;
415             public long size;
416         }
417         
418         
419         @SuppressWarnings("serial")
420         public class Batch implements Serializable
421         {
422             public String operation;
423             public List<Request> objects;
424         }
425         
426         
427         @SuppressWarnings("serial")
428         public class Response implements Serializable
429         {
430             public String oid;
431             public long size;
432             public Map<String, HyperMediaLink> actions;
433             public ObjectError error;
434             public transient int successCode; 
435             
436             public Response(String id, long itemSize, int errorCode, String errorText) {
437                 oid = id;
438                 size = itemSize;
439                 actions = null;
440                 successCode = 0;
441                 error = new ObjectError(errorCode, errorText);
442             }
443             
444             public Response(String id, long itemSize, int actionCode, String action, String uri) {
445                 oid = id;
446                 size = itemSize;
447                 error = null;
448                 successCode = actionCode;
449                 actions = new HashMap<String, HyperMediaLink>();
450                 actions.put(action, new HyperMediaLink(action, uri));
451             }
452             
453         }
454         
455         @SuppressWarnings("serial")
456         public class BatchResponse implements Serializable {
457             public List<Response> objects;
458             
459             public BatchResponse() {
460                 objects = new ArrayList<Response>();
461             }
462         }
463         
464         
465         @SuppressWarnings("serial")
466         public class ObjectError implements Serializable
467         {
468             public String message;
469             public int code;
470             public String documentation_url;
471             public Integer request_id;
472             
473             public ObjectError(int errorCode, String errorText) {
474                 code = errorCode;
475                 message = errorText;
476                 request_id = null;
477             }
478         }
479         
480         @SuppressWarnings("serial")
481         public class HyperMediaLink implements Serializable
482         {
483             public String href;
484             public transient String header;
485             //public Date expires_at;
486             
487             public HyperMediaLink(String action, String uri) {
488                 header = action;
489                 href = uri;
490             }
491         }
492     }
493
494
495     
496 }