# HG changeset patch # User Gregory Szorc # Date 2018-03-13 18:17:10 # Node ID b6a7070e7663901c6635abd2c412a2ff5fa2b931 # Parent fc89398256328f1c1a3b541cf7829e6aa1b3c6ff debugcommands: support sending HTTP requests with debugwireproto We implement an action that can issue an HTTP request. We can define headers via arguments and specify a file to use for the HTTP request body. The request uses the HTTP peer's opener, which is already configured for auth, etc. This is both good and bad. Good in that we get some nice behavior out of the box. Bad in that some HTTP request headers are added automatically. Differential Revision: https://phab.mercurial-scm.org/D2841 diff --git a/mercurial/debugcommands.py b/mercurial/debugcommands.py --- a/mercurial/debugcommands.py +++ b/mercurial/debugcommands.py @@ -14,6 +14,7 @@ import errno import operator import os import random +import re import socket import ssl import stat @@ -2692,6 +2693,24 @@ def debugwireproto(ui, repo, path=None, This action MUST be paired with a ``batchbegin`` action. + httprequest + --------------------------- + + (HTTP peer only) + + Send an HTTP request to the peer. + + The HTTP request line follows the ``httprequest`` action. e.g. ``GET /foo``. + + Arguments of the form ``: `` are interpreted as HTTP request + headers to add to the request. e.g. ``Accept: foo``. + + The following arguments are special: + + ``BODYFILE`` + The content of the file defined as the value to this argument will be + transferred verbatim as the HTTP request body. + close ----- @@ -2754,6 +2773,7 @@ def debugwireproto(ui, repo, path=None, stdin = None stdout = None stderr = None + opener = None if opts['localssh']: # We start the SSH server in its own process so there is process @@ -2909,6 +2929,42 @@ def debugwireproto(ui, repo, path=None, ui.status(_('response #%d: %s\n') % (i, util.escapedata(chunk))) batchedcommands = None + + elif action.startswith('httprequest '): + if not opener: + raise error.Abort(_('cannot use httprequest without an HTTP ' + 'peer')) + + request = action.split(' ', 2) + if len(request) != 3: + raise error.Abort(_('invalid httprequest: expected format is ' + '"httprequest ')) + + method, httppath = request[1:] + headers = {} + body = None + for line in lines: + line = line.lstrip() + m = re.match(b'^([a-zA-Z0-9_-]+): (.*)$', line) + if m: + headers[m.group(1)] = m.group(2) + continue + + if line.startswith(b'BODYFILE '): + with open(line.split(b' ', 1), 'rb') as fh: + body = fh.read() + else: + raise error.Abort(_('unknown argument to httprequest: %s') % + line) + + url = path + httppath + req = urlmod.urlreq.request(pycompat.strurl(url), body, headers) + + try: + opener.open(req).read() + except util.urlerr.urlerror as e: + e.read() + elif action == 'close': peer.close() elif action == 'readavailable': diff --git a/tests/test-http-protocol.t b/tests/test-http-protocol.t --- a/tests/test-http-protocol.t +++ b/tests/test-http-protocol.t @@ -226,4 +226,39 @@ Test listkeys for listing namespaces s> phases response: bookmarks \nnamespaces \nphases +Same thing, but with "httprequest" command + + $ hg --verbose debugwireproto --peer raw http://$LOCALIP:$HGPORT << EOF + > httprequest GET ?cmd=listkeys + > accept: application/mercurial-0.1 + > user-agent: mercurial/proto-1.0 (Mercurial 42) + > x-hgarg-1: namespace=namespaces + > EOF + using raw connection to peer + s> sendall(*, 0): (glob) + s> GET /?cmd=listkeys HTTP/1.1\r\n + s> Accept-Encoding: identity\r\n + s> accept: application/mercurial-0.1\r\n + s> user-agent: mercurial/proto-1.0 (Mercurial 42)\r\n (glob) + s> x-hgarg-1: namespace=namespaces\r\n + s> host: $LOCALIP:$HGPORT\r\n (glob) + s> \r\n + s> makefile('rb', None) + s> readline() -> 36: + s> HTTP/1.1 200 Script output follows\r\n + s> readline() -> 28: + s> Server: testing stub value\r\n + s> readline() -> *: (glob) + s> Date: $HTTP_DATE$\r\n + s> readline() -> 41: + s> Content-Type: application/mercurial-0.1\r\n + s> readline() -> 20: + s> Content-Length: 30\r\n + s> readline() -> 2: + s> \r\n + s> read(30) -> 30: + s> bookmarks \n + s> namespaces \n + s> phases + $ killdaemons.py