Show More
@@ -1,320 +1,327 b'' | |||||
1 | # wireprotolfsserver.py - lfs protocol server side implementation |
|
1 | # wireprotolfsserver.py - lfs protocol server side implementation | |
2 | # |
|
2 | # | |
3 | # Copyright 2018 Matt Harbison <matt_harbison@yahoo.com> |
|
3 | # Copyright 2018 Matt Harbison <matt_harbison@yahoo.com> | |
4 | # |
|
4 | # | |
5 | # This software may be used and distributed according to the terms of the |
|
5 | # This software may be used and distributed according to the terms of the | |
6 | # GNU General Public License version 2 or any later version. |
|
6 | # GNU General Public License version 2 or any later version. | |
7 |
|
7 | |||
8 | from __future__ import absolute_import |
|
8 | from __future__ import absolute_import | |
9 |
|
9 | |||
10 | import datetime |
|
10 | import datetime | |
11 | import errno |
|
11 | import errno | |
12 | import json |
|
12 | import json | |
13 | import traceback |
|
13 | import traceback | |
14 |
|
14 | |||
15 | from mercurial.hgweb import ( |
|
15 | from mercurial.hgweb import ( | |
16 | common as hgwebcommon, |
|
16 | common as hgwebcommon, | |
17 | ) |
|
17 | ) | |
18 |
|
18 | |||
19 | from mercurial import ( |
|
19 | from mercurial import ( | |
20 | pycompat, |
|
20 | pycompat, | |
21 | ) |
|
21 | ) | |
22 |
|
22 | |||
23 | from . import blobstore |
|
23 | from . import blobstore | |
24 |
|
24 | |||
25 | HTTP_OK = hgwebcommon.HTTP_OK |
|
25 | HTTP_OK = hgwebcommon.HTTP_OK | |
26 | HTTP_CREATED = hgwebcommon.HTTP_CREATED |
|
26 | HTTP_CREATED = hgwebcommon.HTTP_CREATED | |
27 | HTTP_BAD_REQUEST = hgwebcommon.HTTP_BAD_REQUEST |
|
27 | HTTP_BAD_REQUEST = hgwebcommon.HTTP_BAD_REQUEST | |
28 | HTTP_NOT_FOUND = hgwebcommon.HTTP_NOT_FOUND |
|
28 | HTTP_NOT_FOUND = hgwebcommon.HTTP_NOT_FOUND | |
|
29 | HTTP_METHOD_NOT_ALLOWED = hgwebcommon.HTTP_METHOD_NOT_ALLOWED | |||
|
30 | HTTP_NOT_ACCEPTABLE = hgwebcommon.HTTP_NOT_ACCEPTABLE | |||
|
31 | HTTP_UNSUPPORTED_MEDIA_TYPE = hgwebcommon.HTTP_UNSUPPORTED_MEDIA_TYPE | |||
29 |
|
32 | |||
30 | def handlewsgirequest(orig, rctx, req, res, checkperm): |
|
33 | def handlewsgirequest(orig, rctx, req, res, checkperm): | |
31 | """Wrap wireprotoserver.handlewsgirequest() to possibly process an LFS |
|
34 | """Wrap wireprotoserver.handlewsgirequest() to possibly process an LFS | |
32 | request if it is left unprocessed by the wrapped method. |
|
35 | request if it is left unprocessed by the wrapped method. | |
33 | """ |
|
36 | """ | |
34 | if orig(rctx, req, res, checkperm): |
|
37 | if orig(rctx, req, res, checkperm): | |
35 | return True |
|
38 | return True | |
36 |
|
39 | |||
37 | if not rctx.repo.ui.configbool('experimental', 'lfs.serve'): |
|
40 | if not rctx.repo.ui.configbool('experimental', 'lfs.serve'): | |
38 | return False |
|
41 | return False | |
39 |
|
42 | |||
40 | if not req.dispatchpath: |
|
43 | if not req.dispatchpath: | |
41 | return False |
|
44 | return False | |
42 |
|
45 | |||
43 | try: |
|
46 | try: | |
44 | if req.dispatchpath == b'.git/info/lfs/objects/batch': |
|
47 | if req.dispatchpath == b'.git/info/lfs/objects/batch': | |
45 | checkperm(rctx, req, 'pull') |
|
48 | checkperm(rctx, req, 'pull') | |
46 | return _processbatchrequest(rctx.repo, req, res) |
|
49 | return _processbatchrequest(rctx.repo, req, res) | |
47 | # TODO: reserve and use a path in the proposed http wireprotocol /api/ |
|
50 | # TODO: reserve and use a path in the proposed http wireprotocol /api/ | |
48 | # namespace? |
|
51 | # namespace? | |
49 | elif req.dispatchpath.startswith(b'.hg/lfs/objects'): |
|
52 | elif req.dispatchpath.startswith(b'.hg/lfs/objects'): | |
50 | return _processbasictransfer(rctx.repo, req, res, |
|
53 | return _processbasictransfer(rctx.repo, req, res, | |
51 | lambda perm: |
|
54 | lambda perm: | |
52 | checkperm(rctx, req, perm)) |
|
55 | checkperm(rctx, req, perm)) | |
53 | return False |
|
56 | return False | |
54 | except hgwebcommon.ErrorResponse as e: |
|
57 | except hgwebcommon.ErrorResponse as e: | |
55 | # XXX: copied from the handler surrounding wireprotoserver._callhttp() |
|
58 | # XXX: copied from the handler surrounding wireprotoserver._callhttp() | |
56 | # in the wrapped function. Should this be moved back to hgweb to |
|
59 | # in the wrapped function. Should this be moved back to hgweb to | |
57 | # be a common handler? |
|
60 | # be a common handler? | |
58 | for k, v in e.headers: |
|
61 | for k, v in e.headers: | |
59 | res.headers[k] = v |
|
62 | res.headers[k] = v | |
60 | res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e)) |
|
63 | res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e)) | |
61 | res.setbodybytes(b'0\n%s\n' % pycompat.bytestr(e)) |
|
64 | res.setbodybytes(b'0\n%s\n' % pycompat.bytestr(e)) | |
62 | return True |
|
65 | return True | |
63 |
|
66 | |||
64 | def _sethttperror(res, code, message=None): |
|
67 | def _sethttperror(res, code, message=None): | |
65 | res.status = hgwebcommon.statusmessage(code, message=message) |
|
68 | res.status = hgwebcommon.statusmessage(code, message=message) | |
66 | res.headers[b'Content-Type'] = b'text/plain; charset=utf-8' |
|
69 | res.headers[b'Content-Type'] = b'text/plain; charset=utf-8' | |
67 | res.setbodybytes(b'') |
|
70 | res.setbodybytes(b'') | |
68 |
|
71 | |||
69 | def _logexception(req): |
|
72 | def _logexception(req): | |
70 | """Write information about the current exception to wsgi.errors.""" |
|
73 | """Write information about the current exception to wsgi.errors.""" | |
71 | tb = pycompat.sysbytes(traceback.format_exc()) |
|
74 | tb = pycompat.sysbytes(traceback.format_exc()) | |
72 | errorlog = req.rawenv[r'wsgi.errors'] |
|
75 | errorlog = req.rawenv[r'wsgi.errors'] | |
73 |
|
76 | |||
74 | uri = b'' |
|
77 | uri = b'' | |
75 | if req.apppath: |
|
78 | if req.apppath: | |
76 | uri += req.apppath |
|
79 | uri += req.apppath | |
77 | uri += b'/' + req.dispatchpath |
|
80 | uri += b'/' + req.dispatchpath | |
78 |
|
81 | |||
79 | errorlog.write(b"Exception happened while processing request '%s':\n%s" % |
|
82 | errorlog.write(b"Exception happened while processing request '%s':\n%s" % | |
80 | (uri, tb)) |
|
83 | (uri, tb)) | |
81 |
|
84 | |||
82 | def _processbatchrequest(repo, req, res): |
|
85 | def _processbatchrequest(repo, req, res): | |
83 | """Handle a request for the Batch API, which is the gateway to granting file |
|
86 | """Handle a request for the Batch API, which is the gateway to granting file | |
84 | access. |
|
87 | access. | |
85 |
|
88 | |||
86 | https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md |
|
89 | https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md | |
87 | """ |
|
90 | """ | |
88 |
|
91 | |||
89 | # Mercurial client request: |
|
92 | # Mercurial client request: | |
90 | # |
|
93 | # | |
91 | # HOST: localhost:$HGPORT |
|
94 | # HOST: localhost:$HGPORT | |
92 | # ACCEPT: application/vnd.git-lfs+json |
|
95 | # ACCEPT: application/vnd.git-lfs+json | |
93 | # ACCEPT-ENCODING: identity |
|
96 | # ACCEPT-ENCODING: identity | |
94 | # USER-AGENT: git-lfs/2.3.4 (Mercurial 4.5.2+1114-f48b9754f04c+20180316) |
|
97 | # USER-AGENT: git-lfs/2.3.4 (Mercurial 4.5.2+1114-f48b9754f04c+20180316) | |
95 | # Content-Length: 125 |
|
98 | # Content-Length: 125 | |
96 | # Content-Type: application/vnd.git-lfs+json |
|
99 | # Content-Type: application/vnd.git-lfs+json | |
97 | # |
|
100 | # | |
98 | # { |
|
101 | # { | |
99 | # "objects": [ |
|
102 | # "objects": [ | |
100 | # { |
|
103 | # { | |
101 | # "oid": "31cf...8e5b" |
|
104 | # "oid": "31cf...8e5b" | |
102 | # "size": 12 |
|
105 | # "size": 12 | |
103 | # } |
|
106 | # } | |
104 | # ] |
|
107 | # ] | |
105 | # "operation": "upload" |
|
108 | # "operation": "upload" | |
106 | # } |
|
109 | # } | |
107 |
|
110 | |||
108 |
if |
|
111 | if req.method != b'POST': | |
109 | or req.headers[b'Content-Type'] != b'application/vnd.git-lfs+json' |
|
112 | _sethttperror(res, HTTP_METHOD_NOT_ALLOWED) | |
110 | or req.headers[b'Accept'] != b'application/vnd.git-lfs+json'): |
|
113 | return True | |
111 | # TODO: figure out what the proper handling for a bad request to the |
|
114 | ||
112 | # Batch API is. |
|
115 | if req.headers[b'Content-Type'] != b'application/vnd.git-lfs+json': | |
113 | _sethttperror(res, HTTP_BAD_REQUEST, b'Invalid Batch API request') |
|
116 | _sethttperror(res, HTTP_UNSUPPORTED_MEDIA_TYPE) | |
|
117 | return True | |||
|
118 | ||||
|
119 | if req.headers[b'Accept'] != b'application/vnd.git-lfs+json': | |||
|
120 | _sethttperror(res, HTTP_NOT_ACCEPTABLE) | |||
114 | return True |
|
121 | return True | |
115 |
|
122 | |||
116 | # XXX: specify an encoding? |
|
123 | # XXX: specify an encoding? | |
117 | lfsreq = json.loads(req.bodyfh.read()) |
|
124 | lfsreq = json.loads(req.bodyfh.read()) | |
118 |
|
125 | |||
119 | # If no transfer handlers are explicitly requested, 'basic' is assumed. |
|
126 | # If no transfer handlers are explicitly requested, 'basic' is assumed. | |
120 | if 'basic' not in lfsreq.get('transfers', ['basic']): |
|
127 | if 'basic' not in lfsreq.get('transfers', ['basic']): | |
121 | _sethttperror(res, HTTP_BAD_REQUEST, |
|
128 | _sethttperror(res, HTTP_BAD_REQUEST, | |
122 | b'Only the basic LFS transfer handler is supported') |
|
129 | b'Only the basic LFS transfer handler is supported') | |
123 | return True |
|
130 | return True | |
124 |
|
131 | |||
125 | operation = lfsreq.get('operation') |
|
132 | operation = lfsreq.get('operation') | |
126 | if operation not in ('upload', 'download'): |
|
133 | if operation not in ('upload', 'download'): | |
127 | _sethttperror(res, HTTP_BAD_REQUEST, |
|
134 | _sethttperror(res, HTTP_BAD_REQUEST, | |
128 | b'Unsupported LFS transfer operation: %s' % operation) |
|
135 | b'Unsupported LFS transfer operation: %s' % operation) | |
129 | return True |
|
136 | return True | |
130 |
|
137 | |||
131 | localstore = repo.svfs.lfslocalblobstore |
|
138 | localstore = repo.svfs.lfslocalblobstore | |
132 |
|
139 | |||
133 | objects = [p for p in _batchresponseobjects(req, lfsreq.get('objects', []), |
|
140 | objects = [p for p in _batchresponseobjects(req, lfsreq.get('objects', []), | |
134 | operation, localstore)] |
|
141 | operation, localstore)] | |
135 |
|
142 | |||
136 | rsp = { |
|
143 | rsp = { | |
137 | 'transfer': 'basic', |
|
144 | 'transfer': 'basic', | |
138 | 'objects': objects, |
|
145 | 'objects': objects, | |
139 | } |
|
146 | } | |
140 |
|
147 | |||
141 | res.status = hgwebcommon.statusmessage(HTTP_OK) |
|
148 | res.status = hgwebcommon.statusmessage(HTTP_OK) | |
142 | res.headers[b'Content-Type'] = b'application/vnd.git-lfs+json' |
|
149 | res.headers[b'Content-Type'] = b'application/vnd.git-lfs+json' | |
143 | res.setbodybytes(pycompat.bytestr(json.dumps(rsp))) |
|
150 | res.setbodybytes(pycompat.bytestr(json.dumps(rsp))) | |
144 |
|
151 | |||
145 | return True |
|
152 | return True | |
146 |
|
153 | |||
147 | def _batchresponseobjects(req, objects, action, store): |
|
154 | def _batchresponseobjects(req, objects, action, store): | |
148 | """Yield one dictionary of attributes for the Batch API response for each |
|
155 | """Yield one dictionary of attributes for the Batch API response for each | |
149 | object in the list. |
|
156 | object in the list. | |
150 |
|
157 | |||
151 | req: The parsedrequest for the Batch API request |
|
158 | req: The parsedrequest for the Batch API request | |
152 | objects: The list of objects in the Batch API object request list |
|
159 | objects: The list of objects in the Batch API object request list | |
153 | action: 'upload' or 'download' |
|
160 | action: 'upload' or 'download' | |
154 | store: The local blob store for servicing requests""" |
|
161 | store: The local blob store for servicing requests""" | |
155 |
|
162 | |||
156 | # Successful lfs-test-server response to solict an upload: |
|
163 | # Successful lfs-test-server response to solict an upload: | |
157 | # { |
|
164 | # { | |
158 | # u'objects': [{ |
|
165 | # u'objects': [{ | |
159 | # u'size': 12, |
|
166 | # u'size': 12, | |
160 | # u'oid': u'31cf...8e5b', |
|
167 | # u'oid': u'31cf...8e5b', | |
161 | # u'actions': { |
|
168 | # u'actions': { | |
162 | # u'upload': { |
|
169 | # u'upload': { | |
163 | # u'href': u'http://localhost:$HGPORT/objects/31cf...8e5b', |
|
170 | # u'href': u'http://localhost:$HGPORT/objects/31cf...8e5b', | |
164 | # u'expires_at': u'0001-01-01T00:00:00Z', |
|
171 | # u'expires_at': u'0001-01-01T00:00:00Z', | |
165 | # u'header': { |
|
172 | # u'header': { | |
166 | # u'Accept': u'application/vnd.git-lfs' |
|
173 | # u'Accept': u'application/vnd.git-lfs' | |
167 | # } |
|
174 | # } | |
168 | # } |
|
175 | # } | |
169 | # } |
|
176 | # } | |
170 | # }] |
|
177 | # }] | |
171 | # } |
|
178 | # } | |
172 |
|
179 | |||
173 | # TODO: Sort out the expires_at/expires_in/authenticated keys. |
|
180 | # TODO: Sort out the expires_at/expires_in/authenticated keys. | |
174 |
|
181 | |||
175 | for obj in objects: |
|
182 | for obj in objects: | |
176 | # Convert unicode to ASCII to create a filesystem path |
|
183 | # Convert unicode to ASCII to create a filesystem path | |
177 | oid = obj.get('oid').encode('ascii') |
|
184 | oid = obj.get('oid').encode('ascii') | |
178 | rsp = { |
|
185 | rsp = { | |
179 | 'oid': oid, |
|
186 | 'oid': oid, | |
180 | 'size': obj.get('size'), # XXX: should this check the local size? |
|
187 | 'size': obj.get('size'), # XXX: should this check the local size? | |
181 | #'authenticated': True, |
|
188 | #'authenticated': True, | |
182 | } |
|
189 | } | |
183 |
|
190 | |||
184 | exists = True |
|
191 | exists = True | |
185 | verifies = False |
|
192 | verifies = False | |
186 |
|
193 | |||
187 | # Verify an existing file on the upload request, so that the client is |
|
194 | # Verify an existing file on the upload request, so that the client is | |
188 | # solicited to re-upload if it corrupt locally. Download requests are |
|
195 | # solicited to re-upload if it corrupt locally. Download requests are | |
189 | # also verified, so the error can be flagged in the Batch API response. |
|
196 | # also verified, so the error can be flagged in the Batch API response. | |
190 | # (Maybe we can use this to short circuit the download for `hg verify`, |
|
197 | # (Maybe we can use this to short circuit the download for `hg verify`, | |
191 | # IFF the client can assert that the remote end is an hg server.) |
|
198 | # IFF the client can assert that the remote end is an hg server.) | |
192 | # Otherwise, it's potentially overkill on download, since it is also |
|
199 | # Otherwise, it's potentially overkill on download, since it is also | |
193 | # verified as the file is streamed to the caller. |
|
200 | # verified as the file is streamed to the caller. | |
194 | try: |
|
201 | try: | |
195 | verifies = store.verify(oid) |
|
202 | verifies = store.verify(oid) | |
196 | except IOError as inst: |
|
203 | except IOError as inst: | |
197 | if inst.errno != errno.ENOENT: |
|
204 | if inst.errno != errno.ENOENT: | |
198 | _logexception(req) |
|
205 | _logexception(req) | |
199 |
|
206 | |||
200 | rsp['error'] = { |
|
207 | rsp['error'] = { | |
201 | 'code': 500, |
|
208 | 'code': 500, | |
202 | 'message': inst.strerror or 'Internal Server Server' |
|
209 | 'message': inst.strerror or 'Internal Server Server' | |
203 | } |
|
210 | } | |
204 | yield rsp |
|
211 | yield rsp | |
205 | continue |
|
212 | continue | |
206 |
|
213 | |||
207 | exists = False |
|
214 | exists = False | |
208 |
|
215 | |||
209 | # Items are always listed for downloads. They are dropped for uploads |
|
216 | # Items are always listed for downloads. They are dropped for uploads | |
210 | # IFF they already exist locally. |
|
217 | # IFF they already exist locally. | |
211 | if action == 'download': |
|
218 | if action == 'download': | |
212 | if not exists: |
|
219 | if not exists: | |
213 | rsp['error'] = { |
|
220 | rsp['error'] = { | |
214 | 'code': 404, |
|
221 | 'code': 404, | |
215 | 'message': "The object does not exist" |
|
222 | 'message': "The object does not exist" | |
216 | } |
|
223 | } | |
217 | yield rsp |
|
224 | yield rsp | |
218 | continue |
|
225 | continue | |
219 |
|
226 | |||
220 | elif not verifies: |
|
227 | elif not verifies: | |
221 | rsp['error'] = { |
|
228 | rsp['error'] = { | |
222 | 'code': 422, # XXX: is this the right code? |
|
229 | 'code': 422, # XXX: is this the right code? | |
223 | 'message': "The object is corrupt" |
|
230 | 'message': "The object is corrupt" | |
224 | } |
|
231 | } | |
225 | yield rsp |
|
232 | yield rsp | |
226 | continue |
|
233 | continue | |
227 |
|
234 | |||
228 | elif verifies: |
|
235 | elif verifies: | |
229 | yield rsp # Skip 'actions': already uploaded |
|
236 | yield rsp # Skip 'actions': already uploaded | |
230 | continue |
|
237 | continue | |
231 |
|
238 | |||
232 | expiresat = datetime.datetime.now() + datetime.timedelta(minutes=10) |
|
239 | expiresat = datetime.datetime.now() + datetime.timedelta(minutes=10) | |
233 |
|
240 | |||
234 | rsp['actions'] = { |
|
241 | rsp['actions'] = { | |
235 | '%s' % action: { |
|
242 | '%s' % action: { | |
236 | 'href': '%s%s/.hg/lfs/objects/%s' |
|
243 | 'href': '%s%s/.hg/lfs/objects/%s' | |
237 | % (req.baseurl, req.apppath, oid), |
|
244 | % (req.baseurl, req.apppath, oid), | |
238 | # datetime.isoformat() doesn't include the 'Z' suffix |
|
245 | # datetime.isoformat() doesn't include the 'Z' suffix | |
239 | "expires_at": expiresat.strftime('%Y-%m-%dT%H:%M:%SZ'), |
|
246 | "expires_at": expiresat.strftime('%Y-%m-%dT%H:%M:%SZ'), | |
240 | 'header': { |
|
247 | 'header': { | |
241 | # The spec doesn't mention the Accept header here, but avoid |
|
248 | # The spec doesn't mention the Accept header here, but avoid | |
242 | # a gratuitous deviation from lfs-test-server in the test |
|
249 | # a gratuitous deviation from lfs-test-server in the test | |
243 | # output. |
|
250 | # output. | |
244 | 'Accept': 'application/vnd.git-lfs' |
|
251 | 'Accept': 'application/vnd.git-lfs' | |
245 | } |
|
252 | } | |
246 | } |
|
253 | } | |
247 | } |
|
254 | } | |
248 |
|
255 | |||
249 | yield rsp |
|
256 | yield rsp | |
250 |
|
257 | |||
251 | def _processbasictransfer(repo, req, res, checkperm): |
|
258 | def _processbasictransfer(repo, req, res, checkperm): | |
252 | """Handle a single file upload (PUT) or download (GET) action for the Basic |
|
259 | """Handle a single file upload (PUT) or download (GET) action for the Basic | |
253 | Transfer Adapter. |
|
260 | Transfer Adapter. | |
254 |
|
261 | |||
255 | After determining if the request is for an upload or download, the access |
|
262 | After determining if the request is for an upload or download, the access | |
256 | must be checked by calling ``checkperm()`` with either 'pull' or 'upload' |
|
263 | must be checked by calling ``checkperm()`` with either 'pull' or 'upload' | |
257 | before accessing the files. |
|
264 | before accessing the files. | |
258 |
|
265 | |||
259 | https://github.com/git-lfs/git-lfs/blob/master/docs/api/basic-transfers.md |
|
266 | https://github.com/git-lfs/git-lfs/blob/master/docs/api/basic-transfers.md | |
260 | """ |
|
267 | """ | |
261 |
|
268 | |||
262 | method = req.method |
|
269 | method = req.method | |
263 | oid = req.dispatchparts[-1] |
|
270 | oid = req.dispatchparts[-1] | |
264 | localstore = repo.svfs.lfslocalblobstore |
|
271 | localstore = repo.svfs.lfslocalblobstore | |
265 |
|
272 | |||
266 | if len(req.dispatchparts) != 4: |
|
273 | if len(req.dispatchparts) != 4: | |
267 | _sethttperror(res, HTTP_NOT_FOUND) |
|
274 | _sethttperror(res, HTTP_NOT_FOUND) | |
268 | return True |
|
275 | return True | |
269 |
|
276 | |||
270 | if method == b'PUT': |
|
277 | if method == b'PUT': | |
271 | checkperm('upload') |
|
278 | checkperm('upload') | |
272 |
|
279 | |||
273 | # TODO: verify Content-Type? |
|
280 | # TODO: verify Content-Type? | |
274 |
|
281 | |||
275 | existed = localstore.has(oid) |
|
282 | existed = localstore.has(oid) | |
276 |
|
283 | |||
277 | # TODO: how to handle timeouts? The body proxy handles limiting to |
|
284 | # TODO: how to handle timeouts? The body proxy handles limiting to | |
278 | # Content-Length, but what happens if a client sends less than it |
|
285 | # Content-Length, but what happens if a client sends less than it | |
279 | # says it will? |
|
286 | # says it will? | |
280 |
|
287 | |||
281 | statusmessage = hgwebcommon.statusmessage |
|
288 | statusmessage = hgwebcommon.statusmessage | |
282 | try: |
|
289 | try: | |
283 | localstore.download(oid, req.bodyfh) |
|
290 | localstore.download(oid, req.bodyfh) | |
284 | res.status = statusmessage(HTTP_OK if existed else HTTP_CREATED) |
|
291 | res.status = statusmessage(HTTP_OK if existed else HTTP_CREATED) | |
285 | except blobstore.LfsCorruptionError: |
|
292 | except blobstore.LfsCorruptionError: | |
286 | _logexception(req) |
|
293 | _logexception(req) | |
287 |
|
294 | |||
288 | # XXX: Is this the right code? |
|
295 | # XXX: Is this the right code? | |
289 | res.status = statusmessage(422, b'corrupt blob') |
|
296 | res.status = statusmessage(422, b'corrupt blob') | |
290 |
|
297 | |||
291 | # There's no payload here, but this is the header that lfs-test-server |
|
298 | # There's no payload here, but this is the header that lfs-test-server | |
292 | # sends back. This eliminates some gratuitous test output conditionals. |
|
299 | # sends back. This eliminates some gratuitous test output conditionals. | |
293 | res.headers[b'Content-Type'] = b'text/plain; charset=utf-8' |
|
300 | res.headers[b'Content-Type'] = b'text/plain; charset=utf-8' | |
294 | res.setbodybytes(b'') |
|
301 | res.setbodybytes(b'') | |
295 |
|
302 | |||
296 | return True |
|
303 | return True | |
297 | elif method == b'GET': |
|
304 | elif method == b'GET': | |
298 | checkperm('pull') |
|
305 | checkperm('pull') | |
299 |
|
306 | |||
300 | res.status = hgwebcommon.statusmessage(HTTP_OK) |
|
307 | res.status = hgwebcommon.statusmessage(HTTP_OK) | |
301 | res.headers[b'Content-Type'] = b'application/octet-stream' |
|
308 | res.headers[b'Content-Type'] = b'application/octet-stream' | |
302 |
|
309 | |||
303 | try: |
|
310 | try: | |
304 | # TODO: figure out how to send back the file in chunks, instead of |
|
311 | # TODO: figure out how to send back the file in chunks, instead of | |
305 | # reading the whole thing. (Also figure out how to send back |
|
312 | # reading the whole thing. (Also figure out how to send back | |
306 | # an error status if an IOError occurs after a partial write |
|
313 | # an error status if an IOError occurs after a partial write | |
307 | # in that case. Here, everything is read before starting.) |
|
314 | # in that case. Here, everything is read before starting.) | |
308 | res.setbodybytes(localstore.read(oid)) |
|
315 | res.setbodybytes(localstore.read(oid)) | |
309 | except blobstore.LfsCorruptionError: |
|
316 | except blobstore.LfsCorruptionError: | |
310 | _logexception(req) |
|
317 | _logexception(req) | |
311 |
|
318 | |||
312 | # XXX: Is this the right code? |
|
319 | # XXX: Is this the right code? | |
313 | res.status = hgwebcommon.statusmessage(422, b'corrupt blob') |
|
320 | res.status = hgwebcommon.statusmessage(422, b'corrupt blob') | |
314 | res.setbodybytes(b'') |
|
321 | res.setbodybytes(b'') | |
315 |
|
322 | |||
316 | return True |
|
323 | return True | |
317 | else: |
|
324 | else: | |
318 |
_sethttperror(res, HTTP_ |
|
325 | _sethttperror(res, HTTP_METHOD_NOT_ALLOWED, | |
319 | message=b'Unsupported LFS transfer method: %s' % method) |
|
326 | message=b'Unsupported LFS transfer method: %s' % method) | |
320 | return True |
|
327 | return True |
@@ -1,258 +1,260 b'' | |||||
1 | # hgweb/common.py - Utility functions needed by hgweb_mod and hgwebdir_mod |
|
1 | # hgweb/common.py - Utility functions needed by hgweb_mod and hgwebdir_mod | |
2 | # |
|
2 | # | |
3 | # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net> |
|
3 | # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net> | |
4 | # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com> |
|
4 | # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com> | |
5 | # |
|
5 | # | |
6 | # This software may be used and distributed according to the terms of the |
|
6 | # This software may be used and distributed according to the terms of the | |
7 | # GNU General Public License version 2 or any later version. |
|
7 | # GNU General Public License version 2 or any later version. | |
8 |
|
8 | |||
9 | from __future__ import absolute_import |
|
9 | from __future__ import absolute_import | |
10 |
|
10 | |||
11 | import base64 |
|
11 | import base64 | |
12 | import errno |
|
12 | import errno | |
13 | import mimetypes |
|
13 | import mimetypes | |
14 | import os |
|
14 | import os | |
15 | import stat |
|
15 | import stat | |
16 |
|
16 | |||
17 | from .. import ( |
|
17 | from .. import ( | |
18 | encoding, |
|
18 | encoding, | |
19 | pycompat, |
|
19 | pycompat, | |
20 | util, |
|
20 | util, | |
21 | ) |
|
21 | ) | |
22 |
|
22 | |||
23 | httpserver = util.httpserver |
|
23 | httpserver = util.httpserver | |
24 |
|
24 | |||
25 | HTTP_OK = 200 |
|
25 | HTTP_OK = 200 | |
26 | HTTP_CREATED = 201 |
|
26 | HTTP_CREATED = 201 | |
27 | HTTP_NOT_MODIFIED = 304 |
|
27 | HTTP_NOT_MODIFIED = 304 | |
28 | HTTP_BAD_REQUEST = 400 |
|
28 | HTTP_BAD_REQUEST = 400 | |
29 | HTTP_UNAUTHORIZED = 401 |
|
29 | HTTP_UNAUTHORIZED = 401 | |
30 | HTTP_FORBIDDEN = 403 |
|
30 | HTTP_FORBIDDEN = 403 | |
31 | HTTP_NOT_FOUND = 404 |
|
31 | HTTP_NOT_FOUND = 404 | |
32 | HTTP_METHOD_NOT_ALLOWED = 405 |
|
32 | HTTP_METHOD_NOT_ALLOWED = 405 | |
|
33 | HTTP_NOT_ACCEPTABLE = 406 | |||
|
34 | HTTP_UNSUPPORTED_MEDIA_TYPE = 415 | |||
33 | HTTP_SERVER_ERROR = 500 |
|
35 | HTTP_SERVER_ERROR = 500 | |
34 |
|
36 | |||
35 |
|
37 | |||
36 | def ismember(ui, username, userlist): |
|
38 | def ismember(ui, username, userlist): | |
37 | """Check if username is a member of userlist. |
|
39 | """Check if username is a member of userlist. | |
38 |
|
40 | |||
39 | If userlist has a single '*' member, all users are considered members. |
|
41 | If userlist has a single '*' member, all users are considered members. | |
40 | Can be overridden by extensions to provide more complex authorization |
|
42 | Can be overridden by extensions to provide more complex authorization | |
41 | schemes. |
|
43 | schemes. | |
42 | """ |
|
44 | """ | |
43 | return userlist == ['*'] or username in userlist |
|
45 | return userlist == ['*'] or username in userlist | |
44 |
|
46 | |||
45 | def checkauthz(hgweb, req, op): |
|
47 | def checkauthz(hgweb, req, op): | |
46 | '''Check permission for operation based on request data (including |
|
48 | '''Check permission for operation based on request data (including | |
47 | authentication info). Return if op allowed, else raise an ErrorResponse |
|
49 | authentication info). Return if op allowed, else raise an ErrorResponse | |
48 | exception.''' |
|
50 | exception.''' | |
49 |
|
51 | |||
50 | user = req.remoteuser |
|
52 | user = req.remoteuser | |
51 |
|
53 | |||
52 | deny_read = hgweb.configlist('web', 'deny_read') |
|
54 | deny_read = hgweb.configlist('web', 'deny_read') | |
53 | if deny_read and (not user or ismember(hgweb.repo.ui, user, deny_read)): |
|
55 | if deny_read and (not user or ismember(hgweb.repo.ui, user, deny_read)): | |
54 | raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized') |
|
56 | raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized') | |
55 |
|
57 | |||
56 | allow_read = hgweb.configlist('web', 'allow_read') |
|
58 | allow_read = hgweb.configlist('web', 'allow_read') | |
57 | if allow_read and (not ismember(hgweb.repo.ui, user, allow_read)): |
|
59 | if allow_read and (not ismember(hgweb.repo.ui, user, allow_read)): | |
58 | raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized') |
|
60 | raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized') | |
59 |
|
61 | |||
60 | if op == 'pull' and not hgweb.allowpull: |
|
62 | if op == 'pull' and not hgweb.allowpull: | |
61 | raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized') |
|
63 | raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized') | |
62 | elif op == 'pull' or op is None: # op is None for interface requests |
|
64 | elif op == 'pull' or op is None: # op is None for interface requests | |
63 | return |
|
65 | return | |
64 |
|
66 | |||
65 | # Allow LFS uploading via PUT requests |
|
67 | # Allow LFS uploading via PUT requests | |
66 | if op == 'upload': |
|
68 | if op == 'upload': | |
67 | if req.method != 'PUT': |
|
69 | if req.method != 'PUT': | |
68 | msg = 'upload requires PUT request' |
|
70 | msg = 'upload requires PUT request' | |
69 | raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg) |
|
71 | raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg) | |
70 | # enforce that you can only push using POST requests |
|
72 | # enforce that you can only push using POST requests | |
71 | elif req.method != 'POST': |
|
73 | elif req.method != 'POST': | |
72 | msg = 'push requires POST request' |
|
74 | msg = 'push requires POST request' | |
73 | raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg) |
|
75 | raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg) | |
74 |
|
76 | |||
75 | # require ssl by default for pushing, auth info cannot be sniffed |
|
77 | # require ssl by default for pushing, auth info cannot be sniffed | |
76 | # and replayed |
|
78 | # and replayed | |
77 | if hgweb.configbool('web', 'push_ssl') and req.urlscheme != 'https': |
|
79 | if hgweb.configbool('web', 'push_ssl') and req.urlscheme != 'https': | |
78 | raise ErrorResponse(HTTP_FORBIDDEN, 'ssl required') |
|
80 | raise ErrorResponse(HTTP_FORBIDDEN, 'ssl required') | |
79 |
|
81 | |||
80 | deny = hgweb.configlist('web', 'deny_push') |
|
82 | deny = hgweb.configlist('web', 'deny_push') | |
81 | if deny and (not user or ismember(hgweb.repo.ui, user, deny)): |
|
83 | if deny and (not user or ismember(hgweb.repo.ui, user, deny)): | |
82 | raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized') |
|
84 | raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized') | |
83 |
|
85 | |||
84 | allow = hgweb.configlist('web', 'allow-push') |
|
86 | allow = hgweb.configlist('web', 'allow-push') | |
85 | if not (allow and ismember(hgweb.repo.ui, user, allow)): |
|
87 | if not (allow and ismember(hgweb.repo.ui, user, allow)): | |
86 | raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized') |
|
88 | raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized') | |
87 |
|
89 | |||
88 | # Hooks for hgweb permission checks; extensions can add hooks here. |
|
90 | # Hooks for hgweb permission checks; extensions can add hooks here. | |
89 | # Each hook is invoked like this: hook(hgweb, request, operation), |
|
91 | # Each hook is invoked like this: hook(hgweb, request, operation), | |
90 | # where operation is either read, pull, push or upload. Hooks should either |
|
92 | # where operation is either read, pull, push or upload. Hooks should either | |
91 | # raise an ErrorResponse exception, or just return. |
|
93 | # raise an ErrorResponse exception, or just return. | |
92 | # |
|
94 | # | |
93 | # It is possible to do both authentication and authorization through |
|
95 | # It is possible to do both authentication and authorization through | |
94 | # this. |
|
96 | # this. | |
95 | permhooks = [checkauthz] |
|
97 | permhooks = [checkauthz] | |
96 |
|
98 | |||
97 |
|
99 | |||
98 | class ErrorResponse(Exception): |
|
100 | class ErrorResponse(Exception): | |
99 | def __init__(self, code, message=None, headers=None): |
|
101 | def __init__(self, code, message=None, headers=None): | |
100 | if message is None: |
|
102 | if message is None: | |
101 | message = _statusmessage(code) |
|
103 | message = _statusmessage(code) | |
102 | Exception.__init__(self, pycompat.sysstr(message)) |
|
104 | Exception.__init__(self, pycompat.sysstr(message)) | |
103 | self.code = code |
|
105 | self.code = code | |
104 | if headers is None: |
|
106 | if headers is None: | |
105 | headers = [] |
|
107 | headers = [] | |
106 | self.headers = headers |
|
108 | self.headers = headers | |
107 |
|
109 | |||
108 | class continuereader(object): |
|
110 | class continuereader(object): | |
109 | """File object wrapper to handle HTTP 100-continue. |
|
111 | """File object wrapper to handle HTTP 100-continue. | |
110 |
|
112 | |||
111 | This is used by servers so they automatically handle Expect: 100-continue |
|
113 | This is used by servers so they automatically handle Expect: 100-continue | |
112 | request headers. On first read of the request body, the 100 Continue |
|
114 | request headers. On first read of the request body, the 100 Continue | |
113 | response is sent. This should trigger the client into actually sending |
|
115 | response is sent. This should trigger the client into actually sending | |
114 | the request body. |
|
116 | the request body. | |
115 | """ |
|
117 | """ | |
116 | def __init__(self, f, write): |
|
118 | def __init__(self, f, write): | |
117 | self.f = f |
|
119 | self.f = f | |
118 | self._write = write |
|
120 | self._write = write | |
119 | self.continued = False |
|
121 | self.continued = False | |
120 |
|
122 | |||
121 | def read(self, amt=-1): |
|
123 | def read(self, amt=-1): | |
122 | if not self.continued: |
|
124 | if not self.continued: | |
123 | self.continued = True |
|
125 | self.continued = True | |
124 | self._write('HTTP/1.1 100 Continue\r\n\r\n') |
|
126 | self._write('HTTP/1.1 100 Continue\r\n\r\n') | |
125 | return self.f.read(amt) |
|
127 | return self.f.read(amt) | |
126 |
|
128 | |||
127 | def __getattr__(self, attr): |
|
129 | def __getattr__(self, attr): | |
128 | if attr in ('close', 'readline', 'readlines', '__iter__'): |
|
130 | if attr in ('close', 'readline', 'readlines', '__iter__'): | |
129 | return getattr(self.f, attr) |
|
131 | return getattr(self.f, attr) | |
130 | raise AttributeError |
|
132 | raise AttributeError | |
131 |
|
133 | |||
132 | def _statusmessage(code): |
|
134 | def _statusmessage(code): | |
133 | responses = httpserver.basehttprequesthandler.responses |
|
135 | responses = httpserver.basehttprequesthandler.responses | |
134 | return responses.get(code, ('Error', 'Unknown error'))[0] |
|
136 | return responses.get(code, ('Error', 'Unknown error'))[0] | |
135 |
|
137 | |||
136 | def statusmessage(code, message=None): |
|
138 | def statusmessage(code, message=None): | |
137 | return '%d %s' % (code, message or _statusmessage(code)) |
|
139 | return '%d %s' % (code, message or _statusmessage(code)) | |
138 |
|
140 | |||
139 | def get_stat(spath, fn): |
|
141 | def get_stat(spath, fn): | |
140 | """stat fn if it exists, spath otherwise""" |
|
142 | """stat fn if it exists, spath otherwise""" | |
141 | cl_path = os.path.join(spath, fn) |
|
143 | cl_path = os.path.join(spath, fn) | |
142 | if os.path.exists(cl_path): |
|
144 | if os.path.exists(cl_path): | |
143 | return os.stat(cl_path) |
|
145 | return os.stat(cl_path) | |
144 | else: |
|
146 | else: | |
145 | return os.stat(spath) |
|
147 | return os.stat(spath) | |
146 |
|
148 | |||
147 | def get_mtime(spath): |
|
149 | def get_mtime(spath): | |
148 | return get_stat(spath, "00changelog.i")[stat.ST_MTIME] |
|
150 | return get_stat(spath, "00changelog.i")[stat.ST_MTIME] | |
149 |
|
151 | |||
150 | def ispathsafe(path): |
|
152 | def ispathsafe(path): | |
151 | """Determine if a path is safe to use for filesystem access.""" |
|
153 | """Determine if a path is safe to use for filesystem access.""" | |
152 | parts = path.split('/') |
|
154 | parts = path.split('/') | |
153 | for part in parts: |
|
155 | for part in parts: | |
154 | if (part in ('', pycompat.oscurdir, pycompat.ospardir) or |
|
156 | if (part in ('', pycompat.oscurdir, pycompat.ospardir) or | |
155 | pycompat.ossep in part or |
|
157 | pycompat.ossep in part or | |
156 | pycompat.osaltsep is not None and pycompat.osaltsep in part): |
|
158 | pycompat.osaltsep is not None and pycompat.osaltsep in part): | |
157 | return False |
|
159 | return False | |
158 |
|
160 | |||
159 | return True |
|
161 | return True | |
160 |
|
162 | |||
161 | def staticfile(directory, fname, res): |
|
163 | def staticfile(directory, fname, res): | |
162 | """return a file inside directory with guessed Content-Type header |
|
164 | """return a file inside directory with guessed Content-Type header | |
163 |
|
165 | |||
164 | fname always uses '/' as directory separator and isn't allowed to |
|
166 | fname always uses '/' as directory separator and isn't allowed to | |
165 | contain unusual path components. |
|
167 | contain unusual path components. | |
166 | Content-Type is guessed using the mimetypes module. |
|
168 | Content-Type is guessed using the mimetypes module. | |
167 | Return an empty string if fname is illegal or file not found. |
|
169 | Return an empty string if fname is illegal or file not found. | |
168 |
|
170 | |||
169 | """ |
|
171 | """ | |
170 | if not ispathsafe(fname): |
|
172 | if not ispathsafe(fname): | |
171 | return |
|
173 | return | |
172 |
|
174 | |||
173 | fpath = os.path.join(*fname.split('/')) |
|
175 | fpath = os.path.join(*fname.split('/')) | |
174 | if isinstance(directory, str): |
|
176 | if isinstance(directory, str): | |
175 | directory = [directory] |
|
177 | directory = [directory] | |
176 | for d in directory: |
|
178 | for d in directory: | |
177 | path = os.path.join(d, fpath) |
|
179 | path = os.path.join(d, fpath) | |
178 | if os.path.exists(path): |
|
180 | if os.path.exists(path): | |
179 | break |
|
181 | break | |
180 | try: |
|
182 | try: | |
181 | os.stat(path) |
|
183 | os.stat(path) | |
182 | ct = mimetypes.guess_type(pycompat.fsdecode(path))[0] or "text/plain" |
|
184 | ct = mimetypes.guess_type(pycompat.fsdecode(path))[0] or "text/plain" | |
183 | with open(path, 'rb') as fh: |
|
185 | with open(path, 'rb') as fh: | |
184 | data = fh.read() |
|
186 | data = fh.read() | |
185 |
|
187 | |||
186 | res.headers['Content-Type'] = ct |
|
188 | res.headers['Content-Type'] = ct | |
187 | res.setbodybytes(data) |
|
189 | res.setbodybytes(data) | |
188 | return res |
|
190 | return res | |
189 | except TypeError: |
|
191 | except TypeError: | |
190 | raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename') |
|
192 | raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename') | |
191 | except OSError as err: |
|
193 | except OSError as err: | |
192 | if err.errno == errno.ENOENT: |
|
194 | if err.errno == errno.ENOENT: | |
193 | raise ErrorResponse(HTTP_NOT_FOUND) |
|
195 | raise ErrorResponse(HTTP_NOT_FOUND) | |
194 | else: |
|
196 | else: | |
195 | raise ErrorResponse(HTTP_SERVER_ERROR, |
|
197 | raise ErrorResponse(HTTP_SERVER_ERROR, | |
196 | encoding.strtolocal(err.strerror)) |
|
198 | encoding.strtolocal(err.strerror)) | |
197 |
|
199 | |||
198 | def paritygen(stripecount, offset=0): |
|
200 | def paritygen(stripecount, offset=0): | |
199 | """count parity of horizontal stripes for easier reading""" |
|
201 | """count parity of horizontal stripes for easier reading""" | |
200 | if stripecount and offset: |
|
202 | if stripecount and offset: | |
201 | # account for offset, e.g. due to building the list in reverse |
|
203 | # account for offset, e.g. due to building the list in reverse | |
202 | count = (stripecount + offset) % stripecount |
|
204 | count = (stripecount + offset) % stripecount | |
203 | parity = (stripecount + offset) // stripecount & 1 |
|
205 | parity = (stripecount + offset) // stripecount & 1 | |
204 | else: |
|
206 | else: | |
205 | count = 0 |
|
207 | count = 0 | |
206 | parity = 0 |
|
208 | parity = 0 | |
207 | while True: |
|
209 | while True: | |
208 | yield parity |
|
210 | yield parity | |
209 | count += 1 |
|
211 | count += 1 | |
210 | if stripecount and count >= stripecount: |
|
212 | if stripecount and count >= stripecount: | |
211 | parity = 1 - parity |
|
213 | parity = 1 - parity | |
212 | count = 0 |
|
214 | count = 0 | |
213 |
|
215 | |||
214 | def get_contact(config): |
|
216 | def get_contact(config): | |
215 | """Return repo contact information or empty string. |
|
217 | """Return repo contact information or empty string. | |
216 |
|
218 | |||
217 | web.contact is the primary source, but if that is not set, try |
|
219 | web.contact is the primary source, but if that is not set, try | |
218 | ui.username or $EMAIL as a fallback to display something useful. |
|
220 | ui.username or $EMAIL as a fallback to display something useful. | |
219 | """ |
|
221 | """ | |
220 | return (config("web", "contact") or |
|
222 | return (config("web", "contact") or | |
221 | config("ui", "username") or |
|
223 | config("ui", "username") or | |
222 | encoding.environ.get("EMAIL") or "") |
|
224 | encoding.environ.get("EMAIL") or "") | |
223 |
|
225 | |||
224 | def cspvalues(ui): |
|
226 | def cspvalues(ui): | |
225 | """Obtain the Content-Security-Policy header and nonce value. |
|
227 | """Obtain the Content-Security-Policy header and nonce value. | |
226 |
|
228 | |||
227 | Returns a 2-tuple of the CSP header value and the nonce value. |
|
229 | Returns a 2-tuple of the CSP header value and the nonce value. | |
228 |
|
230 | |||
229 | First value is ``None`` if CSP isn't enabled. Second value is ``None`` |
|
231 | First value is ``None`` if CSP isn't enabled. Second value is ``None`` | |
230 | if CSP isn't enabled or if the CSP header doesn't need a nonce. |
|
232 | if CSP isn't enabled or if the CSP header doesn't need a nonce. | |
231 | """ |
|
233 | """ | |
232 | # Without demandimport, "import uuid" could have an immediate side-effect |
|
234 | # Without demandimport, "import uuid" could have an immediate side-effect | |
233 | # running "ldconfig" on Linux trying to find libuuid. |
|
235 | # running "ldconfig" on Linux trying to find libuuid. | |
234 | # With Python <= 2.7.12, that "ldconfig" is run via a shell and the shell |
|
236 | # With Python <= 2.7.12, that "ldconfig" is run via a shell and the shell | |
235 | # may pollute the terminal with: |
|
237 | # may pollute the terminal with: | |
236 | # |
|
238 | # | |
237 | # shell-init: error retrieving current directory: getcwd: cannot access |
|
239 | # shell-init: error retrieving current directory: getcwd: cannot access | |
238 | # parent directories: No such file or directory |
|
240 | # parent directories: No such file or directory | |
239 | # |
|
241 | # | |
240 | # Python >= 2.7.13 has fixed it by running "ldconfig" directly without a |
|
242 | # Python >= 2.7.13 has fixed it by running "ldconfig" directly without a | |
241 | # shell (hg changeset a09ae70f3489). |
|
243 | # shell (hg changeset a09ae70f3489). | |
242 | # |
|
244 | # | |
243 | # Moved "import uuid" from here so it's executed after we know we have |
|
245 | # Moved "import uuid" from here so it's executed after we know we have | |
244 | # a sane cwd (i.e. after dispatch.py cwd check). |
|
246 | # a sane cwd (i.e. after dispatch.py cwd check). | |
245 | # |
|
247 | # | |
246 | # We can move it back once we no longer need Python <= 2.7.12 support. |
|
248 | # We can move it back once we no longer need Python <= 2.7.12 support. | |
247 | import uuid |
|
249 | import uuid | |
248 |
|
250 | |||
249 | # Don't allow untrusted CSP setting since it be disable protections |
|
251 | # Don't allow untrusted CSP setting since it be disable protections | |
250 | # from a trusted/global source. |
|
252 | # from a trusted/global source. | |
251 | csp = ui.config('web', 'csp', untrusted=False) |
|
253 | csp = ui.config('web', 'csp', untrusted=False) | |
252 | nonce = None |
|
254 | nonce = None | |
253 |
|
255 | |||
254 | if csp and '%nonce%' in csp: |
|
256 | if csp and '%nonce%' in csp: | |
255 | nonce = base64.urlsafe_b64encode(uuid.uuid4().bytes).rstrip('=') |
|
257 | nonce = base64.urlsafe_b64encode(uuid.uuid4().bytes).rstrip('=') | |
256 | csp = csp.replace('%nonce%', nonce) |
|
258 | csp = csp.replace('%nonce%', nonce) | |
257 |
|
259 | |||
258 | return csp, nonce |
|
260 | return csp, nonce |
General Comments 0
You need to be logged in to leave comments.
Login now