##// END OF EJS Templates
lfs: ensure the transfer request is for a known URI...
Matt Harbison -
r37267:9640ccf4 default
parent child Browse files
Show More
@@ -1,286 +1,291
1 1 # wireprotolfsserver.py - lfs protocol server side implementation
2 2 #
3 3 # Copyright 2018 Matt Harbison <matt_harbison@yahoo.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import datetime
11 11 import errno
12 12 import json
13 13
14 14 from mercurial.hgweb import (
15 15 common as hgwebcommon,
16 16 )
17 17
18 18 from mercurial import (
19 19 pycompat,
20 20 )
21 21
22 22 HTTP_OK = hgwebcommon.HTTP_OK
23 23 HTTP_CREATED = hgwebcommon.HTTP_CREATED
24 24 HTTP_BAD_REQUEST = hgwebcommon.HTTP_BAD_REQUEST
25 HTTP_NOT_FOUND = hgwebcommon.HTTP_NOT_FOUND
25 26
26 27 def handlewsgirequest(orig, rctx, req, res, checkperm):
27 28 """Wrap wireprotoserver.handlewsgirequest() to possibly process an LFS
28 29 request if it is left unprocessed by the wrapped method.
29 30 """
30 31 if orig(rctx, req, res, checkperm):
31 32 return True
32 33
33 34 if not rctx.repo.ui.configbool('experimental', 'lfs.serve'):
34 35 return False
35 36
36 37 if not req.dispatchpath:
37 38 return False
38 39
39 40 try:
40 41 if req.dispatchpath == b'.git/info/lfs/objects/batch':
41 42 checkperm(rctx, req, 'pull')
42 43 return _processbatchrequest(rctx.repo, req, res)
43 44 # TODO: reserve and use a path in the proposed http wireprotocol /api/
44 45 # namespace?
45 46 elif req.dispatchpath.startswith(b'.hg/lfs/objects'):
46 47 return _processbasictransfer(rctx.repo, req, res,
47 48 lambda perm:
48 49 checkperm(rctx, req, perm))
49 50 return False
50 51 except hgwebcommon.ErrorResponse as e:
51 52 # XXX: copied from the handler surrounding wireprotoserver._callhttp()
52 53 # in the wrapped function. Should this be moved back to hgweb to
53 54 # be a common handler?
54 55 for k, v in e.headers:
55 56 res.headers[k] = v
56 57 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
57 58 res.setbodybytes(b'0\n%s\n' % pycompat.bytestr(e))
58 59 return True
59 60
60 61 def _sethttperror(res, code, message=None):
61 62 res.status = hgwebcommon.statusmessage(code, message=message)
62 63 res.headers[b'Content-Type'] = b'text/plain; charset=utf-8'
63 64 res.setbodybytes(b'')
64 65
65 66 def _processbatchrequest(repo, req, res):
66 67 """Handle a request for the Batch API, which is the gateway to granting file
67 68 access.
68 69
69 70 https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md
70 71 """
71 72
72 73 # Mercurial client request:
73 74 #
74 75 # HOST: localhost:$HGPORT
75 76 # ACCEPT: application/vnd.git-lfs+json
76 77 # ACCEPT-ENCODING: identity
77 78 # USER-AGENT: git-lfs/2.3.4 (Mercurial 4.5.2+1114-f48b9754f04c+20180316)
78 79 # Content-Length: 125
79 80 # Content-Type: application/vnd.git-lfs+json
80 81 #
81 82 # {
82 83 # "objects": [
83 84 # {
84 85 # "oid": "31cf...8e5b"
85 86 # "size": 12
86 87 # }
87 88 # ]
88 89 # "operation": "upload"
89 90 # }
90 91
91 92 if (req.method != b'POST'
92 93 or req.headers[b'Content-Type'] != b'application/vnd.git-lfs+json'
93 94 or req.headers[b'Accept'] != b'application/vnd.git-lfs+json'):
94 95 # TODO: figure out what the proper handling for a bad request to the
95 96 # Batch API is.
96 97 _sethttperror(res, HTTP_BAD_REQUEST, b'Invalid Batch API request')
97 98 return True
98 99
99 100 # XXX: specify an encoding?
100 101 lfsreq = json.loads(req.bodyfh.read())
101 102
102 103 # If no transfer handlers are explicitly requested, 'basic' is assumed.
103 104 if 'basic' not in lfsreq.get('transfers', ['basic']):
104 105 _sethttperror(res, HTTP_BAD_REQUEST,
105 106 b'Only the basic LFS transfer handler is supported')
106 107 return True
107 108
108 109 operation = lfsreq.get('operation')
109 110 if operation not in ('upload', 'download'):
110 111 _sethttperror(res, HTTP_BAD_REQUEST,
111 112 b'Unsupported LFS transfer operation: %s' % operation)
112 113 return True
113 114
114 115 localstore = repo.svfs.lfslocalblobstore
115 116
116 117 objects = [p for p in _batchresponseobjects(req, lfsreq.get('objects', []),
117 118 operation, localstore)]
118 119
119 120 rsp = {
120 121 'transfer': 'basic',
121 122 'objects': objects,
122 123 }
123 124
124 125 res.status = hgwebcommon.statusmessage(HTTP_OK)
125 126 res.headers[b'Content-Type'] = b'application/vnd.git-lfs+json'
126 127 res.setbodybytes(pycompat.bytestr(json.dumps(rsp)))
127 128
128 129 return True
129 130
130 131 def _batchresponseobjects(req, objects, action, store):
131 132 """Yield one dictionary of attributes for the Batch API response for each
132 133 object in the list.
133 134
134 135 req: The parsedrequest for the Batch API request
135 136 objects: The list of objects in the Batch API object request list
136 137 action: 'upload' or 'download'
137 138 store: The local blob store for servicing requests"""
138 139
139 140 # Successful lfs-test-server response to solict an upload:
140 141 # {
141 142 # u'objects': [{
142 143 # u'size': 12,
143 144 # u'oid': u'31cf...8e5b',
144 145 # u'actions': {
145 146 # u'upload': {
146 147 # u'href': u'http://localhost:$HGPORT/objects/31cf...8e5b',
147 148 # u'expires_at': u'0001-01-01T00:00:00Z',
148 149 # u'header': {
149 150 # u'Accept': u'application/vnd.git-lfs'
150 151 # }
151 152 # }
152 153 # }
153 154 # }]
154 155 # }
155 156
156 157 # TODO: Sort out the expires_at/expires_in/authenticated keys.
157 158
158 159 for obj in objects:
159 160 # Convert unicode to ASCII to create a filesystem path
160 161 oid = obj.get('oid').encode('ascii')
161 162 rsp = {
162 163 'oid': oid,
163 164 'size': obj.get('size'), # XXX: should this check the local size?
164 165 #'authenticated': True,
165 166 }
166 167
167 168 exists = True
168 169 verifies = False
169 170
170 171 # Verify an existing file on the upload request, so that the client is
171 172 # solicited to re-upload if it corrupt locally. Download requests are
172 173 # also verified, so the error can be flagged in the Batch API response.
173 174 # (Maybe we can use this to short circuit the download for `hg verify`,
174 175 # IFF the client can assert that the remote end is an hg server.)
175 176 # Otherwise, it's potentially overkill on download, since it is also
176 177 # verified as the file is streamed to the caller.
177 178 try:
178 179 verifies = store.verify(oid)
179 180 except IOError as inst:
180 181 if inst.errno != errno.ENOENT:
181 182 rsp['error'] = {
182 183 'code': 500,
183 184 'message': inst.strerror or 'Internal Server Server'
184 185 }
185 186 yield rsp
186 187 continue
187 188
188 189 exists = False
189 190
190 191 # Items are always listed for downloads. They are dropped for uploads
191 192 # IFF they already exist locally.
192 193 if action == 'download':
193 194 if not exists:
194 195 rsp['error'] = {
195 196 'code': 404,
196 197 'message': "The object does not exist"
197 198 }
198 199 yield rsp
199 200 continue
200 201
201 202 elif not verifies:
202 203 rsp['error'] = {
203 204 'code': 422, # XXX: is this the right code?
204 205 'message': "The object is corrupt"
205 206 }
206 207 yield rsp
207 208 continue
208 209
209 210 elif verifies:
210 211 yield rsp # Skip 'actions': already uploaded
211 212 continue
212 213
213 214 expiresat = datetime.datetime.now() + datetime.timedelta(minutes=10)
214 215
215 216 rsp['actions'] = {
216 217 '%s' % action: {
217 218 # TODO: Account for the --prefix, if any.
218 219 'href': '%s/.hg/lfs/objects/%s' % (req.baseurl, oid),
219 220 # datetime.isoformat() doesn't include the 'Z' suffix
220 221 "expires_at": expiresat.strftime('%Y-%m-%dT%H:%M:%SZ'),
221 222 'header': {
222 223 # The spec doesn't mention the Accept header here, but avoid
223 224 # a gratuitous deviation from lfs-test-server in the test
224 225 # output.
225 226 'Accept': 'application/vnd.git-lfs'
226 227 }
227 228 }
228 229 }
229 230
230 231 yield rsp
231 232
232 233 def _processbasictransfer(repo, req, res, checkperm):
233 234 """Handle a single file upload (PUT) or download (GET) action for the Basic
234 235 Transfer Adapter.
235 236
236 237 After determining if the request is for an upload or download, the access
237 238 must be checked by calling ``checkperm()`` with either 'pull' or 'upload'
238 239 before accessing the files.
239 240
240 241 https://github.com/git-lfs/git-lfs/blob/master/docs/api/basic-transfers.md
241 242 """
242 243
243 244 method = req.method
244 245 oid = req.dispatchparts[-1]
245 246 localstore = repo.svfs.lfslocalblobstore
246 247
248 if len(req.dispatchparts) != 4:
249 _sethttperror(res, HTTP_NOT_FOUND)
250 return True
251
247 252 if method == b'PUT':
248 253 checkperm('upload')
249 254
250 255 # TODO: verify Content-Type?
251 256
252 257 existed = localstore.has(oid)
253 258
254 259 # TODO: how to handle timeouts? The body proxy handles limiting to
255 260 # Content-Length, but what happens if a client sends less than it
256 261 # says it will?
257 262
258 263 # TODO: download() will abort if the checksum fails. It should raise
259 264 # something checksum specific that can be caught here, and turned
260 265 # into an http code.
261 266 localstore.download(oid, req.bodyfh)
262 267
263 268 statusmessage = hgwebcommon.statusmessage
264 269 res.status = statusmessage(HTTP_OK if existed else HTTP_CREATED)
265 270
266 271 # There's no payload here, but this is the header that lfs-test-server
267 272 # sends back. This eliminates some gratuitous test output conditionals.
268 273 res.headers[b'Content-Type'] = b'text/plain; charset=utf-8'
269 274 res.setbodybytes(b'')
270 275
271 276 return True
272 277 elif method == b'GET':
273 278 checkperm('pull')
274 279
275 280 res.status = hgwebcommon.statusmessage(HTTP_OK)
276 281 res.headers[b'Content-Type'] = b'application/octet-stream'
277 282
278 283 # TODO: figure out how to send back the file in chunks, instead of
279 284 # reading the whole thing.
280 285 res.setbodybytes(localstore.read(oid))
281 286
282 287 return True
283 288 else:
284 289 _sethttperror(res, HTTP_BAD_REQUEST,
285 290 message=b'Unsupported LFS transfer method: %s' % method)
286 291 return True
General Comments 0
You need to be logged in to leave comments. Login now