228 # "unbundle." That assumption is not always valid. |
231 # "unbundle." That assumption is not always valid. |
229 res.setbodybytes('0\n%s\n' % pycompat.bytestr(e)) |
232 res.setbodybytes('0\n%s\n' % pycompat.bytestr(e)) |
230 |
233 |
231 return True |
234 return True |
232 |
235 |
|
236 def _availableapis(repo): |
|
237 apis = set() |
|
238 |
|
239 # Registered APIs are made available via config options of the name of |
|
240 # the protocol. |
|
241 for k, v in API_HANDLERS.items(): |
|
242 section, option = v['config'] |
|
243 if repo.ui.configbool(section, option): |
|
244 apis.add(k) |
|
245 |
|
246 return apis |
|
247 |
233 def handlewsgiapirequest(rctx, req, res, checkperm): |
248 def handlewsgiapirequest(rctx, req, res, checkperm): |
234 """Handle requests to /api/*.""" |
249 """Handle requests to /api/*.""" |
235 assert req.dispatchparts[0] == b'api' |
250 assert req.dispatchparts[0] == b'api' |
236 |
251 |
237 repo = rctx.repo |
252 repo = rctx.repo |
245 return |
260 return |
246 |
261 |
247 # The URL space is /api/<protocol>/*. The structure of URLs under varies |
262 # The URL space is /api/<protocol>/*. The structure of URLs under varies |
248 # by <protocol>. |
263 # by <protocol>. |
249 |
264 |
250 # Registered APIs are made available via config options of the name of |
265 availableapis = _availableapis(repo) |
251 # the protocol. |
|
252 availableapis = set() |
|
253 for k, v in API_HANDLERS.items(): |
|
254 section, option = v['config'] |
|
255 if repo.ui.configbool(section, option): |
|
256 availableapis.add(k) |
|
257 |
266 |
258 # Requests to /api/ list available APIs. |
267 # Requests to /api/ list available APIs. |
259 if req.dispatchparts == [b'api']: |
268 if req.dispatchparts == [b'api']: |
260 res.status = b'200 OK' |
269 res.status = b'200 OK' |
261 res.headers[b'Content-Type'] = b'text/plain' |
270 res.headers[b'Content-Type'] = b'text/plain' |
285 |
294 |
286 API_HANDLERS[proto]['handler'](rctx, req, res, checkperm, |
295 API_HANDLERS[proto]['handler'](rctx, req, res, checkperm, |
287 req.dispatchparts[2:]) |
296 req.dispatchparts[2:]) |
288 |
297 |
289 # Maps API name to metadata so custom API can be registered. |
298 # Maps API name to metadata so custom API can be registered. |
|
299 # Keys are: |
|
300 # |
|
301 # config |
|
302 # Config option that controls whether service is enabled. |
|
303 # handler |
|
304 # Callable receiving (rctx, req, res, checkperm, urlparts) that is called |
|
305 # when a request to this API is received. |
|
306 # apidescriptor |
|
307 # Callable receiving (req, repo) that is called to obtain an API |
|
308 # descriptor for this service. The response must be serializable to CBOR. |
290 API_HANDLERS = { |
309 API_HANDLERS = { |
291 wireprotov2server.HTTPV2: { |
310 wireprotov2server.HTTPV2: { |
292 'config': ('experimental', 'web.api.http-v2'), |
311 'config': ('experimental', 'web.api.http-v2'), |
293 'handler': wireprotov2server.handlehttpv2request, |
312 'handler': wireprotov2server.handlehttpv2request, |
|
313 'apidescriptor': wireprotov2server.httpv2apidescriptor, |
294 }, |
314 }, |
295 } |
315 } |
296 |
316 |
297 def _httpresponsetype(ui, proto, prefer_uncompressed): |
317 def _httpresponsetype(ui, proto, prefer_uncompressed): |
298 """Determine the appropriate response type and compression settings. |
318 """Determine the appropriate response type and compression settings. |
325 # setting a very high compression level could lead to flooding |
345 # setting a very high compression level could lead to flooding |
326 # the server's network or CPU. |
346 # the server's network or CPU. |
327 opts = {'level': ui.configint('server', 'zliblevel')} |
347 opts = {'level': ui.configint('server', 'zliblevel')} |
328 return HGTYPE, util.compengines['zlib'], opts |
348 return HGTYPE, util.compengines['zlib'], opts |
329 |
349 |
|
350 def processcapabilitieshandshake(repo, req, res, proto): |
|
351 """Called during a ?cmd=capabilities request. |
|
352 |
|
353 If the client is advertising support for a newer protocol, we send |
|
354 a CBOR response with information about available services. If no |
|
355 advertised services are available, we don't handle the request. |
|
356 """ |
|
357 # Fall back to old behavior unless the API server is enabled. |
|
358 if not repo.ui.configbool('experimental', 'web.apiserver'): |
|
359 return False |
|
360 |
|
361 clientapis = decodevaluefromheaders(req, b'X-HgUpgrade') |
|
362 protocaps = decodevaluefromheaders(req, b'X-HgProto') |
|
363 if not clientapis or not protocaps: |
|
364 return False |
|
365 |
|
366 # We currently only support CBOR responses. |
|
367 protocaps = set(protocaps.split(' ')) |
|
368 if b'cbor' not in protocaps: |
|
369 return False |
|
370 |
|
371 descriptors = {} |
|
372 |
|
373 for api in sorted(set(clientapis.split()) & _availableapis(repo)): |
|
374 handler = API_HANDLERS[api] |
|
375 |
|
376 descriptorfn = handler.get('apidescriptor') |
|
377 if not descriptorfn: |
|
378 continue |
|
379 |
|
380 descriptors[api] = descriptorfn(req, repo) |
|
381 |
|
382 v1caps = wireproto.dispatch(repo, proto, 'capabilities') |
|
383 assert isinstance(v1caps, wireprototypes.bytesresponse) |
|
384 |
|
385 m = { |
|
386 # TODO allow this to be configurable. |
|
387 'apibase': 'api/', |
|
388 'apis': descriptors, |
|
389 'v1capabilities': v1caps.data, |
|
390 } |
|
391 |
|
392 res.status = b'200 OK' |
|
393 res.headers[b'Content-Type'] = b'application/mercurial-cbor' |
|
394 res.setbodybytes(cbor.dumps(m, canonical=True)) |
|
395 |
|
396 return True |
|
397 |
330 def _callhttp(repo, req, res, proto, cmd): |
398 def _callhttp(repo, req, res, proto, cmd): |
331 # Avoid cycle involving hg module. |
399 # Avoid cycle involving hg module. |
332 from .hgweb import common as hgwebcommon |
400 from .hgweb import common as hgwebcommon |
333 |
401 |
334 def genversion2(gen, engine, engineopts): |
402 def genversion2(gen, engine, engineopts): |
360 _('requested wire protocol command is not available over ' |
428 _('requested wire protocol command is not available over ' |
361 'HTTP')) |
429 'HTTP')) |
362 return |
430 return |
363 |
431 |
364 proto.checkperm(wireproto.commands[cmd].permission) |
432 proto.checkperm(wireproto.commands[cmd].permission) |
|
433 |
|
434 # Possibly handle a modern client wanting to switch protocols. |
|
435 if (cmd == 'capabilities' and |
|
436 processcapabilitieshandshake(repo, req, res, proto)): |
|
437 |
|
438 return |
365 |
439 |
366 rsp = wireproto.dispatch(repo, proto, cmd) |
440 rsp = wireproto.dispatch(repo, proto, cmd) |
367 |
441 |
368 if isinstance(rsp, bytes): |
442 if isinstance(rsp, bytes): |
369 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp) |
443 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp) |